14 - 函數參數檢測-inspect模塊


1 python類型注解

        類型注解,即對變量的類型,進行標注或者說明,因為Python是一門動態編譯型語言,我們無法在賦值時就定義它的變量類型,所以在Python3.5以上版本新增了類型注解,但僅僅是提示作用,並不能嚴格控制,這是動態編譯型語言的通病,下面來仔細看一下什么是Python的類型注解。

2 函數定義的弊端

        Python是動態語言,變量隨時可以被賦值,且賦值為不同的類型,這就與靜態語言不同了,變量的類型是在運行期決定的,而靜態語言事先就已經定義好了變量的類型了。這是動態語言方便之處,但也是一種弊端,我們無法控制變量的類型,也就無法控制異常的產生。舉個栗子

def add(x,y):
    return x + y 
print(add(1,2))
print(add('s','b'))
print(add(1,'a'))

        當用戶傳入兩個數字時,返回它們的和,但是如果我們傳遞其他變量呢?比如字符串,因為Python中實現了+號的類型重載,所以說兩個字符串的確可以加,但是如果是數字和字符串呢?在Python這種強類型語言中來說,屬於非法操作(javascript會隱式轉換),而這時,我們就需要對用戶傳入的數據進行類型判斷,不符合本函數的需求,那么就拋個異常,或者提示等等操作,這樣就不會引起后續代碼在執行期崩潰。如何解決呢?其實主要有兩種方式。

  • 函數文檔
  • 函數注解

3 函數文檔

        在函數中插入說明性文檔的方式成為函數文檔。

def add(x, y):
    """
    This function used to add something
    :param x: int object
    :param y: int object
    :return: int object
    """
    return x + y

在函數中,一般是定義語句后的首行使用三對雙引號表示。通常存儲在函數的__doc__屬性中。當用戶使用help(函數)時,會被打印在屏幕上。

In [68]: def add(x, y):
    ...:     """
    ...:     This function used to add something
    ...:     :param x: int object
    ...:     :param y: int object
    ...:     :return: int object
    ...:     """
    ...:     return x + y
    ...:

In [69]: help(add)
Help on function add in module __main__:

add(x, y)
    This function used to add something
    :param x: int object
    :param y: int object
    :return: int object


In [70]: print(add.__doc__)

    This function used to add something
    :param x: int object
    :param y: int object
    :return: int object

In [71]:

每次都要使用help來查看函數的說明,的確可以讓使用者了解函數的參數以及返回值的類型,但並不是所有人都願意寫doc的,在這個所謂的敏捷開發時代,人們大多會以敏捷開發為借口沒時間寫,所以這種方法不是很用。

4 函數注解

        Python的函數注解是什么呢?首先來看一下如下代碼:

def add(x: int, y: int) -> int:
    return x + y
  • 函數的位置形參,和默認值形參后使用冒號分隔,后面用於標識變量期望的類型。
  • 在def語句末尾,使用->符號后 指定用於標識函數執行后的返回值類型。

完成以上定義后,,主要的差別如下圖:
zhushi1

當我們在IDE中准備傳入非注釋類型變量時,IDE會幫我們進行顏色提示,用於表示這里傳入的變量有點問題。在編寫時我們尚且可以使用這種方式,對我們產生一點'警示',但是當我們寫的函數被其他人調用的時候,那么就無法進行'提示'了,這個時候,我們就需要對傳入的參數進行類型檢查了。

我們來總結一下:

  • 函數注解在Python3.5中引入
  • 對函數的參數、返回值進行類型注解
  • 只對函數的參數做一個輔助的說明,並不對函數參數進行類型檢查
  • 提供給第三方工具,做代碼分析,發現隱藏的BUG
  • 函數注解的信息,保存在函數的__annotation__屬性中。

python3.6 以上還添加了變量的注解:i:int = 10,當然也只是提示的作用。

4.1 annotation屬性

在Python中使用__開頭的表示符一般被用特殊屬性,__annotation__存儲的就是函數的簽名信息

In [71]: def add(x: int, y: int) -> int:
    ...:     return x + y
    ...:

In [73]: add.__annotations__
Out[73]: {'x': int, 'y': int, 'return': int}

        當我們使用變量注釋時,變量名和類型就會存放在函數的__annotations__屬性中。那么即然有變量存儲,那么我們是不是只需要獲取傳入的參數,然后和annotations中存儲的變量類型進行比較是不是就達到目的了呢?仔細思考一下:

  1. 參數檢查勢必要在函數執行前,想要在add執行前添加參數判斷那么就需要使用裝飾器了
  2. __annotations__的值是一個字典,字典是無序的,用戶按照位置傳進來參數是有序的,如何讓它們形成對應關系方便我們檢測呢?

下面我們來了解一下inspect模塊,它可以幫我們完成這個事情。

