Closed14

Python の三項演算子がわかりにくい理由についてひとり妄想する会

nihaonihao

結論

Python の三項演算子 if が読みにくいと感じる理由は

  1. はじまりとおわりがわからない
  2. 式1 if 条件1 else 式2式1 条件1 条件2 式2 という形になっていて語順に規則性がない
a = 1 if 2 == 2 else 0

◯ 予約語を前置する

  1. はじまりとおわりがわからない
    • () で括る
    • if を先頭に持ってくる
  2. 式1 if 条件1 else 式2式1 条件1 条件2 式2 という形になっていて語順に規則性がない
    • if 条件 式1 else 式2 の形にする
a = (if 2 == 2: 1 else: 0)

上記の書き方で既存の書き方を統一してみる

a = 1 if 2 == 2 else 0
b = (2 * i for in range(10))
c = lambda x: 2 * x
d = (e := 1)
a = (if 2 == 2: 1 else: 0)
b = (for i in range(10): 2 * i)
c = (lambda x: 2 * x)
d = (e := 1)

自分は以下のほうが好き

  1. 括弧なし
a = if 2 == 2: 1 else: 0
b = for i in range(10): 2 * i
c = lambda x: 2 * x
d = e := 1
  • d = as e: 1, d = as e = 1 ??? これは汚いか...

◯ 予約語を後置する

  1. 値であることを強調するために式を前に置く(リスト内包表記は可読性がよいと仮定する)
  2. :() といった記号を使わない(はじまりとおわりがわからなくなるけど...)

上記の書き方で既存の書き方を統一してみる

a = 1 if 2 == 2 else 0
b = (2 * i for in range(10))
c = lambda x: 2 * x
d = (e := 1)
a = 0 else 1 if 2 == 2
b = 2 * i for i in range(10)
c = 2 * x lambda x
d = 1 as e
  • 0 else 1 if 2 == 2 は結構無理やりだけど、個人的にこれは納得感が有る... 式1 条件1 条件2 式2式2 条件2 条件1 式1 で比べたら... 単体で比べると酷いけど考え方を統一させているなら...

どの書き方も綻びがでてしまうけど、書き方や考え方を統一させてほしい...


# 偽 Python
# ワンライナーは lambda, for, if などの予約語を後置するとすれば
# (i) 統一感がでるのではないのか?
# (ii) コロン `:` という記号も書かなくてよくなる...
c = 2 * x lambda x

d = 1 as e
# 偽 Python

# (i) lambda が後置だったら...
map(2 * x lambda x, range(10))

# (ii) さらに Iterable 型のオブジェクトに map メソッドが定義されていてら...
range(10).map(2 * x lambda x)

以下いろいろ妄想したこと...

前置

ワンライナーは if, for, lambda といった予約語を後置する。値を前に出し、値であることを強調する。

文をワンライナーにすると式になり末尾の値が自動的に return されるってしてたら、よかったのかな...

# 偽 Python

## 1. 三項演算子
a = if i % 2: 値1 else: 値2

## 2. ジェネレータ式
b = for i in range(10): 2 * i
c = for i in range(10): if i % 2: i

## 3. ラムダ式
d = range(10).map(def x: 2 * x)

いっそ代入式も倒置してしまえば...?結論が頭に来てほしいという思いがあるなかだとこれだと厳しいか...

# ユーザーからの入力を繰り返し受け取ります。
# 'quit' が入力されるまでループします。
while (input("コマンドを入力してください(終了するには 'quit' と入力): ")) != 'quit' = command:
    print(f"入力されたコマンド: {command}")

2 が通常のワンライナーの文法とバッティングしてしまう...

# これはそのまま動いてしまう...
for i in range(10): 2 * i

# こっちはだめだけど
# for i in range(10): if i % 2: i

そこで倒置してこれを回避する...

https://x.com/lavender_k4f/status/1779534114473078852

nihaonihao

後置

予約語を倒置させる...

# 偽 Python

## 1. 三項演算子
### コロン ; で節を区切る...
a = 値1 if i % 2; 値2 else
                ^ コロンは使いたくないな... Python はあまり記号使わない言語だし...

値を前に出す -> コロン; のような区切りは汚い -> else を前に出して...
a = 値1 if i % 2 else 値2 にしよう...
みたいな流れかな

