Python 3.8 の概要 (その1) - Assignment expressions

古来、Pythonでは「代入は文であるべき!」と一貫して主張してきました。

C言語などでは、代入は足し算や掛け算と同じ、値を計算する「式」で、たとえば

a = (b=100) / 2;

と書くと、b には 100 を代入し、a100/2=50 を代入します。1+12 という値になる ですが、b=100 も同様に値が 100 となる なのです。

Pythonでは、代入は式ではないので、こういう書き方はできません。

Pythonの代入は、足し算などの演算子の仲間ではなく、iffor のような制御文の仲間で、あまり自由な書き方は出来ないのです。

Python FAQ では、その理由として

Python の式中での代入を許さない理由は、この構造によって起こる、他の言語ではありがちで見つけづらいバグです:
if (x = 0) {
    // error handling
}
else {
    // code that only works for nonzero x
}

と説明しています。

このC言語のコードでは、if 文の中で x = 0 と書いて変数 x に値を代入しています。しかし、このコードは

if (x == 0) {
    // error handling
}
else {
    // code that only works for nonzero x
}

の書き間違いである場合が多いのです。本来、 if (x == 0) { と書くべきところを、if (x = 0) { と書いてしまうという間違いですね。

この間違いは比較的発生しやすく、一見正しい処理に見えてしまうため発見しにくいBugの原因として知られています。多くのプロジェクトで、このエラーを回避するために

== で比較するときには、x == 0 ではなく 0 == x のように書く

というコーディング規約を定めています。

このように、定数を左側に記述するようにすると、間違えて === と書いてしまっても、0 = x と定数に変数を代入する式となるので、コンパイルエラーが発生してミスを発見できるためです。このような書き方は、ヨーダ記法 とも呼ばれます。

PEP 572 -- Assignment Expressions

Pythonは1990年代初頭にリリースされて以来30年近く、代入が演算子であるプログラミング言語をこんな風にDisり続けてきたわけですが、ここへ来て突如方針を転換します。君子は豹変するのです。

あるとき、Pythonの開発者メーリングリストで「PythonC言語のような代入演算子を導入しよう」という提案がありました。過去にも似たような提案がされており、やれやれまたか、まあ採用はされないだろうけどできるだけいろいろ議論してドキュメントにまとめ、今後似たような提案があったときに参照できるようにしよう、ということになりました。

多くの開発者はこの提案が受け入れられるとは思ってもいなかったし、Pythonの仕様決定権をもつPythonの父 Guido van Rossum 氏もそのつもりはなかったようです。しかし、議論が進むにつれ、なんとGuidoが代入演算子を受け入れる意向を示し始めました。

多くのPython開発者はこの提案に反対で、大変な議論が巻き起こりましたが、Guidoを始めとする推進派は丁寧に議論を進め、最終的に PEP 572 -- Assignment Expressions としてPythonに導入されることとなりました。

この議論で疲れ果てた Guido は、Pythonの最高権力者であるBDFL(Benevolent Dictator For Life:慈悲深き終身独裁官) からの退任を表明することとなります。

セイウチ演算子

このような紆余曲折を経て、Python 3.8では 代入式 が導入され、次のように書けるようになりました。

>>> a = (b:=100) / 2
>>> a, b
(50.0, 100)

= は従来どおり代入文で使われます。新しく追加された代入式は、:= 演算子を使用します。

いつのまにか、:= 演算子は「セイウチ演算子」(Walrus operator) と呼ばれるようになりました。そう言われてしまうと、もうセイウチにしか見えなくなります。

f:id:atsuoishimoto:20190903002638p:plain

:= 演算子はふつうの式として、ほとんどの処理で利用できます。

たとえば、これまで

from random import random

a = random()
b = random()

print(a, '+', b, '=', a+b)

と書いていた処理は、

from random import random

print(a:=random() '+', b:=random(), '=', a+b)

のように書けます。

また、if 文や for などの制御文で

v = dist_data.get(key, -1)
if v != -1:
    do_something(v)

のように書いていた処理は、

if (v:= dist_data.get(key, -1)) != -1:
    do_something(v)

と一行にまとめて書けるようになります。

:= 演算子が特に便利なのは、リスト内包などの式でしょう。

これまで、データの編集と判定を同時に行うような処理は、リスト内包ではすっきりと書けませんでした。

for item in data:
    text = item.strip()
    if text:
        result.append(text)

この処理をリスト内包で書こうとすると、

result = [text.strip() for text in data if text.strip()]

のように、text.strip() を2回呼び出すか、

result = [s for s in (text.strip() for text in data) if s]

のように、一つ余計にイテレータを作成する必要がありました。

しかし、:=演算子のおかげで

result = [t for text in data if (t:=text.strip())]

とすっきり記述できるようになりました。

注意点

:=演算子は、あいまいな書き方ができないようにするために、 () でくくらないと SyntaxError とされる場合があります。

>>> a = b:=2
  File "<stdin>", line 1
    a = b:=2
         ^
SyntaxError: invalid syntax

>>> x=(1+y:=2)
  File "<stdin>", line 1
SyntaxError: cannot use named assignment with operator

>>> dict(a=a:=10, b=a*2)
  File "<stdin>", line 1
    dict(a=a:=10, b=a*2)
            ^
SyntaxError: invalid syntax

上記のようなエラーは、:=演算子() でくくって範囲を明確にすると解消します。

>>> a = (b:=2)
>>> x = 1+(y:=2)
>>> dict(a=(a:=10), b=a*2)
{'a': 10, 'b': 20}

また、:= 演算子は非常に優先順位が低いので、複雑な式では () を使うことが多くなるでしょう。たとえば

[a := 1 +  2]

という式は、

[(a := (1 +  2))]

と評価され、 a の値は 3 となります。

カッコをつかって

[(a := 1) +  2]

と書けば、a1 となります。