typing模塊的使用方法


typing模塊的使用方法

實例引入

我們知道 Python 是一種動態語言,在聲明一個變量時我們不需要顯式地聲明它的類型,例如下面的例子:

a = 2print('1 + a =', 1 + a)

運行結果:

1 + a = 3

這里我們首先聲明了一個變量 a,並將其賦值為了 2,然后將最后的結果打印出來,程序輸出來了正確的結果。但在這個過程中,我們沒有聲明它到底是什么類型。

但如果這時候我們將 a 變成一個字符串類型,結果會是怎樣的呢?改寫如下:

a = '2'
print('1 + a =', 1 + a)

運行結果:

TypeError: unsupported operand type(s) for +: 'int' and 'str'

直接報錯了,錯誤原因是我們進行了字符串類型的變量和數值類型變量的加和,兩種數據類型不同,是無法進行相加的。

如果我們將上面的語句改寫成一個方法定義:

def add(a): return a + 1

這里定義了一個方法,傳入一個參數,然后將其加 1 並返回。

如果這時候如果用下面的方式調用,傳入的參數是一個數值類型:

add(2)

則可以正常輸出結果 3。但如果我們傳入的參數並不是我們期望的類型,比如傳入一個字符類型,那么就會同樣報剛才類似的錯誤。

但又由於 Python 的特性,很多情況下我們並不用去聲明它的類型,因此從方法定義上面來看,我們實際上是不知道一個方法的參數到底應該傳入什么類型的。

這樣其實就造成了很多不方便的地方,在某些情況下一些復雜的方法,如果不借助於一些額外的說明,我們是不知道參數到底是什么類型的。

因此,Python 中的類型注解就顯得比較重要了。

類型注解

在 Python 3.5 中,Python PEP 484 引入了類型注解(type hints),在 Python 3.6 中,PEP 526 又進一步引入了變量注解(Variable Annotations),所以上面的代碼我們改寫成如下寫法:

a: int = 2
print('5 + a =', 5 + a) 

def add(a: int) -> int: 
	return a + 1

具體的語法是可以歸納為兩點:

  • 在聲明變量時,變量的后面可以加一個冒號,后面再寫上變量的類型,如 int、list 等等。
  • 在聲明方法返回值的時候,可以在方法的后面加一個箭頭,后面加上返回值的類型,如 int、list 等等。

PEP 8 中,具體的格式是這樣規定的:

  • 在聲明變量類型時,變量后方緊跟一個冒號,冒號后面跟一個空格,再跟上變量的類型。
  • 在聲明方法返回值的時候,箭頭左邊是方法定義,箭頭右邊是返回值的類型,箭頭左右兩邊都要留有空格。

有了這樣的聲明,以后我們如果看到這個方法的定義,我們就知道傳入的參數類型了,如調用 add 方法的時候,我們就知道傳入的需要是一個數值類型的變量,而不是字符串類型,非常直觀。

但值得注意的是,這種類型和變量注解實際上只是一種類型提示,對運行實際上是沒有影響的,比如調用 add 方法的時候,我們傳入的不是 int 類型,而是一個 float 類型,它也不會報錯,也不會對參數進行類型轉換,如:

add(1.5)

我們傳入的是一個 float 類型的數值 1.5,看下運行結果:

2.5

可以看到,運行結果正常輸出,而且 1.5 並沒有經過強制類型轉換變成 1,否則結果會變成 2。

因此,類型和變量注解只是提供了一種提示,對於運行實際上沒有任何影響。

不過有了類型注解,一些 IDE 是可以識別出來並提示的,比如 PyCharm 就可以識別出來在調用某個方法的時候參數類型不一致,會提示 WARNING。

比如上面的調用,如果在 PyCharm 中,就會有如下提示內容:

Expected type 'int', got 'float' instead
This inspection detects type errors in function call expressions. Due to dynamic dispatch and duck typing, this is possible in a limited but useful number of cases. Types of function parameters can be specified in docstrings or in Python 3 function annotations.

另外也有一些庫是支持類型檢查的,比如 mypy,安裝之后,利用 mypy 即可檢查出 Python 腳本中不符合類型注解的調用情況。

上面只是用一個簡單的 int 類型做了實例,下面我們再看下一些相對復雜的數據結構,例如列表、元組、字典等類型怎么樣來聲明。