5 inspect模塊

        官方解釋如下:inspect模塊提供了幾個有用的函數來幫助獲取關於活動對象的信息,例如模塊、類、方法、函數、回溯、框架對象和代碼對象。例如,它可以幫助您檢查類的內容、檢索方法的源代碼、提取並格式化函數的參數列表,或者獲取顯示詳細回溯所需的所有信息。

5.1 常用方法

分類 方法名稱 功能
判斷 inspect.getmodulename(path) 獲取模塊名稱
inspect.ismodule(object) 是不是個模塊
inspect.isclass(object) 是不是個類
inspect.ismethod(object) 是不是一個方法
inspect.isfunction(object) 是不是一個函數
inspect.isgeneratorfunction(object) 是不是一個生成器函數
inspect.isgenerator(object) 是不是一個生成器
inspect.iscoroutinefunction(object) 是不是一個協程函數
獲取信息 inspect.getmodulename(path) 獲取模塊名稱
inspect.getsource(object) 獲取對象的原碼(並不會解析裝飾器原碼)

5.2 signature類

        首先我們要說的是函數的簽名信息:它包含了了函數的函數名、它的參數類型,它所在的類和名稱空間及其他信息,簽名對象(signature object)表示可調用對象的調用簽名信息和它的注解信息,當我們使用signature()時,它會重新返回一個包含可調用對象信息的簽名對象。

5.3 parameters屬性

        signature類的parameters屬性,它里面存放的是函數的參數注解和返回值注解,組成的有序字典,其中參數注解的格式為:參數名稱,使用inspect.Parameters類包裝的參數注解,這個參數注解很強大,它包含如下常用的方法:

方法名稱 含義
empty 等同於inspect._empty表示一個參數沒有被類型注釋
name 參數的名稱
default 參數的默認值,如果一個參數沒有默認值,這個屬性的值為inspect.empty
annotation 參數的注解類型,如果參數沒有定義注解,這個屬性的值為inspect.empty
kind 參數的類型

這里的參數類型表示的是inspect內置參數類型(其實就是幾個常用的函數參數定義類型而已,只是換個名字而已)

_POSITIONAL_ONLY         = _ParameterKind.POSITIONAL_ONLY       # 位置參數_only
_POSITIONAL_OR_KEYWORD   = _ParameterKind.POSITIONAL_OR_KEYWORD # 位置或關鍵字參數
_VAR_POSITIONAL          = _ParameterKind.VAR_POSITIONAL        # 可變位置參數
_KEYWORD_ONLY            = _ParameterKind.KEYWORD_ONLY          # keyword-only參數
_VAR_KEYWORD             = _ParameterKind.VAR_KEYWORD           # 可變關鍵字參數

其中POSITIONAL_ONLY,Python中沒有被實現。

5.4 獲取對象的參數簽名

根據上面講的方法,我們可以通過如下方式,簡單的獲取參數的簽名:

In [11]: import inspect
    ...:
    ...: def add(x: int, y: int) -> int:
    ...:     return x + y
    ...:
    ...: sig = inspect.signature(add)
    ...: params = sig.parameters
    ...: print(params)
OrderedDict([('x', <Parameter "x:int">), ('y', <Parameter "y:int">)])

In [21]: params['x'].annotation
Out[21]: int    # 如果沒有定義x的參數注解,那么這里就是inspect._empty

通過它的屬性,搭配有序字典這個特性,有沒有很興奮?參數有序,傳入的實參有序,還能獲取參數注解的類型,那么就可以開工進行參數檢查了!

6 檢查參數

以上面函數為例子,當給add函數傳入的x,y時進行參數檢查,如果x,y不是int類型,那么返回異常,並退出函數

import inspect
import functools

def check(fn):
    @functools.wraps(fn)   # 等於 wrapper.__annotation__ = fn.__annotation__ 還有其他的屬性比如__doc__,__module__等
    def wrapper(*args, **kwargs):
        sig = inspect.signature(fn)   # 獲取add函數簽名信息
        params = sig.parameters     # 獲取add函數的參數信息
        values = list(params.values())   # 由於params是個有序字典,那么values也是有序的,只需根據索一一對應判斷即可
        for i, k in enumerate(args):   # 遍歷用戶傳入的位置參數
            if values[i].annotation != inspect._empty:   # 如果定義了參數注解,則開始檢查
                if not isinstance(k, values[i].annotation):   # 如果檢查不通過,曝出異常
                    raise('Key Error')
        for k,v in kwargs.items():
            if params[k].annotation != inspect._empty:
                if not isinstance(v,params[k].annotation):
                    raise('Key Error')
        return fn(*args, **kwargs)
    return wrapper

@check
def add(x: int, y: int) -> int:
    return x + y
    
add(4,y=5)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM