使用者:Xyy23330121/Python/函數注釋
如同本學習資料第一次提到注釋時一樣:在編寫程序時,程序員會不可避免地忘記之前寫的內容。為了讓程序員在讀已經忘記的函數代碼時,可以快速了解函數的內容、函數的使用要點、以及修改函數的方法。需要進行注釋。
而本章所述的注釋方法,可以由功能比較強勁的文本編輯器讀取、並在使用函數時實時地給程序員以提示。這讓複雜項目的編程工作變得十分便利。
但如果項目的內容較少,有時是沒有作注釋的必要的。是否採取注釋和如何使用注釋,要視讀者的情況而定。
之後學習 類(class) 的時候,使用的注釋方法和這個是一樣的。所以類的注釋不再單獨列一個章節。
本章一些內容可能較為複雜,而且就算完全不寫注釋、程序也能運行。因此本章為選學。

可以通過以下方式,寫入和輸出文檔字符串。一些文本編輯器(比如VSCode)可以在編輯代碼時簡單地查閱函數文檔。利用文本編輯器的這些功能,通過編寫詳細的文檔,在編寫大型項目時可以少記憶許多內容。
def func():
"""# 什么都不做
本函数仅包含一个 pass 语句。
当使用 if / else / def 等语句时,冒号
的下一行必须是语句组的内容,要进行缩进。
但单独只有缩进也会导致出错。因此,必须
要写入至少一个语句。若希望语句组不执行
任何操作,可以使用 pass 语句来作为语句
组中唯一的语句。
在使用 pass 语句时,应注意能否有更好的
方法。比如 if True: pass; else: do so-
mething的情况。可以直接用 if not True。
使用方法:
>>> func()
"""
pass
print(func.__doc__) #输出文档字符串的内容
輸出為:
# 什么都不做
本函数仅包含一个 pass 语句。
当使用 if / else / def 等语句时,冒号
的下一行必须是语句组的内容,要进行缩进。
但单独只有缩进也会导致出错。因此,必须
要写入至少一个语句。若希望语句组不执行
任何操作,可以使用 pass 语句来作为语句
组中唯一的语句。
在使用 pass 语句时,应注意能否有更好的
方法。比如 if True: pass; else: do so-
mething的情况。可以直接用 if not True。
使用方法:
>>> func()使用文檔字符串時,應當注意其可讀性。
除上面的「文檔字符串」以外,類型註解也是一種良好的注釋方式。
對於參數的類型註解如果有不了解的地方,可以查閱 Python 文檔。如果查閱 Python 文檔之後還不了解,那就直接省略註解也是可以的。
| 類型名 | 含義 |
|---|---|
int |
整數 |
float |
浮點數 |
complex |
複數 |
bool |
布爾值 |
list |
列表 |
tuple |
元組 |
str |
字符串 |
dict |
字典 |
set |
集合 |
frozenset |
不可變集合 |
以下示例展示了對參數的類型進行註解,以及設置默認值的方式。該示例還提示了該函數返回值的類型。
def plus(arg1: int, arg2: int = 0) -> int:
return arg1 + arg2
這代表着:plus 函數的參數 arg1 應傳入一個 int 類型的值;arg2 有默認值 0,如果要傳入,應傳入一個 int 類型的值。plus 函數應返回一個 int 類型的值。
類型註解僅作為標識,對函數運行無影響。如果我們執行:
print(plus(1.1))
輸出為:
1.1儘管類型註解提示了應當給參數傳入整數,但傳入浮點數時,函數依舊正常運行。而函數的返回值也並非類型註解提示的整數。
右側表格簡單總結了目前學過的一些類型的名稱。讀者可以查閱 Python 文檔,了解更多的內置類型。
關於類型名稱,我們將在「類」章節中得到更深刻的了解。
除上述類型之外,還有一些特殊類型。特別的,typing 模塊提供了許多特殊類型。為篇幅起見,這裡只列出兩個最常用的特殊類型:
我們之前提到過,None 是 Python 中代表「無」的量。實際上,它同時也是一個類型。我們可以用 None 來標記函數沒有返回值。例如:
def prtplus(arg1: int, arg2: int = 0) -> None:
print(arg1 + arg2)
該類型代表任意類型。它可以說明函數參數可以傳入任意類型,也可以說明函數可以返回任意類型。例如:
from typing import Any
def prttype(arg: Any) -> None:
print(type(arg))
特別的,對於未進行類型註解的參數或返回值,默認使用 typing.Any 作為其參數類型和返回值類型。
抽象基類 也可以用於類型註解。
比如,collections.abc.Mapping 表示映射類型,任何具有映射功能的類型,比如字典,都會被判斷為 collections.abc.Mapping 的子類型,也就可以用該類型作為標註:
from collections.abc import Mapping
def func(arg: Mapping) -> None: pass
為了讓代碼的類型在進行檢查時儘可能地適用廣泛,可以使用抽象基類。
對於 collections.abc 提到的「抽象方法」等內容,可能要到類的方法與Python的計算章節才會詳細講解。
類型註解可以用於靜態類型檢查器。靜態類型檢查器會檢查 Python 代碼中各元素的類型,並對於其中類型不正確的內容進行提示和報錯。比如:
def func(s: int) -> None:
s[index]
在靜態類型檢查器中會提示報錯,因為整數類型不支持索引操作。
一些功能強大的文本編輯器會按照類型來提供提示,比如,輸入以下內容後:
a = [0, 1] #文本编辑器检查这一行,得知 a 的类型
a.
文本編輯器會自動在後面添加一個下拉的提示框。以提示之後的內容可以寫append、clear、copy等。
但這對於函數的參數是沒用的,
def func(a):
a.
此時,文本編輯器不會提供任何提示。這是因為文本編輯器不能通過賦值表達式知道函數參數的類型。
但如果我們使用:
def func(a:list): #文本编辑器检查这一行,得知 a 的类型
a.
文本編輯器就能夠提供提示了。這在許多情況下很有幫助。
類型註解的內容會被作為函數的屬性存儲起來。我們可以用以下方式輸出所存儲的註解:
print(plus.__annotations__)
註解的輸出為一個字典:
{'arg1': <class 'int'>, 'arg2': <class 'int'>, 'return': <class 'int'>}使用Python交互模式、查文檔又比較麻煩時。可以用這種方式查詢類型註解。
參見《Python指南:註解最佳實踐》:
有時我們希望說明一項參數的類型「不是int就是float」,或者希望說明一項參數是「由int構成的列表」。此時就需要對類型進行某種運算。
可以用|運算符,表示類型是左右兩者之一。
R = int | float #整数或浮点数
C = int | float | complex #整数或浮点数,或复数
也可以用泛型的方法來作標註:
from typing import Union
R = Union[int, float] #整数或浮点数
C = Union[int, float, complex] #整数或浮点数,或复数
序列類型、字典、集合等被稱之為容器類型。許多容器類型都支持下標操作,以表示其內部元素的類型。
我們可以用以下方式標註「由某個類型的元素」組成的列表:
a = list[int] #以多个整数组成的类型。
b = list[int | None] #以多个“整数或None”组成的类型
類似,可以用 set[int] 來表示由多個整數組成的列表等。
而對於映射結構,比如 dict ,則是用 dict[str, int] 表示「用字符串作索引,得到整數」的字典。
對於 Python 中的大多數容器,類型系統會假定容器中的所有元素都是相同類型的。這表現在代碼上,就是 list[ClassName] 等方式最多僅支持一個參數。由此,我們可以總結出泛型的一般形式:
- 一般容器類型:
list[int]set[int]等 - 映射容器類型:
dict[str, int]collections.abc.Mapping[str, int]等
但元組是一個例外。元組中的元素,在許多代碼中並非相同類型,且其位置很關鍵。所以,我們有以下特例:
可以用以下方式標註「由特定類型元素」組成的元組:
a = tuple[int, str] #有二个元素的元组,第一个元素是整数,第二个元素是字符串。
b = tuple[int, ...] #不定长的、由int构成的元组。
被標註了容器內容的容器類型,在使用時和原類型是一致的。
a = list[int]('Python') #等同于 a = list('Python')
print(a, type(a)) #输出:['P', 'y', 't', 'h', 'o', 'n'] <class 'list'>
函數就是可調用的類型。我們可以通過以下方式標註可調用的類型。
from typing import Callable
a = Callable[[int], str] #输入整数参数,返回字符串的可调用类型
b = Callable[[int, int], None] #输入两个整数参数,不返回任何值的可调用类型
特別的,用省略號(三個小數點)可以表示任意參數列表。比如:
from typing import Callable
a = Callable[..., str] #输入任意参数列表,返回字符串的可调用类型
利用 type[ClassName] 的方式,可以要求傳入的參數或返回值的類型是「類」本身或其子類本身。我們在之前的學習中,已經遇到過「類」對象了。比如:
1 #类型为:int
type(1) #类型为:type[int]
int #类型为:type[int]
在使用時,可以用以下方式:
a = type[int] #类 int 或其子类
b = type[object] #类 object 或其子类,即任意类型
有些類型,尤其是複合類型寫起來比較麻煩。此時,我們可以用以下幾種方式創建類型別名:
type vector = list[float]
之後,我們就可以使用名稱 vector 進行類型註解。
type 語句是 Python 3.12 新增的。為了向下兼容,也可以通過簡單賦值語句創建類型別名:
vector = list[float]
有時我們不關注具體的類型是什麼,而只在乎兩個變量屬於同一類型。此時,我們可以用泛型來指代這一類型。
創建泛型的最基本方式,就是用 typing.TypeVar。
from typing import TypeVar
T = TypeVar("T")
c = list[T] #以多个“待定类型T的实例”组成的类型。
此時,如果有一個函數被標註為:
def func(arg: c):
...
那麼向它傳入
arg = [0, 1, 2] #以整数作为待定类型T
arg = ["0", "1", "2"] #以字符串作为待定类型T
都不會被類型檢查器報錯。
我們可以對泛型進行一些額外的限制。
S = TypeVar("S", bound=str)
d = list[S] #以多个“待定类型S的实例”组成的类型。要求S是str的子类。
N = TypeVar("N", int, float, complex)
e = list[N] #以多个“待定类型N的实例”组成的类型。N或者是int,或者是float,或者是complex。
#等同于 e = list[int] | list[float] | list[complex]
當標註所需的泛型比較多,或者泛型的個數不定時,可以用泛型元組。以下面的例子為例:
from typing import TypeVar, TypeVarTuple
T = TypeVar("T")
Ts = TypeVarTuple("Ts")
def move_first_element_to_last(tup: tuple[T, *Ts]) -> tuple[*Ts, T]:
return (*tup[1:], tup[0])
在剛才的例子中,我們並不在乎參數元組的長度和元組中元素的具體類型。我們只希望備註「返回值中各元素類型」和「參數中各元素類型」的對應關係。此時可以使用泛型元組。
泛型元組不能直接用於標記某個對象是元組類型,而是必須被解包後使用:
x: Ts # 不可用
x: tuple[Ts] # 不可用
x: tuple[*Ts] # 正确的做法
x: Callable[[*Ts], T] # 正确的做法
同一級別只能存在一個被解包的泛型元組,比如:
Ts = TypeVarTuple("Ts")
Ts2= TypeVarTuple("Ts2")
x: tuple[*Ts, *Ts2] # Ts 和 Ts2 在同一级被解包,不可用
x: tuple[*Ts, tuple[*Ts2]] # 可用
泛型元組可以用於標記函數的打包參數,比如:
T = TypeVar("T")
Ts = TypeVarTuple("Ts")
def func1(*args: T) -> None:
pass
def func2(*args: *Ts) -> None:
pass
func1在類型標記上,希望被打包的參數屬於同一類型。而func2並不要求如此。
這一泛型是專門用於標註函數參數的。比如:
from typing import Callable, ParamSpec
T = TypeVar("T")
P = ParamSpec('P')
def print_when_run(f: Callable[P, T]) -> Callable[P, T]:
def decorated_f(*args: P.args, **kwargs: P.kwargs) -> T:
print("Function called.")
return f(*args, **kwargs)
return decorated_f
在上面的例子中,我們實現了一個裝飾器,並對裝飾器所裝飾的函數進行了類型標註。
與泛型元組不同,此時還支持 對關鍵字參數的打包。
上面列出的基本方式,主要是為了方便讀者理解。在實際的泛型使用中,一般會使用更為簡便的寫法。
def func[T](arg: T) -> T:
...
這可被認為是等同於
def TYPE_PARAMS_OF_func():
T = typing.TypeVar("T") #泛型“T”在局部作用域里面被自动设置
def func(arg: T) -> T: ...
func.__type_params__ = (T,)
return func
func = TYPE_PARAMS_OF_func()
del TYPE_PARAMS_OF_func
類似的,有
class Bag[T]: ...
# 等同于:
def TYPE_PARAMS_OF_Bag():
T = typing.TypeVar("T")
class Bag(typing.Generic[T]):
__type_params__ = (T,)
...
return Bag
Bag = TYPE_PARAMS_OF_Bag()
del TYPE_PARAMS_OF_Bag
參數或其它相關內容被用這種方式標記的函數和類型,被稱為泛型函數和泛型類。稱用於標註的泛型為函數或類的類型形參。
泛型函數在使用上和一般的函數是一樣的。但它會額外提供一個__type_params__屬性用於存儲所有使用的泛型。
def func[T](arg:T) -> T:
return arg
print(func.__type_params__) #输出:(T,)
__type_params__是一個包含了函數所使用的所有泛型的元組。上面的函數隻使用了T作為泛型,所以元組中只包含T。對於未通過上面方法設置泛型的一般函數而言,__type_params__是空元組。
在使用簡便寫法時,標註泛型元組的方法為:
def func[*Ts](*args:*Ts) -> tuple[*Ts]:
return args
print(func.__type_params__) #输出:(Ts,)
標註專用於參數的泛型時,則是用:
def func[**Params](f:Callable[Params, None]) -> Callable[Params, None]:
return f
print(func.__type_params__) #输出:(Params,)
def func[A: int, B: (str, bytes)](a: A, b: B):
pass
這可被認為是等同於
def TYPE_PARAMS_OF_func():
A = typing.TypeVar("A", bound=int)
B = typing.TypeVar("B", str, bytes)
def func(a: A, b: B): ...
func.__type_params__ = (A, B)
return func
func = TYPE_PARAMS_OF_func()
del TYPE_PARAMS_OF_func
實際上,Python 文檔:類型形參列表提供了更多種類的額外限制。