### 値を前に出す
a = 値1 else 値2 if i % 2

## 2. ジェネレータ式
b = 2 * 1 for i in range(10)
c = i for i in range(10) if i % 2

## 3. ラムダ式
d = range(10).map(2 * x def x)

倒置させると文法の一貫性がなくなる気がするけど
倒置させて予約語を後ろにもってきて式を頭に持ってきたほうが値かんがある... 認知負荷低い?
式ではなく値であることを 強調したい という思いの中で if を倒置したのかもしれない...

倒置(とうち)とは、言語において通常の語順を変更させることである。表現上の効果を狙ってなされる修辞技法の1つで、強調の一つである。
倒置 - Wikipedia

特に Python は print 文を print 関数にしたように文と式をなるべくわけたいという思いが強い言語...
文を表す予約語なのにあれ?式だったのは負荷が大きい気がする...

if の三項演算子は、理解しにくいけど、ジェネレータ式が倒置されていると考えると一貫性はあるよな... PEP みてもよくわからないけど、たぶんジェネレータ式(リスト内包表記)の後置する形式に揃えたんだろうな...

リスト内包表記は [ ] があるから頭からよんですぐにあ、リストだってわかるけど三項演算子 if の場合はそれがないからわかりにくいのかな... 頭に ? でもつければわかりやすくなるかな...

a = ? 値1 if 条件 else 値2

うーん厳しいか... Python はあんまり記号をつかわないからその点でもあんまりよくない気がする...

  • 比較演算子
    • is
    • and
    • or

カッコが妥当かな... 代入式みたいに...

a = (値1 if 条件 else 値2)

Guido が意図的に見にくくしたってコメントしてたらしいけど、多分意図的に見にくくしたのは2つ以上の三項演算子を繋げられないようにするために、ってことなんじゃないのかな... 原文をまったくよんだことないけど... どこにあるんだろう?

ワンライナーは後置する で一貫性をそろえるなら lambda 式も後置してよかったのでは?感がある... そうすると文法の一貫性があがって認知負荷がさがったのではないのかな...

if が読みにくいのは、if の問題ではなく lambda 式のほうの問題だろうな...

lambda を前置にしたら読みにくくなるのかな?頭から読んでも式だから、あんまり負荷が無い気がする... 個人の感想... どうせ "無名" 関数だしね... なにも断りなく式を書いてもいいっしょ... 引数に関数を取る関数なら特に...

デメリットはローカル変数がいきなりでてきてビビる... 言うて長いの書かないしな... リスト内包表記の for i の後置と一緒か

予約語を後置すると 1 値であることを強調できる、また 2 : がいらなくなって記号を好まない Python とは相性がよい?隠れたメリット

f = lambda x: 2 * x
f = 2 * x lambda x

いっそもっと過激にしたい...

a = (i for i in range(10))
b = (b else 0 if b is None)  
c = (2 * x lambda x)
b = (b else 0 if b is None)  
偽のときの値 else 真のときの値 if 条件

# ただ、過激にしても末尾の if が前と後ろどっちにかかっているのかわかりにくい...
(偽のときの値 else) (真のときの値 if 条件)

考え方

  1. 値, 式を前に出す...
  2. ひっくり返した書き方

def じゃなくて lambda って長い予約をつかったんだろう... Guido は lambda が嫌いって言ってたけど...

nihaonihao

Guido は関数表記 >> メソッド表記 って len の説明のときに言っていたけど、処理を組み合わせるときはメソッドのほうがわかりやすいな... 関数でやるならパイプライン演算子 |> があるとありがたいか...

[i for i in range(10)] |> len

読みにくいって思ったときに

  • なにが読みにくいのか?
  • なぜ読みにくいのか?
  • 過去の設計思想はなにか?
  • 現代はどう変化してしまったのか?
  • 過去の将来予測はなにか?
  • なにが外れたのか?
  • いまならどう書くのが望ましいのか?

みたいなことを合わせて併記すると面白いのかなと個人的におもった...


余談ですが、Pythonの以下 3つだけはどうしても好きになれません。慣れはしましたが。

リスト内包表記
lambda
aaaa if x = y else bbbb


これからは AI が表記の登場頻度とかをもとに文法も提案してくれそうだから、あんまり考えたりする必要もないのかもしれないけど...