可想而知了,列表用 list 表示,元組用 tuple 表示,字典用 dict 來表示,那么很自然地,在聲明的時候我們就很自然地寫成這樣了:

names: list = ['Germey', 'Guido']
version: tuple = (3, 7, 4)
operations: dict = {'show': False, 'sort': True}

這么看上去沒有問題,確實聲明為了對應的類型,但實際上並不能反映整個列表、元組的結構,比如我們只通過類型注解是不知道 names 里面的元素是什么類型的,只知道 names 是一個列表 list 類型,實際上里面都是字符串 str 類型。我們也不知道 version 這個元組的每一個元素是什么類型的,實際上是 int 類型。但這些信息我們都無從得知。因此說,僅僅憑借 list、tuple 這樣的聲明是非常“弱”的,我們需要一種更強的類型聲明。

這時候我們就需要借助於 typing 模塊了,它提供了非常“強“的類型支持,比如 List[str]、Tuple[int, int, int] 則可以表示由 str 類型的元素組成的列表和由 int 類型的元素組成的長度為 3 的元組。所以上文的聲明寫法可以改寫成下面的樣子:

from typing import List, Tuple, Dict 

names: List[str] = ['Germey', 'Guido']
version: Tuple[int, int, int] = (3, 7, 4)
operations: Dict[str, bool] = {'show': False, 'sort': True}

這樣一來,變量的類型便可以非常直觀地體現出來了。

目前 typing 模塊也已經被加入到 Python 標准庫中,不需要安裝第三方模塊,我們就可以直接使用了。

typing

下面我們再來詳細看下 typing 模塊的具體用法,這里主要會介紹一些常用的注解類型,如 List、Tuple、Dict、Sequence 等等,了解了每個類型的具體使用方法,我們可以得心應手的對任何變量進行聲明了。

在引入的時候就直接通過 typing 模塊引入就好了,例如:

from typing import List, Tuple

List

List、列表,是 list 的泛型,基本等同於 list,其后緊跟一個方括號,里面代表了構成這個列表的元素類型,如由數字構成的列表可以聲明為:

var: List[int or float] = [2, 3.5]

另外還可以嵌套聲明都是可以的:

var: List[List[int]] = [[1, 2], [2, 3]]

Tuple、NamedTuple

Tuple、元組,是 tuple 的泛型,其后緊跟一個方括號,方括號中按照順序聲明了構成本元組的元素類型,如 Tuple[X, Y] 代表了構成元組的第一個元素是 X 類型,第二個元素是 Y 類型。

比如想聲明一個元組,分別代表姓名、年齡、身高,三個數據類型分別為 str、int、float,那么可以這么聲明:

person: Tuple[str, int, float] = ('Mike', 22, 1.75)

同樣地也可以使用類型嵌套。

NamedTuple,是 collections.namedtuple 的泛型,實際上就和 namedtuple 用法完全一致,但個人其實並不推薦使用 NamedTuple,推薦使用 attrs 這個庫來聲明一些具有表征意義的類。

Dict、Mapping、MutableMapping

Dict、字典,是 dict 的泛型;Mapping,映射,是 collections.abc.Mapping 的泛型。根據官方文檔,Dict 推薦用於注解返回類型,Mapping 推薦用於注解參數。它們的使用方法都是一樣的,其后跟一個中括號,中括號內分別聲明鍵名、鍵值的類型,如:

def size(rect: Mapping[str, int]) -> Dict[str, int]: 
	return {'width': rect['width'] + 100, 'height': rect['width'] + 100}

這里將 Dict 用作了返回值類型注解,將 Mapping 用作了參數類型注解。

MutableMapping 則是 Mapping 對象的子類,在很多庫中也經常用 MutableMapping 來代替 Mapping。

Set、AbstractSet

Set、集合,是 set 的泛型;AbstractSet、是 collections.abc.Set 的泛型。根據官方文檔,Set 推薦用於注解返回類型,AbstractSet 用於注解參數。它們的使用方法都是一樣的,其后跟一個中括號,里面聲明集合中元素的類型,如:

def describe(s: AbstractSet[int]) -> Set[int]: 
	return set(s)

這里將 Set 用作了返回值類型注解,將 AbstractSet 用作了參數類型注解。

Sequence

