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

2018-02-24 15:27 更新

問題

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

解決方案

在演示實(shí)際代碼前,先說明我們的目標(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ù)來實(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ā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ù)定義時(shí)被調(diào)用一次。 有時(shí)候你去掉裝飾器的功能,那么你只需要簡單的返回被裝飾函數(shù)即可。 下面的代碼中,如果全局變量 __debug__ 被設(shè)置成了False(當(dāng)你使用-O或-OO參數(shù)的優(yōu)化模式執(zhí)行程序時(shí)), 那么就直接返回未修改過的函數(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)行你提取一個(gè)可調(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'>)])
>>>

在這個(gè)部分綁定中,你可以注意到缺失的參數(shù)被忽略了(比如并沒有對y進(jìn)行綁定)。不過最重要的是創(chuàng)建了一個(gè)有序字典 bound_types.arguments 。這個(gè)字典會將參數(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()
...
>>>

不過這個(gè)方案還有點(diǎn)小瑕疵,它對于有默認(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ù)注解之間的爭論。例如,為什么不像下面這樣寫一個(gè)裝飾器來查找函數(shù)中的注解呢?

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

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

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

以上內(nèi)容是否對您有幫助:
在線筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號
微信公眾號

編程獅公眾號