鍍金池/ 教程/ Python/ 元編程
類與對象
模塊與包
數(shù)據(jù)編碼和處理
元編程
網(wǎng)絡(luò)與 Web 編程
數(shù)字日期和時間
測試、調(diào)試和異常
字符串和文本
文件與 IO
腳本編程與系統(tǒng)管理
迭代器與生成器
函數(shù)
C 語言擴(kuò)展
并發(fā)編程
數(shù)據(jù)結(jié)構(gòu)和算法

元編程

軟件開發(fā)領(lǐng)域中最經(jīng)典的口頭禪就是“don’t repeat yourself”。 也就是說,任何時候當(dāng)你的程序中存在高度重復(fù)(或者是通過剪切復(fù)制)的代碼時,都應(yīng)該想想是否有更好的解決方案。 在 Python 當(dāng)中,通常都可以通過元編程來解決這類問題。 簡而言之,元編程就是關(guān)于創(chuàng)建操作源代碼(比如修改、生成或包裝原來的代碼)的函數(shù)和類。 主要技術(shù)是使用裝飾器、類裝飾器和元類。不過還有一些其他技術(shù), 包括簽名對象、使用 exec() 執(zhí)行代碼以及對內(nèi)部函數(shù)和類的反射技術(shù)等。 本章的主要目的是向大家介紹這些元編程技術(shù),并且給出實例來演示它們是怎樣定制化你的源代碼行為的。

在函數(shù)上添加包裝器

問題

你想在函數(shù)上添加一個包裝器,增加額外的操作處理(比如日志、計時等)。

解決方案

如果你想使用額外的代碼包裝一個函數(shù),可以定義一個裝飾器函數(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
>>>

討論

一個裝飾器就是一個函數(shù),它接受一個函數(shù)作為參數(shù)并返回一個新的函數(shù)。 當(dāng)你像下面這樣寫:

@timethis
def countdown(n):
    pass

跟像下面這樣寫其實效果是一樣的:

def countdown(n):
    pass
countdown = timethis(countdown)

順便說一下,內(nèi)置的裝飾器比如 @staticmethod, @classmethod,@property原理也是一樣的。 例如,下面這兩個代碼片段是等價的:

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)部定義了一個使用 *args**kwargs來接受任意參數(shù)的函數(shù)。 在這個函數(shù)里面調(diào)用了原始函數(shù)并將其結(jié)果返回,不過你還可以添加其他額外的代碼(比如計時)。 然后這個新的函數(shù)包裝器被作為結(jié)果返回來代替原始函數(shù)。

需要強(qiáng)調(diào)的是裝飾器并不會修改原始函數(shù)的參數(shù)簽名以及返回值。 使用*args**kwargs目的就是確保任何參數(shù)都能適用。 而返回結(jié)果值基本都是調(diào)用原始函數(shù) func(*args, **kwargs) 的返回結(jié)果,其中 func 就是原始函數(shù)。

剛開始學(xué)習(xí)裝飾器的時候,會使用一些簡單的例子來說明,比如上面演示的這個。 不過實際場景使用時,還是有一些細(xì)節(jié)問題要注意的。 比如上面使用 @wraps(func) 注解是很重要的, 它能保留原始函數(shù)的元數(shù)據(jù)(下一小節(jié)會講到),新手經(jīng)常會忽略這個細(xì)節(jié)。 接下來的幾個小節(jié)我們會更加深入的講解裝飾器函數(shù)的細(xì)節(jié)問題,如果你想構(gòu)造你自己的裝飾器函數(shù),需要認(rèn)真看一下。

創(chuàng)建裝飾器時保留函數(shù)元信息

問題

你寫了一個裝飾器作用在某個函數(shù)上,但是這個函數(shù)的重要的元信息比如名字、文檔字符串、注解和參數(shù)簽名都丟失了。

解決方案

