🦫

Pythonでウォルラス演算子を使う際には変数スコープに注意する

2023/09/30に公開

代入式・ウォルラス(セイウチ)演算子とは

代入式はウォルラス演算子(:=)で変数に値を割り当てる新しい構文で、Python 3.8から導入された機能になります。

通常の代入文と同じように変数への代入を実行するだけでなく、その結果を式全体の値として返します。

・・・といってもちょっと分かりづらいと思うので、通常の代入文との比較を交えて説明したいと思います。

通常の"代入文"を使った方法

import random

def get_some_integer():
    return random.randint(0, 5)

# 通常の方法
value = get_some_integer()
if value:
    print(f"{value} is non-zero")
else:
    print(f"{value} is zero")

"代入式"(ウォルラス演算子)を使用した方法

・・・

# ウォルラス演算子を使用
if value := get_some_integer():
    print(f"{value} is non-zero")
else:
    print(f"{value} is zero")

:= により「変数への代入」かつ「評価」がまとめて行われるため、1行節約できスッキリしました。

この他にも、ウォルラス演算子は正規表現によるマッチでの利用など有用なユースケースが多く存在します

代入式を使う上での注意点

便利な代入式ですが、使う上でいくつか注意点があります。
本記事では、その中でも変数スコープについて書いておきたいと思います。

ポイント1:条件文・ループ文のブロック外でも参照可能

前述のif文の例では value のスコープがif文のブロック内であることを明示的に記述できたように見えます。

ただ、これには落とし穴があります。
試しにif文のブロック外で value 変数を参照してみましょう。

# ウォルラス演算子を使用
if value := get_some_integer():
    print(f"{value} is non-zero")
else:
    print(f"{value} is zero")
    
print(value)
実行結果
5 is non-zero
5

エラーなく参照できてしまいました。
このようにスコープを限定しているつもりでウォルラス演算子を使うと事故の原因になり得るので注意が必要そうです。

(実は代入式を使わなくても・・・)

実は代入式を使わない場合でもリークは発生します。

my_list = [10, 21, 32, 43, 54]

for item in my_list:
    quotient = item // 10
    remainder = item % 10

print(f"item: {item}, quotient: {quotient}, remainder: {remainder}")

↑の結果は↓となりエラーは発生しません。

実行結果
item: 54, quotient: 5, remainder: 4

このリークについては、例えばC言語などの他言語だと以下のようにエラー発生するため特に注意が必要そうです。

scope_test.c
#include <stdio.h>

int main(void){
  int i;

  for (i = 1; i <= 3; i++){
    int triple_value = i * 3;
  }
  printf("triple_value: %d\n", triple_value);

  return 0;
}
コンパイル結果
$ gcc scope_test.c
scope_test.c:9:32: error: use of undeclared identifier 'triple_value'
  printf("triple_value: %d\n", triple_value);
                               ^
1 error generated.

ポイント2:内包表記で代入式を使用する場合もリークする

代入式の活用先として内包表記内での使用もありますが、こちらも内包表記外にリークします。

my_list = [10, 21, 32, 43, 54]

limited_quotient_list = [
    hundred_quotient := quotient * 100 for item in my_list if (quotient := item // 10) < 3
]

print(limited_quotient_list)
print(f"hundred_quotient: {hundred_quotient}, quotient: {quotient}")
実行結果
[100, 200]
hundred_quotient: 200, quotient: 5

続いて、ループ用の変数 item を参照しようとしてみます。

・・・
print(f"hundred_quotient: {hundred_quotient}, quotient: {quotient}")
print(item) # ループ用変数を外部から参照してみる
実行結果
[100, 200]
hundred_quotient: 200, quotient: 5
Traceback (most recent call last):
  File "/Users/miotava/Documents/for_zenn/test3.py", line 9, in <module>
    print(item)
          ^^^^
NameError: name 'item' is not defined. Did you mean: 'iter'?

エラーになり、ループ用変数は外部からは参照できないことがわかりました。

このようなバグになりやすいブロック外の変数利用は、エディタ(VS Codeなど)にFlake8などのリンターを入れておけばアラート出してくれるので避けやすいです。

まとめ

用法・用量を守って楽しいウォルラスライフを送りましょう!

参考

Effective Python第2版

スペースマーケット Engineer Blog

Discussion