頻出するものは短く糖衣構文を設ける、頻出しないものは頑張って長く書いてもらう...

nihaonihao
# イコール、または式が求められる箇所では文は式になる。

REPL, 仮引数..., = での代入
そんなことできるのかな?


list = for i in range(10): i
result = if True: "Hello" else: "Nihao"
-> result = "Hello" if True else "Nihao"

const a = cond
  ? a
  ? b
 


# 倒置

f = def _(x): 2 * x
a = 1
# lambda, 特殊記法
nihaonihao

if の三項演算子がなんでよみにくいんだろう?

  • 頭から読んで if の三項演算子と気づけない
  • 値1, 値2 の関係は等価なのに、間に条件がはいっている
    • 変数 = 値1 if 条件 else 値2
    • いっそこっちのほうがわかりやすそう...
      • 変数 = if 条件 値1 else 値2

いろいろ考えたけど消去法的にそうせざるを得なかったのではないのだろうか?
ほかの候補例が PEP にあがっていたからよんでみよう...
「読みやすい」というより「選考の思考過程」について考えたほうがおもしろいかもしれないかな...🤔

# むりくり書式を整えようとすると以下のようになるけど、いろいろ問題がある...
値1 if 条件; 値2 else

# 1. `if 条件; 値2` これが既存の文法とバッティングしてくっついているように見える...
# 2. `;` などの記号を導入しないといけない、一貫性にかける...
# 3. もはや末尾の `else` がいらない... -> あれ? else を前に持ってきて `;` を消せば...

JavaScript は後者側、積極的に短い書き方を採用していくので ? をもとに if を除いているのか?

nihaonihao

歴史的経緯について

  1. 値と式を区別するために予約語を後置したい思いがある
  2. リスト内包表記に引きづられて予約後は後置したい思いがある
  3. 予約語を先頭にもってくると複文を1行で書く文法とバッティングする
  4. 予約語を先頭にもってくると、どうしても : などの記号を導入しないといけなくなる
  5. Python は記号は避けたい言語なのではないのか?例えば is, not, or, and ほかにも { } を使わずインデントを用いる記法
    5.1. 記号を多用する言語は Rust
    5.2. 記号を多用されると読みにくいって SFITB さんがおっしゃっていた...
    5.3. Python は ABC 言語という初心者向けの言語に影響を受けているという歴史的背景がある...

  • 流れ
  • len function -> : とバッティングするのを回避したい -> list comprehension -> if expression
  • Guido は len を関数にしたようにメソッドの記法にも懐疑的だった、これはメソッドの記法のそのもの問題ではなく実装による継承を積極的に行わせるためにオブジェクトの名前空間をあけるために、そのような思考になってしまったのではないのか?

◯ リスト内包表記読みにくいのでは?説...

