Python の代入式ってなに?

代入すると値が返されます。

# 実行すると 0 が返されます。
a := 0

# 値が返されるので代入できます。
b = a := 1
b

>>> # 実行すると 0 が返されます。
>>> a := 0
0
>>> 
>>> # 値が返されるので代入できます。
>>> b = a := 1
>>> b
1
>>>

代入文と代入式を区別することが大切です。

>>> # 代入文 ... 値が返されない。
>>> a = 1
>>> 
>>> # 代入式 ... 値が返される。
>>> a := 1
1
>>> 

PEP 572 で Python 3.8 から代入式が使えるようになりました。 PEP 572 には、 なぜ代入式が必要であるかの理由と、 代入式があればいかにコードが綺麗になるかの具体例が、書かれています。

ここでは代表例を3つ紹介しつつ、代入式がいかにコードが綺麗になるかと。 代入式を使わない場合の方法をそれぞれ示します。

1. 階段状にネストする if 文

標準ライブラリ copy のコードですね

# 1. 問題のコード
reductor = dispatch_table.get(cls)
if reductor:
    rv = reductor(x)
else:
    reductor = getattr(x, "__reduce_ex__", None)
    if reductor:
        rv = reductor(4)
    else:
        reductor = getattr(x, "__reduce__", None)
        if reductor:
            rv = reductor()
        else:
            raise Error(
                "un(deep)copyable object of type %s" % cls)
# 2. 代入式による改善策
if reductor := dispatch_table.get(cls):
    rv = reductor(x)
elif reductor := getattr(x, "__reduce_ex__", None):
    rv = reductor(4)
elif reductor := getattr(x, "__reduce__", None):
    rv = reductor()
else:
    raise Error("un(deep)copyable object of type %s" % cls)
# 3. 代入文による代替策
#     switch-case 文みたいになった笑
for _ in range(1):
    reductor = dispatch_table.get(cls)
    if reductor:
        rv = reductor(x)
        break
    
    reductor = getattr(x, "__reduce_ex__", None)
    if reductor:
       rv = reductor(4)
       break
    
    reductor = getattr(x, "__reduce__", None)
    if reductor:
        rv = reductor()
        break

else:
    raise Error("un(deep)copyable object of type %s" % cls)

2. リスト内包表記の中で2度実行される関数

# 1. 問題のコード
new_lst = [f(x) for x in lst if f(x) > 0]
# 2. 代入式による改善策
new_lst = [y for x in lst if y:=f(x) > 0]
# 3. map による代替策
new_lst = [y for y in map(f, lst) if y > 0]

# 3. ジェネレータ式による代替策
new_lst = [y for y in (f(x) for x in lst) if y > 0]

map でいいんじゃないかなと思ったりもします。 Guido は map が嫌いですが、すでに定義された関数を使う場合は map は有効だと思います。

◯ コメントの代わりに...

リスト内包表記, ジェネレータ式を説明的に書きたいときがあるので、 そういう時に、冗長ですが、こういう書き方も無きにしもあらずかな... と。

# 0.
# x ってなんだろう... と思って if を見に行く
print(x for x in lst if x % 2)

# 1.
# odd 奇数か...
print(odd := x for x in lst if x % 2)

組み込み型の iterable を列挙するコード。

# 0
print(*(cls for cls in __builtins__.__dict__.values() 
    if isinstance(cls, type) and hasattr(cls, '__iter__')), sep="\n")

# 1.
print(*(builtin_iterable_type := cls for cls in __builtins__.__dict__.values()
    if isinstance(cls, type)) and hasattr(cls, '__iter__'), sep="\n")

3. do-while 文が無いことによる煩雑さ

# 1. 問題のコード
while True:
    data = sock.recv(8192)
    print("Received data: ", data)
    if not data:
        break
# 2. 代入式による改善策
while data := sock.recv(8192):
    print("Received data:", data)
# 3. 代入文による代替策
data = sock.recv(8192)
while data:
    print("Received data:", data)
    data = sock.recv(8192)

問題のコードが、そんなに問題じゃないかなと思ったりします。

4. 代入式の何が問題か..

わからない。

代入を見逃してしまう、危険性が指摘されています。

[x * y for x in lst if y:= f(x)]
#                      ^^^^^^^^ 後ろで代入されている。

今までは複雑な式を読み飛ばしても代入を見逃す危険は無かったのが、 PEP 572 が承認された場合は、自分が代入を見逃していないか疑いながらコードを読む必要が出てきます。 (引用したものの具体的に何をさしているか、理解できていません... orz)
最近のPython-dev(2018-04) - DSAS 開発者の部屋

5. Guido の引退

代入式は賛否両論というか、一大論争を巻き起こしたらしく...

PEP 572 の議論は大炎上しました。 5月にLanguage Summit があってそこで議論のオーバーヒートをどう扱うかなどの話がされたのですが、 LWN.net がその まとめ を作ってくれたところから、 またML上での議論が再開し長大なスレッドになっています。。。

結果的に Guido は BDFL から引退してしまいました。

いま PEP 572 の仕事は、終わった。 私はこれ以上 PEP のために戦いたくないし、 多くの人が私が決定したことを侮蔑するのを聞きたくない。
Now that PEP 572 is done, I don't ever want to have to fight so hard for a PEP and find that so many people despise my decisions.
和訳 Transfer of power - Guido van Rossum