本文を読み飛ばす

Pythonで組み込み関数と同名の変数を使うべきでない理由

Python では input()len() など、様々な組み込み関数を用意している。そして、これらと同じ名前で変数や関数を後から定義すると、それらを「上書き」できる。つまり、例えば input という名前の変数にファイルから読み出したデータを格納するようなコードを書くと、以後 input という名前は組み込み関数ではなく、読み出されたデータを指すようになるわけだ。

さて、言語仕様として許可されているコレは、お作法的には良いのか悪いのか。個人的には否定派だったのだけれど、「気持ち悪い」という主観以外に理由を挙げられずにいた。ところが先日、ついに否定の根拠になるような嫌な思いをする機会に恵まれた(?)ので、今日はそれについて書いておこうと思う。

体験例

いつもは結論を先に書くのだけれど、今回は趣向を変えて例から始めてみたい。

まず、次のようなプログラムを考えたい。標準入力から複数の数値を読み出して、行ごとに何個の数値が書かれているかを図示し、また和の表示も行っている。ただし行ごとの和を、組み込み関数 sum() と同じ sum という名前の変数に格納している。

import sys

for i, line in enumerate(sys.stdin):
    numbers = [int(n) for n in line.split()]
    bar = ""
    sum = 0
    for n in numbers:
        bar += "*"
        sum += n
    print(f"{i + 1:3d} {bar:20s} {sum:3d}")

実行例は次のような感じ。

$ cat numbers.txt
50 95 28
60 71 57 41 47 55 92 42 50
76 94 17 19 41
$ python counter.py < numbers.txt
  1 ***                  173
  2 *********            515
  3 *****                247

さて、ここで「和の計算については組み込み関数の sum()numbers を渡した方が意図が分かりやすいし、bar の文字列も "*" * 個数 で作れるからループは不要だな」などと思い、次のようにリファクタリングしたとしよう。

import sys

for i, line in enumerate(sys.stdin):
    numbers = [int(n) for n in line.split()]
    bar = "*" * len(numbers)  # NEW!
    total = sum(numbers)      # NEW!
    print(f"{i + 1:3d} {bar:20s} {sum}")

組み込み関数の sum() を使う都合上、和を格納していた sum という変数は total という名前に変えた。このプログラムには flake8mypy も特に警告を出さないし、実行してもエラーは出ない。実行すると次のような結果になる:

$ python counter.py < numbers.txt
  1 ***                  <built-in function sum>
  2 *********            <built-in function sum>
  3 *****                <built-in function sum>

…そう、エラーは出ない。どう考えてもダメな実行結果なのに、エラーも出さずに正常終了してしまう。

結論:組み込み関数と同名の変数を使うべきでない理由

組み込み関数と同じ名前の変数を使うと、後日リファクタリングでその変数名を変えるときに以下 2 つ問題が出てくるので、避けた方が良いと思う。

  1. リネーム漏れを見つけにくい
  2. リネーム漏れしたときに発生するエラーが分かりにくいか、エラーにならない

1 つ目は、変数のリネーム漏れが発生していてもツールで検出できずミスに気付きにくい、という問題。なにせ組み込み関数と同じ名前を使っていたのだから、リネーム漏れした個所は「組み込み関数を使った有効なコード」になってしまう。これに対して、もし元の変数名を sum_ のような組み込み関数とは違う名前にしてあったなら、たとえば flake8 は未定義変数だと警告を出してくれるし、走らせればそこでクラッシュするし、問題に気付きやすかったはず。

2 つ目は、(a)リネーム漏れが原因で起こるエラーのメッセージが分かりにくかったり、(b)エラーが起こらず動いてしまいトラブルシュートに苦労する、という問題。もし先のプログラム例の最後が {sum} ではなく {sum:3d} であった場合、TypeError: unsupported format string passed to builtin_function_or_method.__format__ というエラーメッセージが表示されるだろう。組み込み関数を 3 桁の十進数としてフォーマットしようとして「関数は十進数として表現できないよ」と怒られたわけだけれど、正直、何を間違えたのか分かりにくいと思う。また、実例で説明した先のプログラムでは「正しく動いていないのに動いているように見える」という最悪のバグが発生している状況で、誰にとっても嬉しくないと思う。

ということで、組み込み関数と同じ名前の変数の使用は避けた方が良いと思う。

以上、いつか誰かの参考になれば幸い。