Closed9

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

nihaonihao

結論

Python の三項演算子 if が読みにくいと感じる理由は、三項演算子 if そのものではなく lambda にあるのではないかな...

# ワンライナーは基本式が前に来る...
a = (i for in range(10))
b = ... if ... is not None else 0

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

さらに括弧 ( ) で括って式であることを強調

a = (i for i in range(10))
b = (... if ... is not None else 0)
# c = (2 * x lambda x)
d = (e := 0)
# 偽 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 を関数にしたようにメソッドの記法にも懐疑的だった、これはメソッドの記法のそのもの問題ではなく実装による継承を積極的に行わせるためにオブジェクトの名前空間をあけるために、そのような思考になってしまったのではないのか?
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 の分だけ長いか...

このスクラップは19日前にクローズされました