クラス定義のリスト内包からクラス変数にアクセスする


174

クラス定義内のリスト内包から他のクラス変数にどのようにアクセスしますか?以下はPython 2では機能しますが、Python 3では失敗します。

class Foo:
    x = 5
    y = [x for i in range(1)]

Python 3.2はエラーを出します:

NameError: global name 'x' is not defined

試しFoo.xてもうまくいきません。Python 3でこれを行う方法に関するアイデアはありますか?

やや複雑な動機付けの例:

from collections import namedtuple
class StateDatabase:
    State = namedtuple('State', ['name', 'capital'])
    db = [State(*args) for args in [
        ['Alabama', 'Montgomery'],
        ['Alaska', 'Juneau'],
        # ...
    ]]

この例でapply()は、適切な回避策でしたが、悲しいことにPython 3から削除されました。


エラーメッセージが正しくありません。私NameError: global name 'x' is not definedはPython 3.2と3.3を使用していますが、これは私が期待していることです。
Martijn Pieters

興味深い...明らかな回避策の1つは、クラス定義を終了した後にyを割り当てることです。Foo.y = [Foo.x for i in range(1)]
gps

3
+ martijn-pietersの複製へのリンクは正しいです。説明には+ matt-bからのコメントがあります。Python2.7リスト内包表記には独自の名前空間がありません(setやdict内包表記やジェネレータ式とは異なり... ] {}を使用して、実際の動作を確認します)。彼らはすべて 3で独自の名前空間を持っています
gps

@gps:または、クラス定義スイートに(一時的な)関数を挿入して、ネストされたスコープを使用します。
Martijn Pieters

2.7.11でテストしました。名前のエラーが発生しました
Junchao Gu

回答:


244

クラスのスコープとリスト、セットまたはディクショナリの理解、およびジェネレーターの式は混在しません。

その理由; または、これに関する公式の言葉

Python 3では、リスト内包表記に独自の適切なスコープ(ローカル名前空間)が与えられ、ローカル変数が周囲のスコープに流れ込むのを防ぎました(Pythonリスト内包表記は、内包範囲の後でさえ名前を再バインドします。これは正しいですか?)。モジュールや関数でこのようなリスト内包表記を使用する場合は素晴らしいことですが、クラスでは、スコープが少し変わっています。

これはpep 227に文書化されています:

クラススコープ内の名前にはアクセスできません。名前は、最も内側の関数スコープで解決されます。ネストされたスコープのチェーンでクラス定義が発生した場合、解決プロセスはクラス定義をスキップします。

class複合ステートメントのドキュメントで

次に、クラスのスイートは、新しく作成されたローカル名前空間と元のグローバル名前空間を使用して、新しい実行フレーム(セクションネーミングとバインディングを参照)で実行されます。(通常、スイートには関数定義のみが含まれます。)クラスのスイートの実行が完了すると、その実行フレームは破棄されますが、そのローカル名前空間は保存されます。[4]次に、基本クラスの継承リストと属性ディクショナリの保存されたローカル名前空間を使用して、クラスオブジェクトが作成されます。

強調鉱山; 実行フレームは一時的なスコープです。

スコープはクラスオブジェクトの属性として再利用されるため、非ローカルスコープとしても使用できるため、動作が未定義になります。たとえばx、ネストされたスコープ変数と呼ばれるクラスメソッドがFoo.x同様に操作されるとどうなりますか?さらに重要なことは、それはのサブクラスにとって何を意味するのFooでしょうか?Python 、クラスのスコープを関数のスコープとは大きく異なるため、別の方法で扱う必要があります。

最後に、間違いなく重要なことですが、実行モデルのドキュメントのリンクされた名前付けとバインディングのセクションでは、クラスのスコープについて明示的に言及しています。

クラスブロックで定義された名前のスコープは、クラスブロックに限定されます。メソッドのコードブロックには適用されません。これらは関数スコープを使用して実装されるため、内包表記とジェネレータ式を含みます。つまり、以下は失敗します。

class A:
     a = 42
     b = list(a + i for i in range(10))

つまり、要約すると、関数からクラススコープにアクセスできず、そのスコープで囲まれた内包表記またはジェネレータ式をリストできます。それらはそのスコープが存在しないかのように動作します。Python 2では、リスト内包表記はショートカットを使用して実装されていましたが、Python 3では、独自の関数スコープがあり(これまでずっとそうだったはずです)、したがって、例が壊れています。他の内包表記タイプは、Pythonのバージョンに関係なく独自のスコープを持っているため、セットまたはディクトリ内包表記を使用した同様の例は、Python 2では機能しません。

