Python尋找變數的方法是透過LEGB,即Local、Enclosing、Global及Built-in scope,來層層尋找。有興趣深究的朋友,可以參考這篇Real Python的講解。
雖然概念是清楚的,但仍然有一些需要注意的地方。本翼中,我們將分別以L、E、G及B來代稱Local、Enclosing、Global及Built-in scope。
UnBoundLocalError是一個常見的錯誤。# 01中,當unboundlocalerror_func被呼叫時,會raise UnboundLocalError。您可能會覺得疑惑,覺得print(x)會因為找不到L的x,而unboundlocalerror_func又沒有E,所以會在G中來尋找x,為什麼會報錯呢?
這是因為在unboundlocalerror_func有x = 2這個assignment。Python是在execute(或是想成compile)時就決定一個變數是不是local variable。由於unboundlocalerror_func中我們有指定x為2,Python於execute(或compile)階段就會先認定x是一個local variable。接著當我們真正呼叫unboundlocalerror_func時,Python會開始依照LEGB的原則尋找變數。由於x已被認定是一個local variable,所以Python只會在unboundlocalerror_func這個L中尋找x,而我們的確於定義x前,就使用了print(x),所以會報錯。
# 01
x = 1
def unboundlocalerror_func():
print(x)
x = 2
unboundlocalerror_func() # UnboundLocalError
在使用各種Comprehension時,也是一個容易出錯的地方。我們透過一連串小例子,慢慢說明。
# 02a中,當我們真正呼叫adders中每一個adder時,Python會依照LEGB原則尋找n。由於在L找不到n,又沒有E,最後在G中找到n,其值為3(不是10),因為在for n in range(1, 4)中,n最後於G中被指為3。
# 02a
n = 10
adders = []
for n in range(1, 4):
adders.append(lambda x: x+n)
for adder in adders:
print(adder(1)) # 4 4 4
如果能理解# 02a的話,# 02b也是相同概念,最後於G中的n值為10。
# 02b
adders = []
for n in range(1, 4):
adders.append(lambda x: x+n)
n = 10
for adder in adders:
print(adder(1)) # 11 11 11
我們將# 02a以list comprehension的方式改寫為# 02c,答案不變。
# 02c
n = 10
adders = [lambda x:x+n for n in range(1, 4)]
for adder in adders:
print(adder(1)) # 4 4 4
但當我們將# 02b改寫為# 02d時,答案卻變了,這可能會讓你有點驚訝。
# 02d
adders = [lambda x:x+n for n in range(1, 4)]
n = 10
for adder in adders:
print(adder(1)) # 4 4 4
事實上,comprehension內就像一個小的local scope(或可以想成一個namespace),# 02c與# 02d可以改寫為# 02e,至於n = 10放前放後,並不影響答案。
# 02e
n = 10
def get_adders():
adders = []
for n in range(1, 4):
def my_func(x):
return x+n
adders.append(my_func)
return adders
adders2 = get_adders()
for adder in adders2:
print(adder(1)) # 4 4 4
當我們呼叫每個adder時,由於L中找不到n,所以我們是在E這層找到n,其值為3,因為for n in range(1, 4)在最後將n指為3。
如果我們想要每個adder都能獲得不同的n值,# 02f是一個可以參考的寫法。我們將n定義為lambda的keyword argument,並預設其值為n。這麼一來,於迴圈中我們就可以接受不同的n值了。
# 02f
n = 10
adders = [lambda x, n=n:x+n for n in range(1, 4)]
for adder in adders:
print(adder(1)) # 2 3 4
由 class body內取得變數時,也是一個容易讓人搞糊塗的地方,我們透過# 03來了解。
# 03
fruit = 'Apple'
class Basket:
fruit = 'Orange'
partition1 = [fruit] * 3
partition2 = [fruit for _ in range(3)]
def get_fruit(self):
return f'{fruit}'
basket = Basket()
print(basket.get_fruit()) # Apple
print(basket.fruit) # Orange
print(Basket.partition1) # ['Orange', 'Orange', 'Orange']
print(basket.partition2) # ['Apple', 'Apple', 'Apple']
basket.get_fruit()會返回Apple不是Orange,因為在function內,如果要取得class或instance的variable時,要使用類似self或cls等語法。所以當呼叫basket.get_fruit()時,L找不到fruit,又沒有E,所以找到的是G的Apple。basket.fruit會返回Orange,因為basket.__dict__中並沒有fruit,所以basket.fruit會往上到Basket中尋找。此時於Basket有找到fruit,所以返回其值。Basket.partition1會返回['Orange', 'Orange', 'Orange']。Basket.__dict__中有找到partition1,所以返回其值。由於partition1與fruit同是class variable,所以[fruit] * 3實際上是將fruit其連續三次置入list中。basket.partition2會返回['Apple', 'Apple', 'Apple']。因為basket.__dict__中並沒有partition2,所以basket.partition2會往上到Basket中尋找。此時於Basket找到partition2,所以返回其值。由於partition2使用list comprehension,所以相當於其在一個function底下,情況跟get_fruit是非常類似的,所以其最後找到的是,也會是G的Apple。今天最後,我們來一個metaclasses與scope的綜合練習題。
寫一個metaclasses來生成如TargetClass的class。
__init__接受任意數目的**kwargs。kwargs中的key加上_(underscore)後,設為instance variable,其值為原key所相對應的value。kwargs中的key,建立property,並返回相對應加上_(underscore)的instance variable。# 04
class TargetClass:
def __init__(self, **kwargs):
"""
kwargs: {'x': 1, 'y':2, ...}
"""
self.__dict__.update({f'_{k}': v for k, v in kwargs.items()})
@property
def x(self):
return self._x
@property
def y(self):
return self._y
...
MyType1繼承type。MyType1.__new__中使用super().__new__生成cls。init function,其內部邏輯與上述TargetClass.__init__相同,並指定給cls.__init__。kwargs打個迴圈,將其key依序設為property,並返回相對應的底層加上_(underscore)的instance variable。cls。這麼一來我們就可以使用MyType1作為MyClass1的metaclass,並搭配kwds作為MyType1.__new__的最後一個參數**kwargs,來生成MyClass1,及使用my_inst1 = MyClass1(**kwds)生成my_inst1。
# 04
class MyType1(type):
def __new__(mcls, cls_name, cls_bases, cls_dict, **kwargs):
cls = super().__new__(mcls, cls_name, cls_bases, cls_dict)
def init(self, **kwargs):
self.__dict__.update({f'_{k}': v for k, v in kwargs.items()})
cls.__init__ = init
for prop in kwargs:
setattr(cls,
prop,
property(lambda self: getattr(self, f'_{prop}')))
return cls
kwds = {'x': 1, 'y': 2}
class MyClass1(metaclass=MyType1, **kwds):
pass
my_inst1 = MyClass1(**kwds)
...
但是如果觀察my_inst1.x與my_inst1.y,卻發現其值都為2。
>>> vars(my_inst1) # {'_x': 1, '_y': 2}
>>> my_inst1.x, my_inst1.y # 2 2
問題出在property(lambda self: getattr(self, f'_{prop}'))。lambda self: getattr(self, f'_{prop}'是一個getter function,其接收的prop參數,是由for prop in kwargs而來。當我們真正使用my_inst1.x及my_inst1.y時,每個getter都會先在L中找prop,因為找不到,所以往外找。最後在E中找到prop,並認為prop是kwargs的最後一個key。
修正解法是使用keyword arguments,將property(lambda self: getattr(self, f'_{prop}'))改為property(lambda self, attr=prop: getattr(self, f'_{attr}'))。整體思路是與# 02f類似的,只是因為在metaclass內,語法比較複雜而已。
# 04
...
class MyType2(type):
def __new__(mcls, cls_name, cls_bases, cls_dict, **kwargs):
cls = super().__new__(mcls, cls_name, cls_bases, cls_dict)
def init(self, **kwargs):
self.__dict__.update({f'_{k}': v for k, v in kwargs.items()})
cls.__init__ = init
for prop in kwargs:
setattr(cls,
prop,
property(lambda self, attr=prop: getattr(self, f'_{attr}')))
return cls
kwds = {'x': 1, 'y': 2}
class MyClass2(metaclass=MyType2, **kwds):
pass
my_inst2 = MyClass2(**kwds)
觀察my_inst2.x為1,而my_inst2.y為2,皆正確無誤。
>>> vars(my_inst2) # {'_x': 1, '_y': 2}
>>> my_inst2.x, my_inst2.y # 1 2
mutate,而發生預期外的行為。function(或lambda)的參數時,keyword arguments可能是您的好幫手。Dr. Fred Baptiste於多個單元中,都曾反覆強調。
牛刀小試的例題改寫自參考Part 4-Section 14 -Metaprogramming-11 Metaprogramming Application 1。