静かなる名辞

pythonとプログラミングのこと



【python】クラス変数のスコープには注意が必要

 pythonでクラスを書くとき、一番多用するのは(恐らく)インスタンス変数ですが、クラス変数もたまに使います。

 しかし、pythonのクラス変数にはちょっと「クセ」があります。リスト内包表記と組み合わせて問題になることが多いようです。

問題の例

 リスト内包表記と組み合わせ、他のクラス変数を使いまわしてクラス変数を定義しようとした例です。

class Hoge:
    x = 5
    y = [x for i in range(10)]

 なんと以下のエラーを吐きます。

NameError: name 'x' is not defined

 こんな人畜無害そうなコードなのに、どうしてなのでしょう。

原因

 クラス変数のスコープの取り扱いが原因です。以下のstackoverflowのページによくまとまった説明があります。

python - Accessing class variables from a list comprehension in the class definition - Stack Overflow

Names in class scope are not accessible. Names are resolved in the innermost enclosing function scope. If a class definition occurs in a chain of nested scopes, the resolution process skips class definitions.

The class’s suite is then executed in a new execution frame (see section Naming and binding), using a newly created local namespace and the original global namespace. (Usually, the suite contains only function definitions.) When the class’s suite finishes execution, its execution frame is discarded but its local namespace is saved. [4] A class object is then created using the inheritance list for the base classes and the saved local namespace for the attribute dictionary.

 要約すると、クラス変数の中にスコープがネストされた場合、クラス変数の定義されたスコープは読まないよ、ということを言っています。リスト内包表記はスコープを作るので、この問題が起こります。

 どうしてこんな仕様になっているんでしょう? 上のリンク先でも貼られているPEP 227のページに解説があります。

PEP 227 -- Statically Nested Scopes | Python.org

Names in class scope are not accessible. Names are resolved in the innermost enclosing function scope. If a class definition occurs in a chain of nested scopes, the resolution process skips class definitions. This rule prevents odd interactions between class attributes and local variable access. If a name binding operation occurs in a class definition, it creates an attribute on the resulting class object. To access this variable in a method, or in a function nested within a method, an attribute reference must be used, either via self or via the class name.

 「それができるとメソッド定義で事故るだろーが、親切なpython様が対策してやってるんだよ」と書いてあります。

class Hoge:
    x = 5
    def fuga(self):
        x = 10
        print(x)

 色々な事故を招きそうなコードです。こういうことはできない方が妥当で、クラス変数にはHoge.xとしてアクセスするべきです。だけど、この措置がまったく無関係のリスト内包表記にまで影響してしまっています。

 じゃあ、こうすれば動くのか?

class Hoge:
    x = 5
    y = [Hoge.x for i in range(10)]

 動きません。

NameError: name 'Hoge' is not defined

 エラー内容が変わります。Hogeの定義が終わっていない以上、Hoge.xにも当然アクセスできない、ということです。困ったもんですね。

解決策

 こういう場合、lambda式による関数呼び出しなどを使って「ネストされたスコープ」にクラス変数を届けてやれ、とリンク先で解説されています。

class Hoge:
    x = 5
    y = (lambda x:[x for i in range(10)])(x)

 スコープのネストさえなければ他のクラス変数は使えるし、いったん関数スコープを作ってやれば、あとは普通の名前解決のルールで処理されるので大丈夫な訳です。

 これは面倒くさいし、慣れていない人にはラムダ式を直接呼び出しているこのコードは可読性が低いだろうし*1、コードレビューなどで「何やってんのこれ?」とか突っ込まれるくらいならまだ良い方で、これで書いたコードを勝手に他の誰かが直しちゃって大惨事、という事態も想定されます。なので、複数人で管理するコードでは、

  • コメントで何をやっているのかしっかり明記する。このページのURLを貼っておくのもおすすめです(爆)
  • そもそも使わないで回避策で書く

 このような対策が必要になるかと思います。

 回避策というのは、要するにリスト内包表記を使わなければ良いので、たとえば今回例として出したコードならこう書けます。

class Hoge:
    x = 5
    y = [x]*10

 記述もすっきりするし、どうしてもリスト内包表記を使わないと書けないような処理(そんな処理はほぼない。最悪普通のfor文で書ける)以外では、積極的にリスト内包表記を使う理由はありませんね・・・。

まとめ

  • クラス変数のスコープにはクセがあり、リスト内包表記と組み合わせて使うと事故る
  • 正面から対策するのは厄介
  • 逃げちゃえ

*1:私はpythonワンライナーで遊んできた経験があるので、この手のコードにも慣れてしまってなんとも思いませんが