跳至內容

使用者:Xyy23330121/Python/函數注釋

來自維基學院


如同本學習資料第一次提到注釋時一樣:在編寫程序時,程序員會不可避免地忘記之前寫的內容。為了讓程序員在讀已經忘記的函數代碼時,可以快速了解函數的內容、函數的使用要點、以及修改函數的方法。需要進行注釋。

而本章所述的注釋方法,可以由功能比較強勁的文本編輯器讀取、並在使用函數時實時地給程序員以提示。這讓複雜項目的編程工作變得十分便利。

但如果項目的內容較少,有時是沒有作注釋的必要的。是否採取注釋和如何使用注釋,要視讀者的情況而定。

之後學習 類(class) 的時候,使用的注釋方法和這個是一樣的。所以類的注釋不再單獨列一個章節。

本章一些內容可能較為複雜,而且就算完全不寫注釋、程序也能運行。因此本章為選學。

文檔字符串

[編輯 | 編輯原始碼]
在鼠標移動到 "func" 的位置時,VSCode 自動在鼠標上方顯示了函數的文檔字符串。而且用類似Markdown的方式渲染了。
在鼠標移動到 "func" 的位置時,VSCode 自動在鼠標上方顯示了函數的文檔字符串。而且用類似Markdown的方式渲染了。

可以通過以下方式,寫入和輸出文檔字符串。一些文本編輯器(比如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)

typing.Any

[編輯 | 編輯原始碼]

該類型代表任意類型。它可以說明函數參數可以傳入任意類型,也可以說明函數可以返回任意類型。例如:

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.

文本編輯器會自動在後面添加一個下拉的提示框。以提示之後的內容可以寫appendclearcopy等。

但這對於函數的參數是沒用的,

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] 等方式最多僅支持一個參數。由此,我們可以總結出泛型的一般形式:

  1. 一般容器類型:list[int] set[int]
  2. 映射容器類型: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 語句

[編輯 | 編輯原始碼]
type vector = list[float]

之後,我們就可以使用名稱 vector 進行類型註解。

簡單賦值語句

[編輯 | 編輯原始碼]

type 語句是 Python 3.12 新增的。為了向下兼容,也可以通過簡單賦值語句創建類型別名:

vector = list[float]

有時我們不關注具體的類型是什麼,而只在乎兩個變量屬於同一類型。此時,我們可以用泛型來指代這一類型。

基本方式:typing.TypeVar

[編輯 | 編輯原始碼]

創建泛型的最基本方式,就是用 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]

泛型元組:typing.TypeVarTuple

[編輯 | 編輯原始碼]

當標註所需的泛型比較多,或者泛型的個數不定時,可以用泛型元組。以下面的例子為例:

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並不要求如此。

作為函數參數的泛型:typing.ParamSpec

[編輯 | 編輯原始碼]

這一泛型是專門用於標註函數參數的。比如:

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__屬性

[編輯 | 編輯原始碼]

參數或其它相關內容被用這種方式標記的函數和類型,被稱為泛型函數和泛型類。稱用於標註的泛型為函數或類的類型形參。

泛型函數在使用上和一般的函數是一樣的。但它會額外提供一個__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 文檔:類型形參列表提供了更多種類的額外限制。