任何時候你定義裝飾器的時候,都應(yīng)該使用 functools 庫中的 @wraps裝飾器來注解底層包裝函數(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

下面我們使用這個被包裝后的函數(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'>}
>>>

討論

在編寫裝飾器的時候復(fù)制元信息是一個非常重要的部分。如果你忘記了使用 @wrap , 那么你會發(fā)現(xiàn)被裝飾函數(shù)丟失了所有有用的信息。比如如果忽略 @wrap 后的效果是下面這樣的:

>>> countdown.__name__
'wrapper'
>>> countdown.__doc__
>>> countdown.__annotations__
{}
>>>

@wraps 有一個重要特征是它能讓你通過屬性__wrapped__ 直接訪問被包裝函數(shù)。例如:

>>> countdown.__wrapped__(100000)
>>>

__wrapped__ 屬性還能讓被裝飾函數(shù)正確暴露底層的參數(shù)簽名信息。例如:

>>> from inspect import signature
>>> print(signature(countdown))
(n:int)
>>>

一個很普遍的問題是怎樣讓裝飾器去直接復(fù)制原始函數(shù)的參數(shù)簽名信息, 如果想自己手動實現(xiàn)的話需要做大量的工作,最好就簡單的使用 __wrapped__裝飾器。 通過底層的 __wrapped__屬性訪問到函數(shù)簽名信息。更多關(guān)于簽名的內(nèi)容可以參考9.16小節(jié)。

解除一個裝飾器

問題

一個裝飾器已經(jīng)作用在一個函數(shù)上,你想撤銷它,直接訪問原始的未包裝的那個函數(shù)。

解決方案

假設(shè)裝飾器是通過 @wraps (參考9.2小節(jié))來實現(xiàn)的,那么你可以通過訪問 __wrapped__屬性來訪問原始函數(shù):

>>> @somedecorator
>>> def add(x, y):
...     return x + y
...
>>> orig_add = add.__wrapped__
>>> orig_add(3, 4)
7
>>>

討論

直接訪問未包裝的原始函數(shù)在調(diào)試、內(nèi)省和其他函數(shù)操作時是很有用的。 但是我們這里的方案僅僅適用于在包裝器中正確使用了@wraps 或者直接設(shè)置了 __wrapped__ 屬性的情況。

如果有多個包裝器,那么訪問 __wrapped__ 屬性的行為是不可預(yù)知的,應(yīng)該避免這樣做。 在 Python3.3 中,它會略過所有的包裝層,比如,假如你有如下的代碼:

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

下面我們在 Python3.3 下測試:

>>> add(2, 3)
Decorator 1
Decorator 2
5
>>> add.__wrapped__(2, 3)
5
>>>

下面我們在 Python3.4 下測試:

>>> add(2, 3)
Decorator 1
Decorator 2
5
>>> add.__wrapped__(2, 3)
Decorator 2
5
>>>

最后要說的是,并不是所有的裝飾器都使用了@wraps ,因此這里的方案并不全部適用。 特別的,內(nèi)置的裝飾器@staticmethod@classmethod就沒有遵循這個約定 (它們把原始函數(shù)存儲在屬性 __func__中)。

定義一個帶參數(shù)的裝飾器

問題

你想定義一個可以接受參數(shù)的裝飾器

解決方案

我們用一個例子詳細(xì)闡述下接受參數(shù)的處理過程。 假設(shè)你想寫一個裝飾器,給函數(shù)添加日志功能,當(dāng)時允許用戶指定日志的級別和其他的選項。 下面是這個裝飾器的定義和使用示例:

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!')

初看起來,這種實現(xiàn)看上去很復(fù)雜,但是核心思想很簡單。 最外層的函數(shù)logged() 接受參數(shù)并將它們作用在內(nèi)部的裝飾器函數(shù)上面。 內(nèi)層的函數(shù) decorate() 接受一個函數(shù)作為參數(shù),然后在函數(shù)上面放置一個包裝器。 這里的關(guān)鍵點是包裝器是可以使用傳遞給 logged() 的參數(shù)的。

討論

定義一個接受參數(shù)的包裝器看上去比較復(fù)雜主要是因為底層的調(diào)用序列。特別的,如果你有下面這個代碼:

@decorator(x, y, z)
def func(a, b):
    pass

裝飾器處理過程跟下面的調(diào)用是等效的;

def func(a, b):
    pass
func = decorator(x, y, z)(func)

decorator(x, y, z)的返回結(jié)果必須是一個可調(diào)用對象,它接受一個函數(shù)作為參數(shù)并包裝它, 可以參考9.7小節(jié)中另外一個可接受參數(shù)的包裝器例子。

可自定義屬性的裝飾器

問題

你想寫一個裝飾器來包裝一個函數(shù),并且允許用戶提供參數(shù)在運(yùn)行時控制裝飾器行為。

解決方案

引入一個訪問函數(shù),使用 nolocal 來修改內(nèi)部變量。 然后這個訪問函數(shù)被作為一個屬性賦值給包裝函數(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)鍵點在于訪問函數(shù)(如 set_message()set_level() ),它們被作為屬性賦給包裝器。 每個訪問函數(shù)允許使用 nonlocal 來修改函數(shù)內(nèi)部的變量。

還有一個令人吃驚的地方是訪問函數(shù)會在多層裝飾器間傳播(如果你的裝飾器都使用了 @functools.wraps 注解)。 例如,假設(shè)你引入另外一個裝飾器,比如9.2小節(jié)中的 @timethis,像下面這樣:

@timethis
@logged(logging.DEBUG)
def countdown(n):
    while n > 0:
        n -= 1

你會發(fā)現(xià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
>>>

你還會發(fā)現(xiàn)即使裝飾器像下面這樣以相反的方向排放,效果也是一樣的:

@logged(logging.DEBUG)
@timethis
def countdown(n):
    while n > 0:
        n -= 1

還能通過使用 lambda 表達(dá)式代碼來讓訪問函數(shù)的返回不同的設(shè)定值:

@attach_wrapper(wrapper)
def get_level():
    return level

# Alternative
wrapper.get_level = lambda: level

一個比較難理解的地方就是對于訪問函數(shù)的首次使用。例如,你可能會考慮另外一個方法直接訪問函數(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

這個方法也可能正常工作,但前提是它必須是最外層的裝飾器才行。 如果它的上面還有另外的裝飾器(比如上面提到的 @timethis 例子),那么它會隱藏底層屬性,使得修改它們沒有任何作用。 而通過使用訪問函數(shù)就能避免這樣的局限性。

最后提一點,這一小節(jié)的方案也可以作為9.9小節(jié)中裝飾器類的另一種實現(xiàn)方法。

帶可選參數(shù)的裝飾器

問題

你想寫一個裝飾器,既可以不傳參數(shù)給它,比如 @decorator , 也可以傳遞可選參數(shù)給它,比如@decorator(x,y,z) 。

解決方案

下面是9.5小節(jié)中日志裝飾器的一個修改版本:

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ù)。

討論

這里提到的這個問題就是通常所說的編程一致性問題。 當(dāng)我們使用裝飾器的時候,大部分程序員習(xí)慣了要么不給它們傳遞任何參數(shù),要么給它們傳遞確切參數(shù)。 其實從技術(shù)上來講,我們可以定義一個所有參數(shù)都是可選的裝飾器,就像下面這樣:

@logged()
def add(x, y):
    return x+y

但是,這種寫法并不符合我們的習(xí)慣,有時候程序員忘記加上后面的括號會導(dǎo)致錯誤。 這里我們向你展示了如何以一致的編程風(fēng)格來同時滿足沒有括號和有括號兩種情況。

為了理解代碼是如何工作的,你需要非常熟悉裝飾器是如何作用到函數(shù)上以及它們的調(diào)用規(guī)則。 對于一個像下面這樣的簡單裝飾器:

# Example use
@logged
def add(x, y):
    return x + y

這個調(diào)用序列跟下面等價:

def add(x, y):
    return x + y

add = logged(add)

這時候,被裝飾函數(shù)會被當(dāng)做第一個參數(shù)直接傳遞給 logged裝飾器。 因此,logged() 中的第一個參數(shù)就是被包裝函數(shù)本身。所有其他參數(shù)都必須有默認(rèn)值。

而對于一個下面這樣有參數(shù)的裝飾器:

@logged(level=logging.CRITICAL, name='example')
def spam():
    print('Spam!')

調(diào)用序列跟下面等價:

def spam():
    print('Spam!')
spam = logged(level=logging.CRITICAL, name='example')(spam)

初始調(diào)用 logged() 函數(shù)時,被包裝函數(shù)并沒有傳遞進(jìn)來。 因此在裝飾器內(nèi),它必須是可選的。這個反過來會迫使其他參數(shù)必須使用關(guān)鍵字來指定。 并且,但這些參數(shù)被傳遞進(jìn)來后,裝飾器要返回一個接受一個函數(shù)參數(shù)并包裝它的函數(shù)(參考9.5小節(jié))。 為了這樣做,我們使用了一個技巧,就是利用 functools.partial 。 它會返回一個未完全初始化的自身,除了被包裝函數(shù)外其他參數(shù)都已經(jīng)確定下來了。 可以參考7.8小節(jié)獲取更多 partial() 方法的知識。

利用裝飾器強(qiáng)制函數(shù)上的類型檢查

問題

作為某種編程規(guī)約,你想在對函數(shù)參數(shù)進(jìn)行強(qiáng)制類型檢查。

解決方案

在演示實際代碼前,先說明我們的目標(biāo):能對函數(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ù)來實現(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

可以看出這個裝飾器非常靈活,既可以指定所有參數(shù)類型,也可以只指定部分。 并且可以通過位置或關(guān)鍵字來指定參數(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é)是高級裝飾器示例,引入了很多重要的概念。

首先,裝飾器只會在函數(shù)定義時被調(diào)用一次。 有時候你去掉裝飾器的功能,那么你只需要簡單的返回被裝飾函數(shù)即可。 下面的代碼中,如果全局變量 __debug__ 被設(shè)置成了 False(當(dāng)你使用-O 或-OO 參數(shù)的優(yōu)化模式執(zhí)行程序時), 那么就直接返回未修改過的函數(shù)本身:

def decorate(func):
    # If in optimized mode, disable type checking
    if not __debug__:
        return func

其次,這里還對被包裝函數(shù)的參數(shù)簽名進(jìn)行了檢查,我們使用了 inspect.signature()函數(shù)。 簡單來講,它運(yùn)行你提取一個可調(diào)用對象的參數(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'>
>>>

裝飾器的開始部分,我們使用了 bind_partial() 方法來執(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'>)])
>>>

在這個部分綁定中,你可以注意到缺失的參數(shù)被忽略了(比如并沒有對 y 進(jìn)行綁定)。 不過最重要的是創(chuàng)建了一個有序字典 bound_types.arguments。 這個字典會將參數(shù)名以函數(shù)簽名中相同順序映射到指定的類型值上面去。 在我們的裝飾器例子中,這個映射包含了我們要強(qiáng)制指定的類型斷言。

在裝飾器創(chuàng)建的實際包裝函數(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)])
>>>

使用這個映射我們可以很輕松的實現(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()
...
>>>

不過這個方案還有點小瑕疵,它對于有默認(rèn)值的參數(shù)并不適用。 比如下面的代碼可以正常工作,盡管 items 的類型是錯誤的:

>>> @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]
>>>

最后一點是關(guān)于適用裝飾器參數(shù)和函數(shù)注解之間的爭論。 例如,為什么不像下面這樣寫一個裝飾器來查找函數(shù)中的注解呢?

@typeassert
def spam(x:int, y, z:int = 42):
    print(x,y,z)

一個可能的原因是如果使用了函數(shù)參數(shù)注解,那么就被限制了。 如果注解被用來做類型檢查就不能做其他事情了。而且 @typeassert 不能再用于使用注解做其他事情的函數(shù)了。 而使用上面的裝飾器參數(shù)靈活性大多了,也更加通用。

可以在 PEP 362 以及 inspect 模塊中找到更多關(guān)于函數(shù)參數(shù)對象的信息。在9.16小節(jié)還有另外一個例子。

將裝飾器定義為類的一部分

問題

你想在類中定義裝飾器,并將其作用在其他函數(shù)或方法上。

解決方案

在類里面定義裝飾器很簡單,但是你首先要確認(rèn)它的使用方式。比如到底是作為一個實例方法還是類方法。 下面我們用例子來闡述它們的不同:

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)一個是實例調(diào)用,一個是類調(diào)用。

討論

在類中定義裝飾器初看上去好像很奇怪,但是在標(biāo)準(zhǔn)庫中有很多這樣的例子。 特別的,@property 裝飾器實際上是一個類,它里面定義了三個方法 getter(), setter(), deleter() ,每一個方法都是一個裝飾器。例如:

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

它為什么要這么定義的主要原因是各種不同的裝飾器方法會在關(guān)聯(lián)的 property實例上操作它的狀態(tài)。 因此,任何時候只要你碰到需要在裝飾器中記錄或綁定信息,那么這不失為一種可行方法。

在類中定義裝飾器有個難理解的地方就是對于額外參數(shù) selfcls 的正確使用。 盡管最外層的裝飾器函數(shù)比如 decorator1()decorator2()需要提供一個 selfcls參數(shù), 但是在兩個裝飾器內(nèi)部被創(chuàng)建的wrapper() 函數(shù)并不需要包含這個 self參數(shù)。 你唯一需要這個參數(shù)是在你確實要訪問包裝器中這個實例的某些部分的時候。其他情況下都不用去管它。

對于類里面定義的包裝器還有一點比較難理解,就是在涉及到繼承的時候。 例如,假設(shè)你想讓在 A 中定義的裝飾器作用在子類 B 中。你需要像下面這樣寫:

class B(A):
    @A.decorator2
    def bar(self):
        pass

也就是說,裝飾器要被定義成類方法并且你必須顯式的使用父類名去調(diào)用它。 你不能使用 @B.decorator2,因為在方法定義時,這個類 B 還沒有被創(chuàng)建。

將裝飾器定義為類

問題

你想使用一個裝飾器去包裝函數(shù),但是希望返回一個可調(diào)用的實例。 你需要讓你的裝飾器可以同時工作在類定義的內(nèi)部和外部。

解決方案

為了將裝飾器定義成一個實例,你需要確保它實現(xiàn)了 __call__()__get__() 方法。 例如,下面的代碼定義了一個類,它在其他函數(shù)上放置一個簡單的記錄層:

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)做一個普通的裝飾器來使用,在類里面或外面都可以:

@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

討論

將裝飾器定義成類通常是很簡單的。但是這里還是有一些細(xì)節(jié)需要解釋下,特別是當(dāng)你想將它作用在實例方法上的時候。

首先,使用 functools.wraps()函數(shù)的作用跟之前還是一樣,將被包裝函數(shù)的元信息復(fù)制到可調(diào)用實例中去。

其次,通常很容易會忽視上面的 __get__() 方法。如果你忽略它,保持其他代碼不變再次運(yùn)行, 你會發(fā)現(xiàn)當(dāng)你去調(diào)用被裝飾實例方法時出現(xiàn)很奇怪的問題。例如:

>>> s = Spam()
>>> s.bar(3)
Traceback (most recent call last):
...
TypeError: bar() missing 1 required positional argument: 'x'

出錯原因是當(dāng)方法函數(shù)在一個類中被查找時,它們的 __get__() 方法依據(jù)描述器協(xié)議被調(diào)用, 在8.9小節(jié)已經(jīng)講述過描述器協(xié)議了。在這里,__get__()的目的是創(chuàng)建一個綁定方法對象 (最終會給這個方法傳遞 self 參數(shù))。下面是一個例子來演示底層原理:

>>> s = Spam()
>>> def grok(self, x):
...     pass
...
>>> grok.__get__(s, Spam)
<bound method Spam.grok of <__main__.Spam object at 0x100671e90>>
>>>

__get__()方法是為了確保綁定方法對象能被正確的創(chuàng)建。 type.MethodType() 手動創(chuàng)建一個綁定方法來使用。只有當(dāng)實例被使用的時候綁定方法才會被創(chuàng)建。 如果這個方法是在類上面來訪問, 那么 __get__() 中的 instance 參數(shù)會被設(shè)置成 None 并直接返回 Profiled實例本身。 這樣的話我們就可以提取它的 ncalls屬性了。

如果你想避免一些混亂,也可以考慮另外一個使用閉包和 nonlocal 變量實現(xiàn)的裝飾器,這個在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

這個方式跟之前的效果幾乎一樣,除了對于 ncalls的訪問現(xiàn)在是通過一個被綁定為屬性的函數(shù)來實現(xiàn),例如:

>>> add(2, 3)
5
>>> add(4, 5)
9
>>> add.ncalls()
2
>>>

為類和靜態(tài)方法提供裝飾器

問題

你想給類或靜態(tài)方法提供裝飾器。

解決方案

給類或靜態(tài)方法提供裝飾器是很簡單的,不過要確保裝飾器在 @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)方法可正常工作,只不過增加了額外的計時功能:

>>> 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了就會出錯。例如,假設(shè)你像下面這樣寫:

class Spam:
    @timethis
    @staticmethod
    def static_method(n):
        print(n)
        while n > 0:
            n -= 1

那么你調(diào)用這個鏡頭方法時就會報錯:

>>> 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
>>>

問題在于 @classmethod@staticmethod實際上并不會創(chuàng)建可直接調(diào)用的對象, 而是創(chuàng)建特殊的描述器對象(參考8.9小節(jié))。因此當(dāng)你試著在其他裝飾器中將它們當(dāng)做函數(shù)來使用時就會出錯。 確保這種裝飾器出現(xiàn)在裝飾器鏈中的第一個位置可以修復(fù)這個問題。

當(dāng)我們在抽象基類中定義類方法和靜態(tài)方法(參考8.12小節(jié))時,這里講到的知識就很有用了。 例如,如果你想定義一個抽象類方法,可以使用類似下面的代碼:

from abc import ABCMeta, abstractmethod
class A(metaclass=ABCMeta):
    @classmethod
    @abstractmethod
    def method(cls):
        pass

在這段代碼中,@classmethod@abstractmethod兩者的順序是有講究的,如果你調(diào)換它們的順序就會出錯。

裝飾器為被包裝函數(shù)增加參數(shù)

問題

你想在裝飾器中給被包裝函數(shù)增加額外的參數(shù),但是不能影響這個函數(shù)現(xiàn)有的調(diào)用規(guī)則。

解決方案

可以使用關(guān)鍵字參數(shù)來給被包裝函數(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
>>>

討論

通過裝飾器來給被包裝函數(shù)增加參數(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

這種實現(xiàn)方案之所以行得通,在于強(qiáng)制關(guān)鍵字參數(shù)很容易被添加到接受 *args**kwargs 參數(shù)的函數(shù)中。 通過使用強(qiáng)制關(guān)鍵字參數(shù),它被作為一個特殊情況被挑選出來, 并且接下來僅僅使用剩余的位置和關(guān)鍵字參數(shù)去調(diào)用這個函數(shù)時,這個特殊參數(shù)會被排除在外。 也就是說,它并不會被納入到**kwargs中去。

還有一個難點就是如何去處理被添加的參數(shù)與被包裝函數(shù)參數(shù)直接的名字沖突。 例如,如果裝飾器 @optional_debug 作用在一個已經(jīng)擁有一個 debug 參數(shù)的函數(shù)上時會有問題。 這里我們增加了一步名字檢查。

上面的方案還可以更完美一點,因為精明的程序員應(yīng)該發(fā)現(xiàn)了被包裝函數(shù)的函數(shù)簽名其實是錯誤的。例如:

>>> @optional_debug
... def add(x,y):
...     return x+y
...
>>> import inspect
>>> print(inspect.signature(add))
(x, y)
>>>

通過如下的修改,可以解決這個問題:

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

通過這樣的修改,包裝后的函數(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ù)簽名的信息。

使用裝飾器擴(kuò)充類的功能

問題

你想通過反省或者重寫類定義的某部分來修改它的行為,但是你又不希望使用繼承或元類的方式。

解決方案

這種情況可能是類裝飾器最好的使用場景了。例如,下面是一個重寫了特殊方法 __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
>>>

討論

類裝飾器通常可以作為其他高級技術(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é)介紹的繼承知識。 某種程度上來講,類裝飾器方案就顯得更加直觀,并且它不會引入新的繼承體系。它的運(yùn)行速度也更快一些, 因為他并不依賴 super()函數(shù)。

如果你系想在一個類上面使用多個類裝飾器,那么就需要注意下順序問題。 例如,一個裝飾器 A 會將其裝飾的方法完整替換成另一種實現(xiàn), 而另一個裝飾器 B 只是簡單的在其裝飾的方法中添加點額外邏輯。 那么這時候裝飾器A就需要放在裝飾器 B 的前面。

你還可以回顧一下8.13小節(jié)另外一個關(guān)于類裝飾器的有用的例子。

使用元類控制實例的創(chuàng)建

問題

你想通過改變實例創(chuàng)建方式來實現(xiàn)單例、緩存或其他類似的特性。

解決方案

Python 程序員都知道,如果你定義了一個類,就能像函數(shù)一樣的調(diào)用它來創(chuàng)建實例,例如:

class Spam:
    def __init__(self, name):
        self.name = name

a = Spam('Guido')
b = Spam('Diana')

如果你想自定義這個步驟,你可以定義一個元類并自己實現(xiàn) __call__() 方法。

為了演示,假設(shè)你不想任何人創(chuàng)建這個類的實例:

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)用這個類的靜態(tài)方法,而不能使用通常的方法來創(chuàng)建它的實例。例如:

>>> 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)在,假如你想實現(xiàn)單例模式(只能創(chuàng)建唯一實例的類),實現(xià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)建唯一的實例了,演示如下:

>>> a = Spam()
Creating Spam
>>> b = Spam()
>>> a is b
True
>>> c = Spam()
>>> a is c
True
>>>

最后,假設(shè)你想創(chuàng)建8.25小節(jié)中那樣的緩存實例。下面我們可以通過元類來實現(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

然后我也來測試一下:

>>> 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
>>>

討論

利用元類實現(xiàn)多種實例創(chuàng)建模式通常要比不使用元類的方式優(yōu)雅得多。

假設(shè)你不使用元類,你可能需要將類隱藏在某些工廠函數(shù)后面。 比如為了實現(xiàn)一個單例,你你可能會像下面這樣寫:

class _Spam:
    def __init__(self):
        print('Creating Spam')

_spam_instance = None

def Spam():
    global _spam_in
上一篇:字符串和文本下一篇:模塊與包