Sequence,是 collections.abc.Sequence 的泛型,在某些情況下,我們可能並不需要嚴格區分一個變量或參數到底是列表 list 類型還是元組 tuple 類型,我們可以使用一個更為泛化的類型,叫做 Sequence,其用法類似於 List,如:

def square(elements: Sequence[float]) -> List[float]: 
	return [x ** 2 for x in elements]

NoReturn

NoReturn,當一個方法沒有返回結果時,為了注解它的返回類型,我們可以將其注解為 NoReturn,例如:

def hello() -> NoReturn: 
	print('hello')

Any

Any,是一種特殊的類型,它可以代表所有類型,靜態類型檢查器的所有類型都與 Any 類型兼容,所有的無參數類型注解和返回類型注解的都會默認使用 Any 類型,也就是說,下面兩個方法的聲明是完全等價的:

def add(a): 
	return a + 1 

def add(a: Any) -> Any: 
	return a + 1

原理類似於 object,所有的類型都是 object 的子類。但如果我們將參數聲明為 object 類型,靜態參數類型檢查便會拋出錯誤,而 Any 則不會,具體可以參考官方文檔的說明:https://docs.python.org/zh-cn/3/library/typing.html?highlight=typing#the-any-type。

TypeVar

TypeVar,我們可以借助它來自定義兼容特定類型的變量,比如有的變量聲明為 int、float、None 都是符合要求的,實際就是代表任意的數字或者空內容都可以,其他的類型則不可以,比如列表 list、字典 dict 等等,像這樣的情況,我們可以使用 TypeVar 來表示。

例如一個人的身高,便可以使用 int 或 float 或 None 來表示,但不能用 dict 來表示,所以可以這么聲明:

height = 1.75
Height = TypeVar('Height', int, float, None)

def get_height() -> Height: 
	return height

這里我們使用 TypeVar 聲明了一個 Height 類型,然后將其用於注解方法的返回結果。

NewType

NewType,我們可以借助於它來聲明一些具有特殊含義的類型,例如像 Tuple 的例子一樣,我們需要將它表示為 Person,即一個人的含義,但但從表面上聲明為 Tuple 並不直觀,所以我們可以使用 NewType 為其聲明一個類型,如:

Person = NewType('Person', Tuple[str, int, float])
person = Person(('Mike', 22, 1.75))

這里實際上 person 就是一個 tuple 類型,我們可以對其像 tuple 一樣正常操作。

Callable

Callable,可調用類型,它通常用來注解一個方法,比如我們剛才聲明了一個 add 方法,它就是一個 Callable 類型:

print(Callable, type(add), isinstance(add, Callable))

運行結果:

typing.Callable <class 'function'> True

在這里雖然二者 add 利用 type 方法得到的結果是 function,但實際上利用 isinstance 方法判斷確實是 True。

Callable 在聲明的時候需要使用 Callable[[Arg1Type, Arg2Type, ...], ReturnType] 這樣的類型注解,將參數類型和返回值類型都要注解出來,例如:

def date(year: int, month: int, day: int) -> str: 
	return f'{year}-{month}-{day}' 

def get_date_fn() -> Callable[[int, int, int], str]:
	return date

這里首先聲明了一個方法 date,接收三個 int 參數,返回一個 str 結果,get_date_fn 方法返回了這個方法本身,它的返回值類型就可以標記為 Callable,中括號內分別標記了返回的方法的參數類型和返回值類型。

Union

Union,聯合類型,Union[X, Y] 代表要么是 X 類型,要么是 Y 類型。

Union type; Union[X, Y] is equivalent to X | Y and means either X or Y.

聯合類型的聯合類型等價於展平后的類型:

Union[Union[int, str], float] == Union[int, str, float]

僅有一個參數的聯合類型會坍縮成參數自身,比如:

Union[int] == int

多余的參數會被跳過,比如:

Union[int, str, int] == Union[int, str]

在比較聯合類型的時候,參數順序會被忽略,比如:

Union[int, str] == Union[str, int]

這個在一些方法參數聲明的時候比較有用,比如一個方法,要么傳一個字符串表示的方法名,要么直接把方法傳過來:

def process(fn: Union[str, Callable]): 
	if isinstance(fn, str): # str2fn and process 
		pass 
	elif isinstance(fn, Callable): 
		fn()

這樣的聲明在一些類庫方法定義的時候十分常見。

Optional