リスト内包表記って現実的には引数 for i を式の後ろに後置しているから読みやすいものではないのではないのか?それが通るなら lambda の後置だって読みやすいって主張が通ってしまう... ぱっとみのコードのブロックが頭の [for がつくからああ、リスト内包表記かってすぐわかるけど...

いわゆる数学の集合の表記を見慣れてればわかりやすいかもしれないけど、みなれてなかったらわかりにくい... のでは?

  1. len が関数であった、メソッドを避けたかった
  2. 集合の表記と見慣れていた
  3. : によるワンライナーとのバッティングをさけたかった...
nihaonihao

三項演算子 if は括弧を強制されていないので、場合によってはよみにくい...

1 + 1 if False else 0
# 2

(1 + 1) if False else 0
# 2

1 + (1 if False else 0)
# 1

代入式, ジェネレータ式は括弧を強要されている

# a := 0
# SyntaxError
(a := 0)
# 0

# i for i in range(10)
# SyntaxError
(i for i in range(10))
# <generator object <genexpr> at 0x7c4870cfd9c0>

文法ではなくて PEP8, autopep8 で縛ればよかったのでは、的な気もしなくもなくはない...

括弧が値を強調するためのものと考えると関数の引数として与えるときはその制限が緩められるのもなっとく感がある...

# 1.
f = lambda x: x
f(a:=1)
# 1

a
# 1
# この機能はうれしいのか...
# 引数に代入式を渡すことってあるのか...

# 2.
sum(i for i in range(10))
# 45
# ジェネレータ式はうれしい気がする...

いまの Python において () は値であることを強調する意味がある。() がタプルという解説もあるが () は値であることを強調する使われ方もする。カンマ , がタプルを表しで 括弧() 値を強調していると把握するのほうが統一感のある覚え方かもしれない。でも初学者には逆にわかりにくいか... ただあとになって、代入式やジェネレータ式バッティングしだす... これら2つの括弧は値であることを強調するためのものであるとしたほうが統一感が有る...

# 括弧 () が必要ない...
# カンマ, があればタプルである 

a = 0, 1
a
(0, 1)
# REPL の表示は括弧でくくられてしまいますが...

b = 0, 1,
b
# (0, 1)

c = 0,
c
# (0, )

# d =
# SyntaxError ... これが SyntaxError になるのを見ると 0 の概念って難しいのかな...
d = ()
d
# ()
nihaonihao

将来について

if 式, match 式を採用するなら整合性をどう取るべきなんだろう... 以下は Rust

// if 式
fn main() {
    let number = 20;
    let message = if number < 10 {
        "less than 10"
    } else if number < 20 {
        "between 10 and 19"
    } else if number == 20 {
        "exactly 20"
    } else {
        "greater than 20"
    };

    println!("The number is: {}", message);
}
// match 式
fn main() {
    let number = 5;
    let message = match number {
        1 => "one",
        2 => "two",
        3 => "three",
        4 => "four",
        _ => "something else",
    };

    println!("The number is: {}", message);
}

式と文の違いを明確にしつつ、Rust のような文法を許容する実装はありうるのだろうか?

「変数に代入されないものは文である」というルールを設けると、None 型を返す関数を呼び出す形式とバッティングしてしまい、文と式を明示的にわけている Python の良さみたいなものが失われてしまう。ような気がする。

いっそ for 文も, if 文も式であるとすればよいのか?でも、そうすると for 文がジェネレータ式を返すことになって記述した箇所で実行できなくなってしまうし、if 文は節の末尾が代入文だったときにバッティングしてしまう...

リスト内包表記が登場した背景は len が関数であるのと同様の理由だと思っている。リスト内包表記は map メソッドのほうがよいのでは?という観点から行くと、倒置することによって値をであることを強調し、文と式を明示的にわける表記手法はなくなる。

変数 = 予約語 ...

の場合は式であるっていう例外規定を設けたほうが、文と式を明示的にわけることを犠牲にしても、きれいになるような気がする...


いまは map, filter を積極的に使って1つ1つのデータに名前、変数をつけて Step by Step でやっていきましょうね, map, filter で都度データを生成し immutable にやりましょうね、っていうのが一定のコンセンサスが得られたらから if 式とかも採用しやすいけど、Python が生まれた頃はそんなこともなかっただろうし...

nihaonihao
data = [i for i in range(10)]
sum(1 for i in data if i % 2 == 0)
sum(i % 2 == 0 for i in data)
# data.filter(def x: x % 2 == 0).list().len()

# (... for i in data if ...)
# data.filter(...).map(...)

可読性的に メソッドチェーン >> ジェネレータ式, 関数 な感がある... メソッドチェーンは逐次的に処理が追える... ジェネレータ式も関数も計算の処理の流れが置いにくい気がする... ジェネレータ式, 右, 左に導線が動く... 視認性はいいんだけど... Guido はメソッドチェーンを嫌っていたのだろうか...

反面、ジェネレータ式は引数の定義は1度で済み for i, また map, filter といった語を書かなくても済み短くなる... in の分だけ長いか...

nihaonihao

self 面倒くさい問題

1. def に暗黙的に代入

2. method 定義文...

省略には一定の理がある... class Class(object):class Class: で省略できるようになった... 経緯がしりたい...

  • 継承は避ける文化になった...
  • 知っているのに書かれるのは煩雑、読みづらい...
func staticmethod(method):
    method.local_vars.del('it')
    return method

# 暗黙的に変数を定義できてしまうのはヤバい気がする...
meth classmethod(method):
    # nonlocal, global
    cls = it.type
    return method

# func classmethod(method):
#     method.local_vars.set('cls', it.type)
#     method.local_vars.del('it')
#     return method

func common_func(it):
    ...

meth common_method():
    common_func(it)


class Class:
    attr1: str
    attr2: mut str

    meth create():
        return Class("Hello, world!", "")

    meth my_method():
        it.attr2 = "Nihao, shijie!"

    @classmethod
    meth 

    @staticmethod
    meth my_staticmethod():
        print("Hello, world!")    

React, Vue.js, Svelte とかもみると...

self はやりすぎかな... でもわからなかったら easy ではなく "Explicit is better than implicit." は正しい態度だと思う...

でも self は false を返したほうがよい、論理的な一貫性よりも easy に倒したほうが良い例なので?


  • 継承はなんでダメだったんだろう?階層的にものごとを定義づけましょうって理にかなっている気がする...
  • len, super()

そんなにきれいに MECE にわけられない、ってことなのだろうか... 重なりなどが生じてしまう...


class Class:
    func staticmethod():
        ...

    func classmethod():
        ...
  • classmethod, staticmethod って組み込み変数, グローバル変数への参照を制限する機能になるからこれは特殊過ぎる...
  • 組み込み変数 -> グローバル変数 -> クロージャ → インスタンス → ローカル

classmethod と staticmethod の区別は存在しない...

class Class:
    @staticmethod
    def func():
        return Class()  # <--- classmethod にできる...
  • self を制約したい例ってあるのかな...
  • クラス内の staticmethod には self はつけてほしくなかったし...
  • クラスメソッドを呼び出す時は Class.func() にすればよいのでは?
  • もし正式に実装するなら... 実はスコープから取り除く操作が必要になるのでは?
  • これは meth を実装する場合と同じ
nihaonihao

3. 既存の文法で対応...

  1. クラスのインデントがあまり視認性の役に立っていない...
  2. self を何度も書くのはあまりにも煩わしい...
  • なら第一引数でクラスを明示させれば... Go 形式...
  • class Class(object): だって class Class: になったし...
  • 結論が頭に来てほしいことを考えるとデータ型、クラス定義文が下に来るのはわかりにくい...
class Person:
    pass

# lain
lain = Person()
lain.name = "岩倉玲音"
lain.age = 14

def print_person(person):
    print(f'{person.name=}, {person.age=}')

print_person(lain)


# arai_san
def initialize_person(person, name, age):
    person.name = name
    person.age = age

arai_san = Person()
initialize_person(arai_san, "アライさん", 17)
print_person(arai_san)

# add method
Person.print = print_person
lain.print()
arai_san.print()

# naruto
Person.__init__ = initialize_person
naruto = Person("うずまきナルト", 16)
naruto.print()
from dataclasses import dataclass

class Class(dataclass):
    attr1: str
    attr2: str

@method
def create(obj: Class):
    return Class("Hello, world!", "")

Class.create = create
nihaonihao

デコレータ, 引数を取る場合 @decorator(arg) と関数の引数を操作する場合...

違いがわからなくなってきた...

def my_classmethod(func):
    def wrapper(*args, **kwargs):
        # 第一引数としてクラスを渡す
        return func(args[0].__class__, *args[1:], **kwargs)
    return wrapper

class MyClass:
    @my_classmethod
    def my_class_method(cls, *args):
        print(f"Called class method with cls: {cls}")
        print(f"Arguments: {args}")

# 使用例
MyClass.my_class_method(1, 2, 3)  # クラスから直接呼び出す
instance = MyClass()
instance.my_class_method(4, 5, 6)  # インスタンスから呼び出す

  1. パラメータを取るデコレータ
  2. 引数を改変するデコレータ
  3. 前後に処理を加えるデコレータ
nihaonihao
  • クラスが小さいとき
    • method 定義でもよいのかも...
  • クラスが大きいとき、メソッドがたくさんついている時
    • 関数を後置してもよいのかも...
    • 最近はなるべくクラスを小さくしましょうねって聞いたような...
nihaonihao

self がダルい.., インデントがダルい... return したくない..., render 関数に引数を渡したくない

Person.py
class Person:
    name: str
    age: str

    def add_age(self):
        self.age += 1

    def render():
        return """\
<p>Name: {{ self.name }}</p>
<p>Age: {{ self.age }}</p>
"""
Person.py
name: str
age: int

def add_age():
    nonlocal age
    age += age

def render():
    return """\
<p>Name: {{ name }}</p>
<p>Age: {{ age }}</p>
"""
このスクラップは2024/04/12にクローズされました