# Same error, in Python 2 or 3
y = {x: x for i in range(1)}

(小さな)例外。または、なぜ一部まだ機能するのか

Pythonのバージョンに関係なく、周囲のスコープで実行される内包表現またはジェネレータ式の一部があります。それが最も外側のイテラブルの式になります。あなたの例では、それはrange(1)

y = [x for i in range(1)]
#               ^^^^^^^^

したがって、xその式で使用してもエラーはスローされません。

# Runs fine
y = [i for i in range(x)]

これは最も外側のイテラブルにのみ適用されます。内包に複数のfor節がある場合、内for節の反復可能オブジェクトは内包のスコープで評価されます。

# NameError
y = [i for i in range(1) for j in range(x)]

この設計上の決定は、ジェネレーター式の最も外側の反復可能オブジェクトを作成するとき、または最も外側の反復可能オブジェクトが反復可能でないことが判明したときに、反復時間ではなくgenexp作成時にエラーをスローするために行われました。理解度は、一貫性のためにこの動作を共有します。

ボンネットの下を見る; または、あなたが望んでいたよりもずっと詳細

disモジュールを使用すると、このすべてを実際に見ることができます。次の例ではPython 3.3を使用しています。これは、検査するコードオブジェクトを適切に識別する修飾名を追加するためです。生成されたバイトコードは、それ以外は機能的にPython 3.2と同じです。

クラスを作成するために、Pythonは基本的にクラス本体を構成するスイート全体(つまり、すべてがclass <name>:行よりも1レベル深くインデントされています)を取得し、それを関数のように実行します。

>>> import dis
>>> def foo():
...     class Foo:
...         x = 5
...         y = [x for i in range(1)]
...     return Foo
... 
>>> dis.dis(foo)
  2           0 LOAD_BUILD_CLASS     
              1 LOAD_CONST               1 (<code object Foo at 0x10a436030, file "<stdin>", line 2>) 
              4 LOAD_CONST               2 ('Foo') 
              7 MAKE_FUNCTION            0 
             10 LOAD_CONST               2 ('Foo') 
             13 CALL_FUNCTION            2 (2 positional, 0 keyword pair) 
             16 STORE_FAST               0 (Foo) 

  5          19 LOAD_FAST                0 (Foo) 
             22 RETURN_VALUE         

最初LOAD_CONSTに、Fooクラス本体のコードオブジェクトを読み込み、それを関数にして呼び出します。次に、その呼び出しの結果を使用して、クラスの名前空間itsが作成されます__dict__。ここまでは順調ですね。

ここで注意すべきことは、バイトコードにはネストされたコードオブジェクトが含まれていることです。Pythonでは、クラス定義、関数、内包表記、ジェネレーターはすべて、バイトコードだけでなく、ローカル変数、定数、グローバルから取得した変数、ネストされたスコープから取得した変数を表す構造も含むコードオブジェクトとして表されます。コンパイルされたバイトコードはそれらの構造を参照し、Pythonインタープリターは、提示されたバイトコードを与えられたものにアクセスする方法を知っています。

ここで覚えておくべき重要なことは、Pythonはコンパイル時にこれらの構造を作成するということです。classスイートは、コードオブジェクトである(<code object Foo at 0x10a436030, file "<stdin>", line 2>既にコンパイルされています)。

クラス本体自体を作成するコードオブジェクトを調べてみましょう。コードオブジェクトのco_consts構造は次のとおりです。

>>> foo.__code__.co_consts
(None, <code object Foo at 0x10a436030, file "<stdin>", line 2>, 'Foo')
>>> dis.dis(foo.__code__.co_consts[1])
  2           0 LOAD_FAST                0 (__locals__) 
              3 STORE_LOCALS         
              4 LOAD_NAME                0 (__name__) 
              7 STORE_NAME               1 (__module__) 
             10 LOAD_CONST               0 ('foo.<locals>.Foo') 
             13 STORE_NAME               2 (__qualname__) 

  3          16 LOAD_CONST               1 (5) 
             19 STORE_NAME               3 (x) 

  4          22 LOAD_CONST               2 (<code object <listcomp> at 0x10a385420, file "<stdin>", line 4>) 
             25 LOAD_CONST               3 ('foo.<locals>.Foo.<listcomp>') 
             28 MAKE_FUNCTION            0 
             31 LOAD_NAME                4 (range) 
             34 LOAD_CONST               4 (1) 
             37 CALL_FUNCTION            1 (1 positional, 0 keyword pair) 
             40 GET_ITER             
             41 CALL_FUNCTION            1 (1 positional, 0 keyword pair) 
             44 STORE_NAME               5 (y) 
             47 LOAD_CONST               5 (None) 
             50 RETURN_VALUE         

上記のバイトコードはクラス本体を作成します。関数が実行され、得られているlocals()名前空間、含有x及びy(ので、それが動作しないことを除いてクラスを作成するために使用されるxグローバルとして定義されていません)。に格納5した後x、別のコードオブジェクトをロードすることに注意してください。それがリストの内包です。クラス本体と同じように、関数オブジェクトでラップされます。作成された関数は、位置指定引数(range(1)ループコードに使用する反復可能)を取り、反復子にキャストします。バイトコードに示されているように、range(1)クラススコープで評価されます。

これから、関数またはジェネレータのコードオブジェクトと内包のコードオブジェクトの唯一の違いは、後者が親コードオブジェクトが実行されるとすぐに実行されることです。バイトコードは、その場で関数を作成し、いくつかの小さなステップで実行します。

Python 2.xは代わりにインラインバイトコードを使用します。Python2.7からの出力は次のとおりです。

  2           0 LOAD_NAME                0 (__name__)
              3 STORE_NAME               1 (__module__)

  3           6 LOAD_CONST               0 (5)
              9 STORE_NAME               2 (x)

  4          12 BUILD_LIST               0
             15 LOAD_NAME                3 (range)
             18 LOAD_CONST               1 (1)
             21 CALL_FUNCTION            1
             24 GET_ITER            
        >>   25 FOR_ITER                12 (to 40)
             28 STORE_NAME               4 (i)
             31 LOAD_NAME                2 (x)
             34 LIST_APPEND              2
             37 JUMP_ABSOLUTE           25
        >>   40 STORE_NAME               5 (y)
             43 LOAD_LOCALS         
             44 RETURN_VALUE        

コードオブジェクトは読み込まれず、代わりにFOR_ITERループがインラインで実行されます。そのため、Python 3.xでは、リストジェネレーターに独自の適切なコードオブジェクトが与えられました。つまり、独自のスコープを持っています。

ただし、モジュールまたはスクリプトがインタープリターによって最初に読み込まれたときに、内包表記が残りのpythonソースコードと共にコンパイルされ、コンパイラーはクラススイートを有効なスコープと見なしませ。リスト内包で参照される変数は、クラス定義を囲むスコープ再帰的に調べる必要があります。コンパイラーによって変数が見つからなかった場合、その変数はグローバルとしてマークされます。リスト内包コードオブジェクトの逆アセンブリは、x実際にグローバルとしてロードされていることを示しています。

>>> foo.__code__.co_consts[1].co_consts
('foo.<locals>.Foo', 5, <code object <listcomp> at 0x10a385420, file "<stdin>", line 4>, 'foo.<locals>.Foo.<listcomp>', 1, None)
>>> dis.dis(foo.__code__.co_consts[1].co_consts[2])
  4           0 BUILD_LIST               0 
              3 LOAD_FAST                0 (.0) 
        >>    6 FOR_ITER                12 (to 21) 
              9 STORE_FAST               1 (i) 
             12 LOAD_GLOBAL              0 (x) 
             15 LIST_APPEND              2 
             18 JUMP_ABSOLUTE            6 
        >>   21 RETURN_VALUE         

このバイトコードのチャンクは、渡された最初の引数(range(1)イテレータ)をロードします。これは、Python 2.xバージョンFOR_ITERがループして出力を作成するために使用するのと同じです。

代わりに関数で定義xした場合、セル変数になります(セルはネストされたスコープを参照します)。foox

>>> def foo():
...     x = 2
...     class Foo:
...         x = 5
...         y = [x for i in range(1)]
...     return Foo
... 
>>> dis.dis(foo.__code__.co_consts[2].co_consts[2])
  5           0 BUILD_LIST               0 
              3 LOAD_FAST                0 (.0) 
        >>    6 FOR_ITER                12 (to 21) 
              9 STORE_FAST               1 (i) 
             12 LOAD_DEREF               0 (x) 
             15 LIST_APPEND              2 
             18 JUMP_ABSOLUTE            6 
        >>   21 RETURN_VALUE         