Optional,意思是說這個參數可以為空或已經聲明的類型,即 Optional[X] 等價於 Union[X, None]。

但值得注意的是,這個並不等價於可選參數,當它作為參數類型注解的時候,不代表這個參數可以不傳遞了,而是說這個參數可以傳為 None。

如當一個方法執行結果,如果執行完畢就不返回錯誤信息, 如果發生問題就返回錯誤信息,則可以這么聲明:

def judge(result: bool) -> Optional[str]: 
	if result: 
		return 'Error Occurred'

Generator

如果想代表一個生成器類型,可以使用 Generator,它的聲明比較特殊,其后的中括號緊跟着三個參數,分別代表 YieldType、SendType、ReturnType,如:

def echo_round() -> Generator[int, float, str]: 
	sent = yield 0 
	while sent >= 0: 
		sent = yield round(sent) 
	return 'Done'

在這里 yield 關鍵字后面緊跟的變量的類型就是 YieldType,yield 返回的結果的類型就是 SendType,最后生成器 return 的內容就是 ReturnType。

當然很多情況下,生成器往往只需要 yield 內容就夠了,我們是不需要 SendType 和 ReturnType 的,可以將其設置為空,如:

def infinite_stream(start: int) -> Generator[int, None, None]: 
	while True: 
		yield start 
		start += 1

Type aliases

簡單的類型注解及其形式如開篇例子所示,那么除了默認的int、str等簡單類型,就可以通過typing模塊來實現注解。首先,我們可以通過給類型賦予別名,簡化類型注釋,如下例中的Vector和List[float]是等價的。

from typing import List
Vector = List[float]

def scale(scalar: float, vector: Vector) -> Vector:
    return [scalar * num for num in vector]

上面的例子,似乎不能很好的體現類型注釋別名的優勢,官網還給了另外一個例子,非常生動形象:

from typing import Dict, Tuple, Sequence

ConnectionOptions = Dict[str, str]
Address = Tuple[str, int]
Server = Tuple[Address, ConnectionOptions]

def broadcast_message(message: str, servers: Sequence[Server]) -> None:
    pass

def broadcast_message2(
        message: str,
        servers: Sequence[Tuple[Tuple[str, int], Dict[str, str]]]) -> None:
    pass

Generics

由於無法以通用的方式靜態推斷有關保存在容器(list set tuple)中對象的類型信息,因此抽象類被用來拓展表示容器中的元素。如下面子里中,使用基類Employee來擴展其可能得子類如 Sub1_Employee、Sub2_Employee等。但是其局限性明顯,所以我們需要引入泛型(generics)。

from typing import Mapping, Sequence

def notify_by_email(employees: Sequence[Employee],
                    overrides: Mapping[str, str]) -> None:
    pass

可以通過typing中的TypeVar將泛型參數化,如:

from typing import Sequence, TypeVar

T = TypeVar('T')      # Can be anything
A = TypeVar('A', str, bytes)  # Must be str or bytes

def first(l: Sequence[T]) -> T:   # Generic function
    return l[0]
User-defined generic types

可以將用戶字定義的類定義為泛型類:

from typing import TypeVar, Generic
from logging import Logger

T = TypeVar('T')

class LoggedVar(Generic[T]):
    def __init__(self, value: T, name: str, logger: Logger) -> None:
        self.name = name
        self.logger = logger
        self.value = value

    def set(self, new: T) -> None:
        self.log('Set ' + repr(self.value))
        self.value = new

    def get(self) -> T:
        self.log('Get ' + repr(self.value))
        return self.value

    def log(self, message: str) -> None:
        self.logger.info('%s: %s', self.name, message)       

Generic[T] 作為基類定義了類 LoggedVar 采用單個類型參數 T。這也使得 T 作為類體內的一個類型有效。通過Generic基類使用元類(metaclass)定義__getitem__()使得LoggedVar[t]是有效類型:

from typing import Iterable

def zero_all_vars(vars: Iterable[LoggedVar[int]]) -> None:
    for var in vars:
        var.set(0)

泛型類型可以有任意數量的類型變量,並且類型變量可能會受到限制:

from typing import TypeVar, Generic

T = TypeVar('T')
S = TypeVar('S', int, str)

class StrangePair(Generic[T, S]):
    pass

Reference Links

[1] https://docs.python.org/zh-cn/3/library/typing.html


免責聲明!

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



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