天天看點

Python利用inpect子產品實作函數參數類型檢查

一丶函數注解Function Annotations

  • 函數注解
    • python3.5引入
    • 對函數的參數進行類型注解
    • 對函數的傳回值進行類型注解
    • 隻讀函數參數做一個輔助的說明,并不對函數進行類型檢查
    • 提供第三方工具,做代碼分析,發現隐藏的BUG
    • 函數注解的資訊儲存在__annotations__屬性中
In [1]: def add(x: int, y: int) -> int:
            return x + y
    
In [2]: add.__annotations__
Out[2]: {'x': int, 'y': int, 'return': int}
           
  • 變量注解
    • python3.6引入,隻是一種對變量的說明,非強制
  • 業務應用
    • 配合裝飾器和inpect子產品可以進行函數參數類型的檢查,下一節将介紹inpect子產品的用法

二丶inpect子產品

官方文檔:inpect子產品提供給了一些有用的函數幫助擷取對象的資訊,例如子產品、類、方法、函數、回溯、幀對象以及代碼對象。例如他可以幫你檢查類的内容,擷取某個方法的源代碼,取得并格式化某個函數的參數清單,或者擷取你需要顯示的回溯的詳細資訊。

該子產品提供了四種主要的功能:類型檢查、擷取源代碼、檢查類與函數、檢查解釋器的調用堆棧。

下面我們将就inspect子產品的檢查函數和類以及類型檢查進行分析。

1.類型和成員

In [4]: inspect.isfunction(add)  # 是否為函數
Out[4]: True

In [5]: inspect.ismethod(add)  # 是否為類的方法
Out[5]: False

In [6]: inspect.isgenerator(add)  # 是否是生成器對象
Out[6]: False

In [7]: inspect.isgeneratorfunction(add)  # 是否是生成器函數
Out[7]: False

In [8]: inspect.isclass(add)  # 是否是類
Out[8]: False

In [9]: inspect.ismodule(add)  # 是否是子產品
Out[9]: False

In [10]: inspect.isbuiltin(add)  # 是否是内建對象 
Out[10]: False
           

上面隻列舉了一些常用的is檢查類型函數,需要更多的可以查閱官方文檔。

2.使用signature對象擷取簽名

函數簽名包含了一個函數的資訊,包括函數名、他的參數類型、他的所在的類和名稱空間以及其他資訊。

  • inspect.signature(callable,*,follow_wrapped=True),傳回可調用對象的signature對象
In [18]: def add(x:int, y:int, *args, m=1, **kwargs) ->int:
    ...:     return x + y
    ...:     

In [19]: sig = inspect.signature(add)  # 擷取add函數的簽名指派給sig變量

In [20]: sig
Out[20]: <Signature (x: int, y: int, *args, m=1, **kwargs) -> int>

In [21]: type(sig)
Out[21]: inspect.Signature  # 類型是inpect子產品的簽名對象
           
  • return_annotation,傳回callable的"傳回"注釋,如果沒有傳回注釋,則傳回Signature.empty
  • empty是一個特殊的累級标記,用于指定缺少的傳回注釋
In [15]: sig.return_annotation
Out[15]: int

In [23]: sig.parameters["args"].annotation
Out[23]: inspect._empty 
           
  • parameters,作用是将參數名稱有序映射到相應的Paramater對象。參數以嚴格的定義順序,包括僅限關鍵字參數。實際上可以了解為傳回對象為一個OrderDict有序字典
In [31]: sig.parameters  # 有序字典,類似Orderdict
Out[31]: 
mappingproxy({'x': <Parameter "x: int">,
              'y': <Parameter "y: int">,
              'args': <Parameter "*args">,
              'm': <Parameter "m=1">,
              'kwargs': <Parameter "**kwargs">})
           
  • 下面介紹parameters對象的其他用法
In [24]: sig.parameters["x"]
Out[24]: <Parameter "x: int">

In [25]: sig.parameters["x"].annotation
Out[25]: int

In [29]: sig.parameters["args"]
Out[29]: <Parameter "*args">

In [30]: sig.parameters["args"].annotation
Out[30]: inspect._empty  # 可變位置參數args的注釋為inspect._empty類型

           
    • name,參數的名字
    • annotation,參數的注釋,可能為inspect._empty
    • default,參數的預設值,可能沒有定義
    • kind實參綁定到形參,就是形參的類型
      • POSITIONAL_ONLY,值必須做未知參數提供
      • POSITIONAL_OR_KEYWORD,值可以作為關鍵字或者位置參數提供
      • VAR_POSITIONAL,可變位置參數,對應*args
      • KEYWORD_ONLY,必須是僅限關鍵字參數keyword-only
      • VAR_KEYWORD,可變關鍵字參數,對應**kwargs
In [32]: for i, j in sig.parameters.items():
    ...:     print(j.name, j.default, j.annotation, j.kind)
    ...:     