LOAD_DEREF間接的にロードするxコードオブジェクトセルオブジェクトから:

>>> foo.__code__.co_cellvars               # foo function `x`
('x',)
>>> foo.__code__.co_consts[2].co_cellvars  # Foo class, no cell variables
()
>>> foo.__code__.co_consts[2].co_consts[2].co_freevars  # Refers to `x` in foo
('x',)
>>> foo().y
[2]

実際の参照では、関数オブジェクトの.__closure__属性から初期化された現在のフレームデータ構造から値が検索されます。内包コードオブジェクト用に作成された関数は再び破棄されるため、その関数のクロージャを検査することはできません。実際のクロージャーを確認するには、代わりにネストされた関数を検査する必要があります。

>>> def spam(x):
...     def eggs():
...         return x
...     return eggs
... 
>>> spam(1).__code__.co_freevars
('x',)
>>> spam(1)()
1
>>> spam(1).__closure__
>>> spam(1).__closure__[0].cell_contents
1
>>> spam(5).__closure__[0].cell_contents
5

要約すると、

  • リスト内包表記はPython 3で独自のコードオブジェクトを取得します。関数、ジェネレーター、内包表記のコードオブジェクトに違いはありません。内包コードオブジェクトは一時的な関数オブジェクトにラップされ、すぐに呼び出されます。
  • コードオブジェクトはコンパイル時に作成され、非ローカル変数は、コードのネストされたスコープに基づいて、グローバル変数またはフリー変数としてマークされます。クラス本体は、これらの変数を検索するためのスコープと見なされません
  • コードを実行するとき、Pythonはグローバルまたは現在実行中のオブジェクトのクロージャを調べればよいだけです。コンパイラはスコープとしてクラス本体を含めなかったため、一時的な関数の名前空間は考慮されません。

回避策。または、それについて何をすべきか

x関数のように、変数の明示的なスコープを作成する場合は、リスト内包表記にクラススコープ変数を使用できます。

>>> class Foo:
...     x = 5
...     def y(x):
...         return [x for i in range(1)]
...     y = y(x)
... 
>>> Foo.y
[5]

「一時」y関数は直接呼び出すことができます。戻り値で置き換えるときにそれを置き換えます。その範囲解決時に考慮されxます:

>>> foo.__code__.co_consts[1].co_consts[2]
<code object y at 0x10a5df5d0, file "<stdin>", line 4>
>>> foo.__code__.co_consts[1].co_consts[2].co_cellvars
('x',)

もちろん、あなたのコードを読んでいる人はこれについて少し頭を悩ますでしょう。なぜあなたはこれをしているのかを説明する大きな脂肪のコメントをそこに入れたいかもしれません。

最善の回避策は、__init__代わりにを使用してインスタンス変数を作成することです。

def __init__(self):
    self.y = [self.x for i in range(1)]

そして、すべての頭を引っかくこと、そしてあなた自身を説明する質問を避けてください。あなた自身の具体的な例として、私はnamedtupleクラスにさえ保存しません。出力を直接使用する(生成されたクラスをまったく保存しない)か、グローバルを使用します。

from collections import namedtuple
State = namedtuple('State', ['name', 'capital'])

class StateDatabase:
    db = [State(*args) for args in [
       ('Alabama', 'Montgomery'),
       ('Alaska', 'Juneau'),
       # ...
    ]]

21
:あなたはまた、結合修正するためにラムダを使用することができますy = (lambda x=x: [x for i in range(1)])()
ecatmur

3
@ecatmur:結局のところ、lambdaまさに匿名関数です。
Martijn Pieters

2
記録として、デフォルトの引数(ラムダまたは関数への)を使用してクラス変数を渡す回避策には問題があります。つまり、変数の現在の値を渡します。したがって、変数が後で変更され、ラムダまたは関数が呼び出された場合、ラムダまたは関数は古い値を使用します。この動作はクロージャーの動作(値ではなく変数への参照をキャプチャする)とは異なるため、予期しない動作になる可能性があります。
Neal Young

9
何かが直感的に機能しない理由を説明する技術情報のページが必要な場合、それをバグと呼びます。
ジョナサン

5
@JonathanLeaders:バグとは言わないで、トレードオフと呼んでください。AとBが必要だが、そのうちの1つしか取得できない場合は、どのように決定しても、結果が気に入らない場合があります。それが人生だ。
Lutz Prechelt 2016年

