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語句末尾,使用->符號后 指定用於標識函數執行后的返回值類型。
完成以上定義后,,主要的差別如下圖:
當我們在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中存儲的變量類型進行比較是不是就達到目的了呢?仔細思考一下:
- 參數檢查勢必要在函數執行前,想要在add執行前添加參數判斷那么就需要使用裝飾器了
- __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)