軟件開(kāi)發(fā)領(lǐng)域中最經(jīng)典的口頭禪就是“don’t repeat yourself”。 也就是說(shuō),任何時(shí)候當(dāng)你的程序中存在高度重復(fù)(或者是通過(guò)剪切復(fù)制)的代碼時(shí),都應(yīng)該想想是否有更好的解決方案。 在 Python 當(dāng)中,通常都可以通過(guò)元編程來(lái)解決這類問(wèn)題。 簡(jiǎn)而言之,元編程就是關(guān)于創(chuàng)建操作源代碼(比如修改、生成或包裝原來(lái)的代碼)的函數(shù)和類。 主要技術(shù)是使用裝飾器、類裝飾器和元類。不過(guò)還有一些其他技術(shù), 包括簽名對(duì)象、使用 exec()
執(zhí)行代碼以及對(duì)內(nèi)部函數(shù)和類的反射技術(shù)等。 本章的主要目的是向大家介紹這些元編程技術(shù),并且給出實(shí)例來(lái)演示它們是怎樣定制化你的源代碼行為的。
你想在函數(shù)上添加一個(gè)包裝器,增加額外的操作處理(比如日志、計(jì)時(shí)等)。
如果你想使用額外的代碼包裝一個(gè)函數(shù),可以定義一個(gè)裝飾器函數(shù),例如:
import time
from functools import wraps
def timethis(func):
'''
Decorator that reports the execution time.
'''
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(func.__name__, end-start)
return result
return wrapper
下面是使用裝飾器的例子:
>>> @timethis
... def countdown(n):
... '''
... Counts down
... '''
... while n > 0:
... n -= 1
...
>>> countdown(100000)
countdown 0.008917808532714844
>>> countdown(10000000)
countdown 0.87188299392912
>>>
一個(gè)裝飾器就是一個(gè)函數(shù),它接受一個(gè)函數(shù)作為參數(shù)并返回一個(gè)新的函數(shù)。 當(dāng)你像下面這樣寫(xiě):
@timethis
def countdown(n):
pass
跟像下面這樣寫(xiě)其實(shí)效果是一樣的:
def countdown(n):
pass
countdown = timethis(countdown)
順便說(shuō)一下,內(nèi)置的裝飾器比如 @staticmethod, @classmethod,@property
原理也是一樣的。 例如,下面這兩個(gè)代碼片段是等價(jià)的:
class A:
@classmethod
def method(cls):
pass
class B:
# Equivalent definition of a class method
def method(cls):
pass
method = classmethod(method)
在上面的 wrapper()
函數(shù)中, 裝飾器內(nèi)部定義了一個(gè)使用 *args
和 **kwargs
來(lái)接受任意參數(shù)的函數(shù)。 在這個(gè)函數(shù)里面調(diào)用了原始函數(shù)并將其結(jié)果返回,不過(guò)你還可以添加其他額外的代碼(比如計(jì)時(shí))。 然后這個(gè)新的函數(shù)包裝器被作為結(jié)果返回來(lái)代替原始函數(shù)。
需要強(qiáng)調(diào)的是裝飾器并不會(huì)修改原始函數(shù)的參數(shù)簽名以及返回值。 使用*args
和 **kwargs
目的就是確保任何參數(shù)都能適用。 而返回結(jié)果值基本都是調(diào)用原始函數(shù) func(*args, **kwargs)
的返回結(jié)果,其中 func 就是原始函數(shù)。
剛開(kāi)始學(xué)習(xí)裝飾器的時(shí)候,會(huì)使用一些簡(jiǎn)單的例子來(lái)說(shuō)明,比如上面演示的這個(gè)。 不過(guò)實(shí)際場(chǎng)景使用時(shí),還是有一些細(xì)節(jié)問(wèn)題要注意的。 比如上面使用 @wraps(func)
注解是很重要的, 它能保留原始函數(shù)的元數(shù)據(jù)(下一小節(jié)會(huì)講到),新手經(jīng)常會(huì)忽略這個(gè)細(xì)節(jié)。 接下來(lái)的幾個(gè)小節(jié)我們會(huì)更加深入的講解裝飾器函數(shù)的細(xì)節(jié)問(wèn)題,如果你想構(gòu)造你自己的裝飾器函數(shù),需要認(rèn)真看一下。
你寫(xiě)了一個(gè)裝飾器作用在某個(gè)函數(shù)上,但是這個(gè)函數(shù)的重要的元信息比如名字、文檔字符串、注解和參數(shù)簽名都丟失了。
任何時(shí)候你定義裝飾器的時(shí)候,都應(yīng)該使用 functools
庫(kù)中的 @wraps
裝飾器來(lái)注解底層包裝函數(shù)。例如:
import time
from functools import wraps
def timethis(func):
'''
Decorator that reports the execution time.
'''
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
result = func(*args, **kwargs)
end = time.time()
print(func.__name__, end-start)
return result
return wrapper
下面我們使用這個(gè)被包裝后的函數(shù)并檢查它的元信息:
>>> @timethis
... def countdown(n:int):
... '''
... Counts down
... '''
... while n > 0:
... n -= 1
...
>>> countdown(100000)
countdown 0.008917808532714844
>>> countdown.__name__
'countdown'
>>> countdown.__doc__
'\n\tCounts down\n\t'
>>> countdown.__annotations__
{'n': <class 'int'>}
>>>
在編寫(xiě)裝飾器的時(shí)候復(fù)制元信息是一個(gè)非常重要的部分。如果你忘記了使用 @wrap
, 那么你會(huì)發(fā)現(xiàn)被裝飾函數(shù)丟失了所有有用的信息。比如如果忽略 @wrap
后的效果是下面這樣的:
>>> countdown.__name__
'wrapper'
>>> countdown.__doc__
>>> countdown.__annotations__
{}
>>>
@wraps
有一個(gè)重要特征是它能讓你通過(guò)屬性__wrapped__
直接訪問(wèn)被包裝函數(shù)。例如:
>>> countdown.__wrapped__(100000)
>>>
__wrapped__
屬性還能讓被裝飾函數(shù)正確暴露底層的參數(shù)簽名信息。例如:
>>> from inspect import signature
>>> print(signature(countdown))
(n:int)
>>>
一個(gè)很普遍的問(wèn)題是怎樣讓裝飾器去直接復(fù)制原始函數(shù)的參數(shù)簽名信息, 如果想自己手動(dòng)實(shí)現(xiàn)的話需要做大量的工作,最好就簡(jiǎn)單的使用 __wrapped__
裝飾器。 通過(guò)底層的 __wrapped__
屬性訪問(wèn)到函數(shù)簽名信息。更多關(guān)于簽名的內(nèi)容可以參考9.16小節(jié)。
一個(gè)裝飾器已經(jīng)作用在一個(gè)函數(shù)上,你想撤銷它,直接訪問(wèn)原始的未包裝的那個(gè)函數(shù)。
假設(shè)裝飾器是通過(guò) @wraps
(參考9.2小節(jié))來(lái)實(shí)現(xiàn)的,那么你可以通過(guò)訪問(wèn) __wrapped__
屬性來(lái)訪問(wèn)原始函數(shù):
>>> @somedecorator
>>> def add(x, y):
... return x + y
...
>>> orig_add = add.__wrapped__
>>> orig_add(3, 4)
7
>>>
直接訪問(wèn)未包裝的原始函數(shù)在調(diào)試、內(nèi)省和其他函數(shù)操作時(shí)是很有用的。 但是我們這里的方案僅僅適用于在包裝器中正確使用了@wraps
或者直接設(shè)置了 __wrapped__
屬性的情況。
如果有多個(gè)包裝器,那么訪問(wèn) __wrapped__
屬性的行為是不可預(yù)知的,應(yīng)該避免這樣做。 在 Python3.3 中,它會(huì)略過(guò)所有的包裝層,比如,假如你有如下的代碼:
from functools import wraps
def decorator1(func):
@wraps(func)
def wrapper(*args, **kwargs):
print('Decorator 1')
return func(*args, **kwargs)
return wrapper
def decorator2(func):
@wraps(func)
def wrapper(*args, **kwargs):
print('Decorator 2')
return func(*args, **kwargs)
return wrapper
@decorator1
@decorator2
def add(x, y):
return x + y
下面我們?cè)?Python3.3 下測(cè)試:
>>> add(2, 3)
Decorator 1
Decorator 2
5
>>> add.__wrapped__(2, 3)
5
>>>
下面我們?cè)?Python3.4 下測(cè)試:
>>> add(2, 3)
Decorator 1
Decorator 2
5
>>> add.__wrapped__(2, 3)
Decorator 2
5
>>>
最后要說(shuō)的是,并不是所有的裝飾器都使用了@wraps
,因此這里的方案并不全部適用。 特別的,內(nèi)置的裝飾器@staticmethod
和 @classmethod
就沒(méi)有遵循這個(gè)約定 (它們把原始函數(shù)存儲(chǔ)在屬性 __func__
中)。
你想定義一個(gè)可以接受參數(shù)的裝飾器
我們用一個(gè)例子詳細(xì)闡述下接受參數(shù)的處理過(guò)程。 假設(shè)你想寫(xiě)一個(gè)裝飾器,給函數(shù)添加日志功能,當(dāng)時(shí)允許用戶指定日志的級(jí)別和其他的選項(xiàng)。 下面是這個(gè)裝飾器的定義和使用示例:
from functools import wraps
import logging
def logged(level, name=None, message=None):
"""
Add logging to a function. level is the logging
level, name is the logger name, and message is the
log message. If name and message aren't specified,
they default to the function's module and name.
"""
def decorate(func):
logname = name if name else func.__module__
log = logging.getLogger(logname)
logmsg = message if message else func.__name__
@wraps(func)
def wrapper(*args, **kwargs):
log.log(level, logmsg)
return func(*args, **kwargs)
return wrapper
return decorate
# Example use
@logged(logging.DEBUG)
def add(x, y):
return x + y
@logged(logging.CRITICAL, 'example')
def spam():
print('Spam!')
初看起來(lái),這種實(shí)現(xiàn)看上去很復(fù)雜,但是核心思想很簡(jiǎn)單。 最外層的函數(shù)logged()
接受參數(shù)并將它們作用在內(nèi)部的裝飾器函數(shù)上面。 內(nèi)層的函數(shù) decorate()
接受一個(gè)函數(shù)作為參數(shù),然后在函數(shù)上面放置一個(gè)包裝器。 這里的關(guān)鍵點(diǎn)是包裝器是可以使用傳遞給 logged()
的參數(shù)的。
定義一個(gè)接受參數(shù)的包裝器看上去比較復(fù)雜主要是因?yàn)榈讓拥恼{(diào)用序列。特別的,如果你有下面這個(gè)代碼:
@decorator(x, y, z)
def func(a, b):
pass
裝飾器處理過(guò)程跟下面的調(diào)用是等效的;
def func(a, b):
pass
func = decorator(x, y, z)(func)
decorator(x, y, z)
的返回結(jié)果必須是一個(gè)可調(diào)用對(duì)象,它接受一個(gè)函數(shù)作為參數(shù)并包裝它, 可以參考9.7小節(jié)中另外一個(gè)可接受參數(shù)的包裝器例子。
你想寫(xiě)一個(gè)裝飾器來(lái)包裝一個(gè)函數(shù),并且允許用戶提供參數(shù)在運(yùn)行時(shí)控制裝飾器行為。
引入一個(gè)訪問(wèn)函數(shù),使用 nolocal
來(lái)修改內(nèi)部變量。 然后這個(gè)訪問(wèn)函數(shù)被作為一個(gè)屬性賦值給包裝函數(shù)。
from functools import wraps, partial
import logging
# Utility decorator to attach a function as an attribute of obj
def attach_wrapper(obj, func=None):
if func is None:
return partial(attach_wrapper, obj)
setattr(obj, func.__name__, func)
return func
def logged(level, name=None, message=None):
'''
Add logging to a function. level is the logging
level, name is the logger name, and message is the
log message. If name and message aren't specified,
they default to the function's module and name.
'''
def decorate(func):
logname = name if name else func.__module__
log = logging.getLogger(logname)
logmsg = message if message else func.__name__
@wraps(func)
def wrapper(*args, **kwargs):
log.log(level, logmsg)
return func(*args, **kwargs)
# Attach setter functions
@attach_wrapper(wrapper)
def set_level(newlevel):
nonlocal level
level = newlevel
@attach_wrapper(wrapper)
def set_message(newmsg):
nonlocal logmsg
logmsg = newmsg
return wrapper
return decorate
# Example use
@logged(logging.DEBUG)
def add(x, y):
return x + y
@logged(logging.CRITICAL, 'example')
def spam():
print('Spam!')
下面是交互環(huán)境下的使用例子:
>>> import logging
>>> logging.basicConfig(level=logging.DEBUG)
>>> add(2, 3)
DEBUG:__main__:add
5
>>> # Change the log message
>>> add.set_message('Add called')
>>> add(2, 3)
DEBUG:__main__:Add called
5
>>> # Change the log level
>>> add.set_level(logging.WARNING)
>>> add(2, 3)
WARNING:__main__:Add called
5
>>>
這一小節(jié)的關(guān)鍵點(diǎn)在于訪問(wèn)函數(shù)(如 set_message()
和 set_level()
),它們被作為屬性賦給包裝器。 每個(gè)訪問(wèn)函數(shù)允許使用 nonlocal
來(lái)修改函數(shù)內(nèi)部的變量。
還有一個(gè)令人吃驚的地方是訪問(wèn)函數(shù)會(huì)在多層裝飾器間傳播(如果你的裝飾器都使用了 @functools.wraps
注解)。 例如,假設(shè)你引入另外一個(gè)裝飾器,比如9.2小節(jié)中的 @timethis
,像下面這樣:
@timethis
@logged(logging.DEBUG)
def countdown(n):
while n > 0:
n -= 1
你會(huì)發(fā)現(xiàn)訪問(wèn)函數(shù)依舊有效:
>>> countdown(10000000)
DEBUG:__main__:countdown
countdown 0.8198461532592773
>>> countdown.set_level(logging.WARNING)
>>> countdown.set_message("Counting down to zero")
>>> countdown(10000000)
WARNING:__main__:Counting down to zero
countdown 0.8225970268249512
>>>
你還會(huì)發(fā)現(xiàn)即使裝飾器像下面這樣以相反的方向排放,效果也是一樣的:
@logged(logging.DEBUG)
@timethis
def countdown(n):
while n > 0:
n -= 1
還能通過(guò)使用 lambda 表達(dá)式代碼來(lái)讓訪問(wèn)函數(shù)的返回不同的設(shè)定值:
@attach_wrapper(wrapper)
def get_level():
return level
# Alternative
wrapper.get_level = lambda: level
一個(gè)比較難理解的地方就是對(duì)于訪問(wèn)函數(shù)的首次使用。例如,你可能會(huì)考慮另外一個(gè)方法直接訪問(wèn)函數(shù)的屬性,如下:
@wraps(func)
def wrapper(*args, **kwargs):
wrapper.log.log(wrapper.level, wrapper.logmsg)
return func(*args, **kwargs)
# Attach adjustable attributes
wrapper.level = level
wrapper.logmsg = logmsg
wrapper.log = log
這個(gè)方法也可能正常工作,但前提是它必須是最外層的裝飾器才行。 如果它的上面還有另外的裝飾器(比如上面提到的 @timethis
例子),那么它會(huì)隱藏底層屬性,使得修改它們沒(méi)有任何作用。 而通過(guò)使用訪問(wèn)函數(shù)就能避免這樣的局限性。
最后提一點(diǎn),這一小節(jié)的方案也可以作為9.9小節(jié)中裝飾器類的另一種實(shí)現(xiàn)方法。
你想寫(xiě)一個(gè)裝飾器,既可以不傳參數(shù)給它,比如 @decorator
, 也可以傳遞可選參數(shù)給它,比如@decorator(x,y,z)
。
下面是9.5小節(jié)中日志裝飾器的一個(gè)修改版本:
from functools import wraps, partial
import logging
def logged(func=None, *, level=logging.DEBUG, name=None, message=None):
if func is None:
return partial(logged, level=level, name=name, message=message)
logname = name if name else func.__module__
log = logging.getLogger(logname)
logmsg = message if message else func.__name__
@wraps(func)
def wrapper(*args, **kwargs):
log.log(level, logmsg)
return func(*args, **kwargs)
return wrapper
# Example use
@logged
def add(x, y):
return x + y
@logged(level=logging.CRITICAL, name='example')
def spam():
print('Spam!')
可以看到,@logged
裝飾器可以同時(shí)不帶參數(shù)或帶參數(shù)。
這里提到的這個(gè)問(wèn)題就是通常所說(shuō)的編程一致性問(wèn)題。 當(dāng)我們使用裝飾器的時(shí)候,大部分程序員習(xí)慣了要么不給它們傳遞任何參數(shù),要么給它們傳遞確切參數(shù)。 其實(shí)從技術(shù)上來(lái)講,我們可以定義一個(gè)所有參數(shù)都是可選的裝飾器,就像下面這樣:
@logged()
def add(x, y):
return x+y
但是,這種寫(xiě)法并不符合我們的習(xí)慣,有時(shí)候程序員忘記加上后面的括號(hào)會(huì)導(dǎo)致錯(cuò)誤。 這里我們向你展示了如何以一致的編程風(fēng)格來(lái)同時(shí)滿足沒(méi)有括號(hào)和有括號(hào)兩種情況。
為了理解代碼是如何工作的,你需要非常熟悉裝飾器是如何作用到函數(shù)上以及它們的調(diào)用規(guī)則。 對(duì)于一個(gè)像下面這樣的簡(jiǎn)單裝飾器:
# Example use
@logged
def add(x, y):
return x + y
這個(gè)調(diào)用序列跟下面等價(jià):
def add(x, y):
return x + y
add = logged(add)
這時(shí)候,被裝飾函數(shù)會(huì)被當(dāng)做第一個(gè)參數(shù)直接傳遞給 logged
裝飾器。 因此,logged()
中的第一個(gè)參數(shù)就是被包裝函數(shù)本身。所有其他參數(shù)都必須有默認(rèn)值。
而對(duì)于一個(gè)下面這樣有參數(shù)的裝飾器:
@logged(level=logging.CRITICAL, name='example')
def spam():
print('Spam!')
調(diào)用序列跟下面等價(jià):
def spam():
print('Spam!')
spam = logged(level=logging.CRITICAL, name='example')(spam)
初始調(diào)用 logged()
函數(shù)時(shí),被包裝函數(shù)并沒(méi)有傳遞進(jìn)來(lái)。 因此在裝飾器內(nèi),它必須是可選的。這個(gè)反過(guò)來(lái)會(huì)迫使其他參數(shù)必須使用關(guān)鍵字來(lái)指定。 并且,但這些參數(shù)被傳遞進(jìn)來(lái)后,裝飾器要返回一個(gè)接受一個(gè)函數(shù)參數(shù)并包裝它的函數(shù)(參考9.5小節(jié))。 為了這樣做,我們使用了一個(gè)技巧,就是利用 functools.partial
。 它會(huì)返回一個(gè)未完全初始化的自身,除了被包裝函數(shù)外其他參數(shù)都已經(jīng)確定下來(lái)了。 可以參考7.8小節(jié)獲取更多 partial()
方法的知識(shí)。
作為某種編程規(guī)約,你想在對(duì)函數(shù)參數(shù)進(jìn)行強(qiáng)制類型檢查。
在演示實(shí)際代碼前,先說(shuō)明我們的目標(biāo):能對(duì)函數(shù)參數(shù)類型進(jìn)行斷言,類似下面這樣:
>>> @typeassert(int, int)
... def add(x, y):
... return x + y
...
>>>
>>> add(2, 3)
5
>>> add(2, 'hello')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "contract.py", line 33, in wrapper
TypeError: Argument y must be <class 'int'>
>>>
下面是使用裝飾器技術(shù)來(lái)實(shí)現(xiàn) @typeassert
:
from inspect import signature
from functools import wraps
def typeassert(*ty_args, **ty_kwargs):
def decorate(func):
# If in optimized mode, disable type checking
if not __debug__:
return func
# Map function argument names to supplied types
sig = signature(func)
bound_types = sig.bind_partial(*ty_args, **ty_kwargs).arguments
@wraps(func)
def wrapper(*args, **kwargs):
bound_values = sig.bind(*args, **kwargs)
# Enforce type assertions across supplied arguments
for name, value in bound_values.arguments.items():
if name in bound_types:
if not isinstance(value, bound_types[name]):
raise TypeError(
'Argument {} must be {}'.format(name, bound_types[name])
)
return func(*args, **kwargs)
return wrapper
return decorate
可以看出這個(gè)裝飾器非常靈活,既可以指定所有參數(shù)類型,也可以只指定部分。 并且可以通過(guò)位置或關(guān)鍵字來(lái)指定參數(shù)類型。下面是使用示例:
>>> @typeassert(int, z=int)
... def spam(x, y, z=42):
... print(x, y, z)
...
>>> spam(1, 2, 3)
1 2 3
>>> spam(1, 'hello', 3)
1 hello 3
>>> spam(1, 'hello', 'world')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "contract.py", line 33, in wrapper
TypeError: Argument z must be <class 'int'>
>>>
這節(jié)是高級(jí)裝飾器示例,引入了很多重要的概念。
首先,裝飾器只會(huì)在函數(shù)定義時(shí)被調(diào)用一次。 有時(shí)候你去掉裝飾器的功能,那么你只需要簡(jiǎn)單的返回被裝飾函數(shù)即可。 下面的代碼中,如果全局變量 __debug__
被設(shè)置成了 False(當(dāng)你使用-O 或-OO 參數(shù)的優(yōu)化模式執(zhí)行程序時(shí)), 那么就直接返回未修改過(guò)的函數(shù)本身:
def decorate(func):
# If in optimized mode, disable type checking
if not __debug__:
return func
其次,這里還對(duì)被包裝函數(shù)的參數(shù)簽名進(jìn)行了檢查,我們使用了 inspect.signature()
函數(shù)。 簡(jiǎn)單來(lái)講,它運(yùn)行你提取一個(gè)可調(diào)用對(duì)象的參數(shù)簽名信息。例如:
>>> from inspect import signature
>>> def spam(x, y, z=42):
... pass
...
>>> sig = signature(spam)
>>> print(sig)
(x, y, z=42)
>>> sig.parameters
mappingproxy(OrderedDict([('x', <Parameter at 0x10077a050 'x'>),
('y', <Parameter at 0x10077a158 'y'>), ('z', <Parameter at 0x10077a1b0 'z'>)]))
>>> sig.parameters['z'].name
'z'
>>> sig.parameters['z'].default
42
>>> sig.parameters['z'].kind
<_ParameterKind: 'POSITIONAL_OR_KEYWORD'>
>>>
裝飾器的開(kāi)始部分,我們使用了 bind_partial()
方法來(lái)執(zhí)行從指定類型到名稱的部分綁定。 下面是例子演示:
>>> bound_types = sig.bind_partial(int,z=int)
>>> bound_types
<inspect.BoundArguments object at 0x10069bb50>
>>> bound_types.arguments
OrderedDict([('x', <class 'int'>), ('z', <class 'int'>)])
>>>
在這個(gè)部分綁定中,你可以注意到缺失的參數(shù)被忽略了(比如并沒(méi)有對(duì) y 進(jìn)行綁定)。 不過(guò)最重要的是創(chuàng)建了一個(gè)有序字典 bound_types.arguments
。 這個(gè)字典會(huì)將參數(shù)名以函數(shù)簽名中相同順序映射到指定的類型值上面去。 在我們的裝飾器例子中,這個(gè)映射包含了我們要強(qiáng)制指定的類型斷言。
在裝飾器創(chuàng)建的實(shí)際包裝函數(shù)中使用到了 sig.bind()
方法。bind()
跟 bind_partial()
類似,但是它不允許忽略任何參數(shù)。因此有了下面的結(jié)果:
>>> bound_values = sig.bind(1, 2, 3)
>>> bound_values.arguments
OrderedDict([('x', 1), ('y', 2), ('z', 3)])
>>>
使用這個(gè)映射我們可以很輕松的實(shí)現(xiàn)我們的強(qiáng)制類型檢查:
>>> for name, value in bound_values.arguments.items():
... if name in bound_types.arguments:
... if not isinstance(value, bound_types.arguments[name]):
... raise TypeError()
...
>>>
不過(guò)這個(gè)方案還有點(diǎn)小瑕疵,它對(duì)于有默認(rèn)值的參數(shù)并不適用。 比如下面的代碼可以正常工作,盡管 items 的類型是錯(cuò)誤的:
>>> @typeassert(int, list)
... def bar(x, items=None):
... if items is None:
... items = []
... items.append(x)
... return items
>>> bar(2)
[2]
>>> bar(2,3)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "contract.py", line 33, in wrapper
TypeError: Argument items must be <class 'list'>
>>> bar(4, [1, 2, 3])
[1, 2, 3, 4]
>>>
最后一點(diǎn)是關(guān)于適用裝飾器參數(shù)和函數(shù)注解之間的爭(zhēng)論。 例如,為什么不像下面這樣寫(xiě)一個(gè)裝飾器來(lái)查找函數(shù)中的注解呢?
@typeassert
def spam(x:int, y, z:int = 42):
print(x,y,z)
一個(gè)可能的原因是如果使用了函數(shù)參數(shù)注解,那么就被限制了。 如果注解被用來(lái)做類型檢查就不能做其他事情了。而且 @typeassert
不能再用于使用注解做其他事情的函數(shù)了。 而使用上面的裝飾器參數(shù)靈活性大多了,也更加通用。
可以在 PEP 362 以及 inspect
模塊中找到更多關(guān)于函數(shù)參數(shù)對(duì)象的信息。在9.16小節(jié)還有另外一個(gè)例子。
你想在類中定義裝飾器,并將其作用在其他函數(shù)或方法上。
在類里面定義裝飾器很簡(jiǎn)單,但是你首先要確認(rèn)它的使用方式。比如到底是作為一個(gè)實(shí)例方法還是類方法。 下面我們用例子來(lái)闡述它們的不同:
from functools import wraps
class A:
# Decorator as an instance method
def decorator1(self, func):
@wraps(func)
def wrapper(*args, **kwargs):
print('Decorator 1')
return func(*args, **kwargs)
return wrapper
# Decorator as a class method
@classmethod
def decorator2(cls, func):
@wraps(func)
def wrapper(*args, **kwargs):
print('Decorator 2')
return func(*args, **kwargs)
return wrapper
下面是一使用例子:
# As an instance method
a = A()
@a.decorator1
def spam():
pass
# As a class method
@A.decorator2
def grok():
pass
仔細(xì)觀察可以發(fā)現(xiàn)一個(gè)是實(shí)例調(diào)用,一個(gè)是類調(diào)用。
在類中定義裝飾器初看上去好像很奇怪,但是在標(biāo)準(zhǔn)庫(kù)中有很多這樣的例子。 特別的,@property
裝飾器實(shí)際上是一個(gè)類,它里面定義了三個(gè)方法 getter(), setter(), deleter() ,
每一個(gè)方法都是一個(gè)裝飾器。例如:
class Person:
# Create a property instance
first_name = property()
# Apply decorator methods
@first_name.getter
def first_name(self):
return self._first_name
@first_name.setter
def first_name(self, value):
if not isinstance(value, str):
raise TypeError('Expected a string')
self._first_name = value
它為什么要這么定義的主要原因是各種不同的裝飾器方法會(huì)在關(guān)聯(lián)的 property
實(shí)例上操作它的狀態(tài)。 因此,任何時(shí)候只要你碰到需要在裝飾器中記錄或綁定信息,那么這不失為一種可行方法。
在類中定義裝飾器有個(gè)難理解的地方就是對(duì)于額外參數(shù) self
或 cls
的正確使用。 盡管最外層的裝飾器函數(shù)比如 decorator1()
或 decorator2()
需要提供一個(gè) self
或 cls
參數(shù), 但是在兩個(gè)裝飾器內(nèi)部被創(chuàng)建的wrapper()
函數(shù)并不需要包含這個(gè) self
參數(shù)。 你唯一需要這個(gè)參數(shù)是在你確實(shí)要訪問(wèn)包裝器中這個(gè)實(shí)例的某些部分的時(shí)候。其他情況下都不用去管它。
對(duì)于類里面定義的包裝器還有一點(diǎn)比較難理解,就是在涉及到繼承的時(shí)候。 例如,假設(shè)你想讓在 A 中定義的裝飾器作用在子類 B 中。你需要像下面這樣寫(xiě):
class B(A):
@A.decorator2
def bar(self):
pass
也就是說(shuō),裝飾器要被定義成類方法并且你必須顯式的使用父類名去調(diào)用它。 你不能使用 @B.decorator2
,因?yàn)樵诜椒ǘx時(shí),這個(gè)類 B 還沒(méi)有被創(chuàng)建。
你想使用一個(gè)裝飾器去包裝函數(shù),但是希望返回一個(gè)可調(diào)用的實(shí)例。 你需要讓你的裝飾器可以同時(shí)工作在類定義的內(nèi)部和外部。
為了將裝飾器定義成一個(gè)實(shí)例,你需要確保它實(shí)現(xiàn)了 __call__()
和__get__()
方法。 例如,下面的代碼定義了一個(gè)類,它在其他函數(shù)上放置一個(gè)簡(jiǎn)單的記錄層:
import types
from functools import wraps
class Profiled:
def __init__(self, func):
wraps(func)(self)
self.ncalls = 0
def __call__(self, *args, **kwargs):
self.ncalls += 1
return self.__wrapped__(*args, **kwargs)
def __get__(self, instance, cls):
if instance is None:
return self
else:
return types.MethodType(self, instance)
你可以將它當(dāng)做一個(gè)普通的裝飾器來(lái)使用,在類里面或外面都可以:
@Profiled
def add(x, y):
return x + y
class Spam:
@Profiled
def bar(self, x):
print(self, x)
在交互環(huán)境中的使用示例:
>>> add(2, 3)
5
>>> add(4, 5)
9
>>> add.ncalls
2
>>> s = Spam()
>>> s.bar(1)
<__main__.Spam object at 0x10069e9d0> 1
>>> s.bar(2)
<__main__.Spam object at 0x10069e9d0> 2
>>> s.bar(3)
<__main__.Spam object at 0x10069e9d0> 3
>>> Spam.bar.ncalls
3
將裝飾器定義成類通常是很簡(jiǎn)單的。但是這里還是有一些細(xì)節(jié)需要解釋下,特別是當(dāng)你想將它作用在實(shí)例方法上的時(shí)候。
首先,使用 functools.wraps()
函數(shù)的作用跟之前還是一樣,將被包裝函數(shù)的元信息復(fù)制到可調(diào)用實(shí)例中去。
其次,通常很容易會(huì)忽視上面的 __get__()
方法。如果你忽略它,保持其他代碼不變?cè)俅芜\(yùn)行, 你會(huì)發(fā)現(xiàn)當(dāng)你去調(diào)用被裝飾實(shí)例方法時(shí)出現(xiàn)很奇怪的問(wèn)題。例如:
>>> s = Spam()
>>> s.bar(3)
Traceback (most recent call last):
...
TypeError: bar() missing 1 required positional argument: 'x'
出錯(cuò)原因是當(dāng)方法函數(shù)在一個(gè)類中被查找時(shí),它們的 __get__()
方法依據(jù)描述器協(xié)議被調(diào)用, 在8.9小節(jié)已經(jīng)講述過(guò)描述器協(xié)議了。在這里,__get__()
的目的是創(chuàng)建一個(gè)綁定方法對(duì)象 (最終會(huì)給這個(gè)方法傳遞 self 參數(shù))。下面是一個(gè)例子來(lái)演示底層原理:
>>> s = Spam()
>>> def grok(self, x):
... pass
...
>>> grok.__get__(s, Spam)
<bound method Spam.grok of <__main__.Spam object at 0x100671e90>>
>>>
__get__()
方法是為了確保綁定方法對(duì)象能被正確的創(chuàng)建。 type.MethodType()
手動(dòng)創(chuàng)建一個(gè)綁定方法來(lái)使用。只有當(dāng)實(shí)例被使用的時(shí)候綁定方法才會(huì)被創(chuàng)建。 如果這個(gè)方法是在類上面來(lái)訪問(wèn), 那么 __get__()
中的 instance 參數(shù)會(huì)被設(shè)置成 None 并直接返回 Profiled
實(shí)例本身。 這樣的話我們就可以提取它的 ncalls
屬性了。
如果你想避免一些混亂,也可以考慮另外一個(gè)使用閉包和 nonlocal
變量實(shí)現(xiàn)的裝飾器,這個(gè)在9.5小節(jié)有講到。例如:
import types
from functools import wraps
def profiled(func):
ncalls = 0
@wraps(func)
def wrapper(*args, **kwargs):
nonlocal ncalls
ncalls += 1
return func(*args, **kwargs)
wrapper.ncalls = lambda: ncalls
return wrapper
# Example
@profiled
def add(x, y):
return x + y
這個(gè)方式跟之前的效果幾乎一樣,除了對(duì)于 ncalls
的訪問(wèn)現(xiàn)在是通過(guò)一個(gè)被綁定為屬性的函數(shù)來(lái)實(shí)現(xiàn),例如:
>>> add(2, 3)
5
>>> add(4, 5)
9
>>> add.ncalls()
2
>>>
你想給類或靜態(tài)方法提供裝飾器。
給類或靜態(tài)方法提供裝飾器是很簡(jiǎn)單的,不過(guò)要確保裝飾器在 @classmethod
或 @staticmethod
之前。例如:
import time
from functools import wraps
# A simple decorator
def timethis(func):
@wraps(func)
def wrapper(*args, **kwargs):
start = time.time()
r = func(*args, **kwargs)
end = time.time()
print(end-start)
return r
return wrapper
# Class illustrating application of the decorator to different kinds of methods
class Spam:
@timethis
def instance_method(self, n):
print(self, n)
while n > 0:
n -= 1
@classmethod
@timethis
def class_method(cls, n):
print(cls, n)
while n > 0:
n -= 1
@staticmethod
@timethis
def static_method(n):
print(n)
while n > 0:
n -= 1
裝飾后的類和靜態(tài)方法可正常工作,只不過(guò)增加了額外的計(jì)時(shí)功能:
>>> s = Spam()
>>> s.instance_method(1000000)
<__main__.Spam object at 0x1006a6050> 1000000
0.11817407608032227
>>> Spam.class_method(1000000)
<class '__main__.Spam'> 1000000
0.11334395408630371
>>> Spam.static_method(1000000)
1000000
0.11740279197692871
>>>
如果你把裝飾器的順序?qū)戝e(cuò)了就會(huì)出錯(cuò)。例如,假設(shè)你像下面這樣寫(xiě):
class Spam:
@timethis
@staticmethod
def static_method(n):
print(n)
while n > 0:
n -= 1
那么你調(diào)用這個(gè)鏡頭方法時(shí)就會(huì)報(bào)錯(cuò):
>>> Spam.static_method(1000000)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "timethis.py", line 6, in wrapper
start = time.time()
TypeError: 'staticmethod' object is not callable
>>>
問(wèn)題在于 @classmethod
和 @staticmethod
實(shí)際上并不會(huì)創(chuàng)建可直接調(diào)用的對(duì)象, 而是創(chuàng)建特殊的描述器對(duì)象(參考8.9小節(jié))。因此當(dāng)你試著在其他裝飾器中將它們當(dāng)做函數(shù)來(lái)使用時(shí)就會(huì)出錯(cuò)。 確保這種裝飾器出現(xiàn)在裝飾器鏈中的第一個(gè)位置可以修復(fù)這個(gè)問(wèn)題。
當(dāng)我們?cè)诔橄蠡愔卸x類方法和靜態(tài)方法(參考8.12小節(jié))時(shí),這里講到的知識(shí)就很有用了。 例如,如果你想定義一個(gè)抽象類方法,可以使用類似下面的代碼:
from abc import ABCMeta, abstractmethod
class A(metaclass=ABCMeta):
@classmethod
@abstractmethod
def method(cls):
pass
在這段代碼中,@classmethod
跟 @abstractmethod
兩者的順序是有講究的,如果你調(diào)換它們的順序就會(huì)出錯(cuò)。
你想在裝飾器中給被包裝函數(shù)增加額外的參數(shù),但是不能影響這個(gè)函數(shù)現(xiàn)有的調(diào)用規(guī)則。
可以使用關(guān)鍵字參數(shù)來(lái)給被包裝函數(shù)增加額外參數(shù)??紤]下面的裝飾器:
from functools import wraps
def optional_debug(func):
@wraps(func)
def wrapper(*args, debug=False, **kwargs):
if debug:
print('Calling', func.__name__)
return func(*args, **kwargs)
return wrapper
>>> @optional_debug
... def spam(a,b,c):
... print(a,b,c)
...
>>> spam(1,2,3)
1 2 3
>>> spam(1,2,3, debug=True)
Calling spam
1 2 3
>>>
通過(guò)裝飾器來(lái)給被包裝函數(shù)增加參數(shù)的做法并不常見(jiàn)。 盡管如此,有時(shí)候它可以避免一些重復(fù)代碼。例如,如果你有下面這樣的代碼:
def a(x, debug=False):
if debug:
print('Calling a')
def b(x, y, z, debug=False):
if debug:
print('Calling b')
def c(x, y, debug=False):
if debug:
print('Calling c')
那么你可以將其重構(gòu)成這樣:
from functools import wraps
import inspect
def optional_debug(func):
if 'debug' in inspect.getargspec(func).args:
raise TypeError('debug argument already defined')
@wraps(func)
def wrapper(*args, debug=False, **kwargs):
if debug:
print('Calling', func.__name__)
return func(*args, **kwargs)
return wrapper
@optional_debug
def a(x):
pass
@optional_debug
def b(x, y, z):
pass
@optional_debug
def c(x, y):
pass
這種實(shí)現(xiàn)方案之所以行得通,在于強(qiáng)制關(guān)鍵字參數(shù)很容易被添加到接受 *args
和 **kwargs
參數(shù)的函數(shù)中。 通過(guò)使用強(qiáng)制關(guān)鍵字參數(shù),它被作為一個(gè)特殊情況被挑選出來(lái), 并且接下來(lái)僅僅使用剩余的位置和關(guān)鍵字參數(shù)去調(diào)用這個(gè)函數(shù)時(shí),這個(gè)特殊參數(shù)會(huì)被排除在外。 也就是說(shuō),它并不會(huì)被納入到**kwargs
中去。
還有一個(gè)難點(diǎn)就是如何去處理被添加的參數(shù)與被包裝函數(shù)參數(shù)直接的名字沖突。 例如,如果裝飾器 @optional_debug
作用在一個(gè)已經(jīng)擁有一個(gè) debug
參數(shù)的函數(shù)上時(shí)會(huì)有問(wèn)題。 這里我們?cè)黾恿艘徊矫謾z查。
上面的方案還可以更完美一點(diǎn),因?yàn)榫鞯某绦騿T應(yīng)該發(fā)現(xiàn)了被包裝函數(shù)的函數(shù)簽名其實(shí)是錯(cuò)誤的。例如:
>>> @optional_debug
... def add(x,y):
... return x+y
...
>>> import inspect
>>> print(inspect.signature(add))
(x, y)
>>>
通過(guò)如下的修改,可以解決這個(gè)問(wèn)題:
from functools import wraps
import inspect
def optional_debug(func):
if 'debug' in inspect.getargspec(func).args:
raise TypeError('debug argument already defined')
@wraps(func)
def wrapper(*args, debug=False, **kwargs):
if debug:
print('Calling', func.__name__)
return func(*args, **kwargs)
sig = inspect.signature(func)
parms = list(sig.parameters.values())
parms.append(inspect.Parameter('debug',
inspect.Parameter.KEYWORD_ONLY,
default=False))
wrapper.__signature__ = sig.replace(parameters=parms)
return wrapper
通過(guò)這樣的修改,包裝后的函數(shù)簽名就能正確的顯示 debug
參數(shù)的存在了。例如:
>>> @optional_debug
... def add(x,y):
... return x+y
...
>>> print(inspect.signature(add))
(x, y, *, debug=False)
>>> add(2,3)
5
>>>
參考9.16小節(jié)獲取更多關(guān)于函數(shù)簽名的信息。
你想通過(guò)反省或者重寫(xiě)類定義的某部分來(lái)修改它的行為,但是你又不希望使用繼承或元類的方式。
這種情況可能是類裝飾器最好的使用場(chǎng)景了。例如,下面是一個(gè)重寫(xiě)了特殊方法 __getattribute__
的類裝飾器, 可以打印日志:
def log_getattribute(cls):
# Get the original implementation
orig_getattribute = cls.__getattribute__
# Make a new definition
def new_getattribute(self, name):
print('getting:', name)
return orig_getattribute(self, name)
# Attach to the class and return
cls.__getattribute__ = new_getattribute
return cls
# Example use
@log_getattribute
class A:
def __init__(self,x):
self.x = x
def spam(self):
pass
下面是使用效果:
>>> a = A(42)
>>> a.x
getting: x
42
>>> a.spam()
getting: spam
>>>
類裝飾器通??梢宰鳛槠渌呒?jí)技術(shù)比如混入或元類的一種非常簡(jiǎn)潔的替代方案。 比如,上面示例中的另外一種實(shí)現(xiàn)使用到繼承:
class LoggedGetattribute:
def __getattribute__(self, name):
print('getting:', name)
return super().__getattribute__(name)
# Example:
class A(LoggedGetattribute):
def __init__(self,x):
self.x = x
def spam(self):
pass
這種方案也行得通,但是為了去理解它,你就必須知道方法調(diào)用順序、super()
以及其它8.7小節(jié)介紹的繼承知識(shí)。 某種程度上來(lái)講,類裝飾器方案就顯得更加直觀,并且它不會(huì)引入新的繼承體系。它的運(yùn)行速度也更快一些, 因?yàn)樗⒉灰蕾?super()
函數(shù)。
如果你系想在一個(gè)類上面使用多個(gè)類裝飾器,那么就需要注意下順序問(wèn)題。 例如,一個(gè)裝飾器 A 會(huì)將其裝飾的方法完整替換成另一種實(shí)現(xiàn), 而另一個(gè)裝飾器 B 只是簡(jiǎn)單的在其裝飾的方法中添加點(diǎn)額外邏輯。 那么這時(shí)候裝飾器A就需要放在裝飾器 B 的前面。
你還可以回顧一下8.13小節(jié)另外一個(gè)關(guān)于類裝飾器的有用的例子。
你想通過(guò)改變實(shí)例創(chuàng)建方式來(lái)實(shí)現(xiàn)單例、緩存或其他類似的特性。
Python 程序員都知道,如果你定義了一個(gè)類,就能像函數(shù)一樣的調(diào)用它來(lái)創(chuàng)建實(shí)例,例如:
class Spam:
def __init__(self, name):
self.name = name
a = Spam('Guido')
b = Spam('Diana')
如果你想自定義這個(gè)步驟,你可以定義一個(gè)元類并自己實(shí)現(xiàn) __call__()
方法。
為了演示,假設(shè)你不想任何人創(chuàng)建這個(gè)類的實(shí)例:
class NoInstances(type):
def __call__(self, *args, **kwargs):
raise TypeError("Can't instantiate directly")
# Example
class Spam(metaclass=NoInstances):
@staticmethod
def grok(x):
print('Spam.grok')
這樣的話,用戶只能調(diào)用這個(gè)類的靜態(tài)方法,而不能使用通常的方法來(lái)創(chuàng)建它的實(shí)例。例如:
>>> Spam.grok(42)
Spam.grok
>>> s = Spam()
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "example1.py", line 7, in __call__
raise TypeError("Can't instantiate directly")
TypeError: Can't instantiate directly
>>>
現(xiàn)在,假如你想實(shí)現(xiàn)單例模式(只能創(chuàng)建唯一實(shí)例的類),實(shí)現(xiàn)起來(lái)也很簡(jiǎn)單:
class Singleton(type):
def __init__(self, *args, **kwargs):
self.__instance = None
super().__init__(*args, **kwargs)
def __call__(self, *args, **kwargs):
if self.__instance is None:
self.__instance = super().__call__(*args, **kwargs)
return self.__instance
else:
return self.__instance
# Example
class Spam(metaclass=Singleton):
def __init__(self):
print('Creating Spam')
那么 Spam 類就只能創(chuàng)建唯一的實(shí)例了,演示如下:
>>> a = Spam()
Creating Spam
>>> b = Spam()
>>> a is b
True
>>> c = Spam()
>>> a is c
True
>>>
最后,假設(shè)你想創(chuàng)建8.25小節(jié)中那樣的緩存實(shí)例。下面我們可以通過(guò)元類來(lái)實(shí)現(xiàn):
import weakref
class Cached(type):
def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)
self.__cache = weakref.WeakValueDictionary()
def __call__(self, *args):
if args in self.__cache:
return self.__cache[args]
else:
obj = super().__call__(*args)
self.__cache[args] = obj
return obj
# Example
class Spam(metaclass=Cached):
def __init__(self, name):
print('Creating Spam({!r})'.format(name))
self.name = name
然后我也來(lái)測(cè)試一下:
>>> a = Spam('Guido')
Creating Spam('Guido')
>>> b = Spam('Diana')
Creating Spam('Diana')
>>> c = Spam('Guido') # Cached
>>> a is b
False
>>> a is c # Cached value returned
True
>>>
利用元類實(shí)現(xiàn)多種實(shí)例創(chuàng)建模式通常要比不使用元類的方式優(yōu)雅得多。
假設(shè)你不使用元類,你可能需要將類隱藏在某些工廠函數(shù)后面。 比如為了實(shí)現(xiàn)一個(gè)單例,你你可能會(huì)像下面這樣寫(xiě):
class _Spam:
def __init__(self):
print('Creating Spam')
_spam_instance = None
def Spam():
global _spam_in