15

私の意見では、これはPython 3の欠陥です。

古い方法(2.7で動作しNameError: name 'x' is not defined、3以上でスロー):

class A:
    x = 4
    y = [x+i for i in range(1)]

注:単にスコープを設定してA.xも解決しません

新しい方法(3以上で機能):

class A:
    x = 4
    y = (lambda x=x: [x+i for i in range(1)])()

構文が醜いので、通常はコンストラクターですべてのクラス変数を初期化するだけです。


6
この問題はPython 2にも存在し、ジェネレーター式を使用する場合や、セットおよびディクショナリ内包を使用する場合に発生します。これはバグではなく、クラスの名前空間がどのように機能するかの結果です。変更されません。
Martijn Pieters

4
そして、私はあなたの回避策が私の答えがすでに述べているとおり正確に実行することを指摘します:新しいスコープを作成します(ラムダはここでdef関数の作成に使用することと違いはありません)。
Martijn Pieters

1
うん。回避策を一目で確認できるのは良いことですが、これは、言語の動作の副作用である場合(したがって変更されない場合)に、動作をバグとして誤って示しています。
jsbueno 2017年

これは別の問題であり、実際にはPython 3の問題ではありません。sayを使用して埋め込みモードで呼び出した場合にのみ、IPythonで発生しますpython -c "import IPython;IPython.embed()"。say ipythonを使用してIPythonを直接実行すると、問題は解消されます。
Riaz Rizvi 2018

6

受け入れられた回答は優れた情報を提供しますが、ここには他のいくつかのしわがあるようです-リスト内包表記とジェネレータ式の違いです。私が遊んだデモ:

class Foo:

    # A class-level variable.
    X = 10

    # I can use that variable to define another class-level variable.
    Y = sum((X, X))

    # Works in Python 2, but not 3.
    # In Python 3, list comprehensions were given their own scope.
    try:
        Z1 = sum([X for _ in range(3)])
    except NameError:
        Z1 = None

    # Fails in both.
    # Apparently, generator expressions (that's what the entire argument
    # to sum() is) did have their own scope even in Python 2.
    try:
        Z2 = sum(X for _ in range(3))
    except NameError:
        Z2 = None

    # Workaround: put the computation in lambda or def.
    compute_z3 = lambda val: sum(val for _ in range(3))

    # Then use that function.
    Z3 = compute_z3(X)

    # Also worth noting: here I can refer to XS in the for-part of the
    # generator expression (Z4 works), but I cannot refer to XS in the
    # inner-part of the generator expression (Z5 fails).
    XS = [15, 15, 15, 15]
    Z4 = sum(val for val in XS)
    try:
        Z5 = sum(XS[i] for i in range(len(XS)))
    except NameError:
        Z5 = None

print(Foo.Z1, Foo.Z2, Foo.Z3, Foo.Z4, Foo.Z5)

2

これはPythonのバグです。内包表記はforループと同等であると宣伝されていますが、これはクラスでは当てはまりません。少なくともPython 3.6.6までは、クラスで使用される内包表記では、内包表記の内部からアクセスできるのは、内包表記の外側から1つの変数のみであり、最も外側の反復子として使用する必要があります。関数では、このスコープ制限は適用されません。

これがバグである理由を説明するために、元の例に戻りましょう。これは失敗します:

class Foo:
    x = 5
    y = [x for i in range(1)]

しかし、これはうまくいきます:

def Foo():
    x = 5
    y = [x for i in range(1)]

この制限は、リファレンスガイドのこのセクションの最後に記載されています。


1

最も外側のイテレータは周囲のスコープで評価されるため、zip一緒に使用してitertools.repeat依存関係を内包のスコープに引き継ぐことができます。

import itertools as it

class Foo:
    x = 5
    y = [j for i, j in zip(range(3), it.repeat(x))]

for内包されたループを内包で使用して、最も外側の反復可能オブジェクトに依存関係を含めることもできます。

class Foo:
    x = 5
    y = [j for j in (x,) for i in range(3)]

OPの具体例:

from collections import namedtuple
import itertools as it

class StateDatabase:
    State = namedtuple('State', ['name', 'capital'])
    db = [State(*args) for State, args in zip(it.repeat(State), [
        ['Alabama', 'Montgomery'],
        ['Alaska', 'Juneau'],
        # ...
    ])]
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.