x <class 'inspect._empty'> <class 'int'> POSITIONAL_OR_KEYWORD
y <class 'inspect._empty'> <class 'int'> POSITIONAL_OR_KEYWORD
args <class 'inspect._empty'> <class 'inspect._empty'> VAR_POSITIONAL
m 1 <class 'inspect._empty'> KEYWORD_ONLY
kwargs <class 'inspect._empty'> <class 'inspect._empty'> VAR_KEYWORD
           

3.手動實作參數類型檢查裝飾器

1°函數在定義的時候就被注解了,那麼如何來判斷我們輸入的是否滿足注解的要求呢?如果使用者輸入的資料和聲明的資料進行對比,如果不符合,提示使用者或者報異常。
import inspect
from functools import wraps


def decorator(fun):
    @wraps(fun)
    def wrapper(*args, **kwargs):
        sig = inspect.signature(fun)
        params = sig.parameters  # Orderdict
        paramsvalues = list(params.values())
        paramskeys = list(params.keys())
        for i, j in enumerate(args):
            if not isinstance(j, paramsvalues[i].annotation):
                # print("{}error input: {}= {}".format(paramskeys[i], j))
                raise TypeError("{}error input: {}= {}".format(paramskeys[i], j))
        for x, y in kwargs.items(): # x = y, y = 4
            if not isinstance(y,params["x"].annotation):
                # print("error input: {}= {}".format(x, y))
                raise TypeError("error input: {}= {}".format(x, y))
        return fun(*args, **kwargs)
    return wrapper


@decorator
def add(x: int, y: int) -> int:
    return x + y

           
2°如果我們在定義時沒給函數注解,而是動态的通過裝飾器傳入參數給注解,然後判斷輸入值是否滿足注解的要求,這樣這個函數的靈活性就會大大提高,那麼怎麼實作呢?

我們首先先來了解裡面兩個方法

  • bind_partial(*args, **kwargs)

    實作一種類似function.partial類似的模式,給函數"綁定"注解,傳回值為BoundArguments,如果傳遞的參數和簽名不比對,則引發TypeError。bind_partial(*args, **kwargs).argument傳回OrderDict的資料

In [33]: def add(x :int,y :int):
    ...:     return x+y
    ...: sig = inspect.signature(add)

In [34]: bound_type = sig.bind_partial(str,str) # 給add函數綁定兩個注解,但是并不會修改函數本身已有的注解
In [36]: bound_type.arguments
Out[36]: OrderedDict([('x', str), ('y', str)])

In [37]: sig
Out[37]: <Signature (x: int, y: int)>

In [38]: sig.parameters
Out[38]: mappingproxy({'x': <Parameter "x: int">, 'y': <Parameter "y: int">})
           
  • bind(*args, **kwargs)

    建立一個位置和關鍵字參數到實參的映射,如果參數和簽名比對,則傳回BoundArguments,注意這裡不允許忽略任何參數,bind(*args, **kwargs).arguments傳回OrderDict的資料

bind_values = sig.bind(1,2)

bind_values
Out[40]: <BoundArguments (x=1, y=2)>

bind_values.arguments
Out[41]: OrderedDict([('x', 1), ('y', 2)])
           
import inspect
import functools


def decorator(*brgs, **kwbrgs):
    def decorate(fun):
        # 如果處于優化模式, 請禁用類型檢查
        if not __debug__:
            return fun

        #  得到一個可調用的參數簽名資訊對象
        sig = inspect.signature(fun)
        # 用于動态的通過裝飾器傳參對函數注解,傳回類型為形參-注解類型的映射資料類型的OrderDict
        bound_types = sig.bind_partial(*brgs, **kwbrgs)
        # bound_types.argument -> OrderDict 有序字典
        # 調用functools子產品中的wraps()對裝飾器進行優化
        @functools.wraps(fun)
        def wrapper(*args, **kwargs):
            # 建立一個位置和關鍵字參數到實參的映射,OrderDict類型,注意這裡不允許忽略任何參數
            bound_values = sig.bind(*args, **kwargs)
            # 周遊剛剛傳入實參建立的OrderDict映射對象,得到k,v對
            for name,value in bound_values.arguments.items():
                # 如果傳入實參對應的形參能在bound_type中找到
                if name in bound_types.arguments.keys():
                    # 如果傳入實參的類型不符合參數注解的類型
                    if not isinstance(value, bound_types.arguments[name]):
                        # 就會報出異常
                        raise TypeError('Argument {} must be {}'.format(name, bound_types.arguments[name]))
            return fun(*args, **kwargs)
        return wrapper
    return decorate


@decorator(str,z=str)  # 在裝飾器這裡可以通過傳參控制函數的參數類型
def add(x,y,z):
    return x+y+z

add(1,2,3)  # TypeError: Argument x must be <class 'str'>
           

深度了解python複雜裝飾器你還有一些路要走…

以上代碼在python3.7.3環境測試通過,最後函數動态綁定注解并檢查參數類型參考 python cookbook9.7 利用裝飾器對函數裝飾器強制執行類型檢查

繼續閱讀