[英]Nested list comprehension scope
解釋我的問題的最好方法是舉個例子:
例子.py:
class A(object):
integers = [1, 2, 3]
singles = [i for i in integers]
class B(object):
integers = [1, 2, 3]
pairs = [(i, j) for i in integers for j in integers]
當我在 python 2 下運行它時它工作正常,但在 python 3 下我得到一個NameError
類B
(但不是類A
):
$ python example.py
Traceback (most recent call last):
File "example.py", line 6, in <module>
class B(object):
File "example.py", line 8, in B
pairs = [(i, j) for i in integers for j in integers]
File "example.py", line 8, in <listcomp>
pairs = [(i, j) for i in integers for j in integers]
NameError: global name 'integers' is not defined
為什么只有B
類會引發NameError
,為什么只在 Python 3 下?
Python 3 中的類范圍有點奇怪,但這是有充分理由的。
在 Python 2 中,迭代變量(您的示例中的i
和j
)從列表推導式中泄露出來,並將包含在外部作用域中。 這是因為它們是在 Python 2 設計的早期開發的,並且基於顯式循環。 作為意外情況的示例,請檢查 Python 2 中Bi
和Bj
的值,您沒有收到錯誤消息!
在 Python 3 中,列表推導式已更改以防止這種泄漏。 它們現在使用一個函數(它有自己的作用域)來實現,該函數被調用來生成列表值。 這使得它們與生成器表達式的工作方式相同,生成器表達式一直是隱藏的函數。
這樣做的結果是,在類中,列表推導式通常看不到任何類變量。 這與無法直接查看類變量的方法(僅通過self
或顯式類名)並行。 例如,調用下面類中的方法將產生與您在列表理解中看到的相同的NameError
異常:
class Foo:
classvar = "bar"
def blah(self):
print(classvar) # raises "NameError: global name 'classvar' is not defined"
但是,有一個例外:列表推導式的第一個for
子句迭代的序列是在內部函數之外計算的。 這就是你的A
類在 Python 3 中工作的原因。它這樣做是為了讓生成器可以立即捕獲不可迭代的對象(而不是僅在對它們調用next
並且它們的代碼運行時)。
但它不適用於B
類中的兩級理解中的內部for
子句。
如果您使用dis
模塊反匯編一些創建列表dis
函數,您可以看到不同之處:
def f(lst):
return [i for i in lst]
def g(lst):
return [(i, j) for i in lst for j in lst]
這是f
的反匯編:
>>> dis.dis(f)
2 0 LOAD_CONST 1 (<code object <listcomp> at 0x0000000003CCA1E0, file "<pyshell#374>", line 2>)
3 LOAD_CONST 2 ('f.<locals>.<listcomp>')
6 MAKE_FUNCTION 0
9 LOAD_FAST 0 (lst)
12 GET_ITER
13 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
16 RETURN_VALUE
前三行顯示f
加載預編譯的代碼塊並從中創建一個函數(將其命名為f.<locals>.<listcomp>
)。 這是用於制作列表的功能。
接下來的兩行顯示正在加載的lst
變量和由它生成的迭代器。 這發生在f
的范圍內,而不是內部函數的范圍內。 然后使用該迭代器作為參數調用<listcomp>
函數。
這與A
類相當。 它從類變量integers
獲取迭代器,就像您可以在新成員的定義中使用其他類型的對先前類成員的引用一樣。
現在,比較g
的反匯編,它通過對同一個列表迭代兩次來生成對:
>>> dis.dis(g)
2 0 LOAD_CLOSURE 0 (lst)
3 BUILD_TUPLE 1
6 LOAD_CONST 1 (<code object <listcomp> at 0x0000000003CCA810, file "<pyshell#377>", line 2>)
9 LOAD_CONST 2 ('g.<locals>.<listcomp>')
12 MAKE_CLOSURE 0
15 LOAD_DEREF 0 (lst)
18 GET_ITER
19 CALL_FUNCTION 1 (1 positional, 0 keyword pair)
22 RETURN_VALUE
這一次,它使用代碼對象構建了一個閉包,而不是一個基本函數。 閉包是一個帶有一些“自由”變量的函數,這些變量引用封閉范圍內的事物。 對於g
的<listcomp>
函數,這工作得很好,因為它的作用域是正常的。 但是,當您嘗試在類 B 中使用相同類型的推導式時,閉包會失敗,因為類不會讓它們包含的函數以這種方式查看它們的作用域(如上面的Foo
類所示)。
值得注意的是,不僅內部序列值會導致此問題。 正如 BrenBarn 在評論中鏈接到的上一個問題一樣,如果在列表理解中的其他地方引用了類變量,您將遇到同樣的問題:
class C:
num = 5
products = [i * num for i in range(10)] # raises a NameError about num
但是,您不會從多級列表推導式中得到錯誤,其中內部for
(或if
)子句僅引用前面循環的結果。 這是因為這些值不是閉包的一部分,只是<listcomp>
函數范圍內的局部變量。
class D:
nested = [[1, 2, 3], [4, 5, 6]]
flattened = [item for inner in nested for item in inner] # works!
就像我說的,類作用域有點奇怪。
聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.