web-dev-qa-db-ja.com

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

クラス定義内のリスト内包から他のクラス変数にどのようにアクセスしますか?以下は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。

147
Mark Lodato

クラススコープとリスト、セットまたは辞書の内包表記、およびジェネレーター式は混在しません。

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

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

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

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

および class複合ステートメントのドキュメント

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

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

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

最後になりましたが、間違いなく重要なことは、実行モデルのドキュメントのリンクされた Naming and binding セクションで、クラススコープについて明示的に言及されています

クラスブロックで定義される名前の範囲は、クラスブロックに制限されます。メソッドのコードブロックには拡張されません。これには、関数スコープを使用して実装されるため、内包表記と生成式が含まれます。これは、以下が失敗することを意味します。

_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)}
_

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

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 module を使用して、このすべての動作を確認できます。次の例では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クラス本体のコードオブジェクトをロードし、それを関数にして、それを呼び出します。次に、その呼び出しのresultを使用して、クラスの名前空間、その___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         
_

上記のバイトコードはクラス本体を作成します。関数が実行され、xyを含む結果のlocals()名前空間を使用してクラスが作成されます(ただし、xはグローバルとして定義されていません)。 _5_をxに格納した後、別のコードオブジェクトをロードすることに注意してください。それがリストの理解です。クラス本体と同じように、関数オブジェクトにラップされます。作成された関数は位置引数を取ります。range(1)反復コードに使用するイテラブルは、反復子にキャストされます。バイトコードに示すように、range(1)はクラススコープで評価されます。

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

Python 2.xは代わりにインラインバイトコードを使用します。Python 2.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ソースコードとともにコンパイルされ、コンパイラーはnotを考慮しますクラススイートの有効なスコープリスト内包の参照変数は、スコープを再帰的に検索する必要がありますsurroundingクラス定義は、コンパイラによって検出されなかった場合、グローバルとしてマークされます。リスト内包コードオブジェクトの逆アセンブリは、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関数でfooを定義した場合、xはセル変数になります(セルはネストされたスコープを参照します)。

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

回避策。または、それについてどうするか

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

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

'temporary' y関数は直接呼び出すことができます。戻り値で行う場合は置き換えます。そのスコープisxを解決するときに考慮されます:

_>>> 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'),
       # ...
    ]]
_
198
Martijn Pieters

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

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

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)])()

構文が非常にいため、通常、コンストラクター内のすべてのクラス変数を初期化します。

13
Jonathan

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

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)
5
FMc

これは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
bzip2

最も外側のイテレータは周囲のスコープで評価されるため、 Zipitertools.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'],
        # ...
    ])]
0
a_guest