Python?Type?Hints?學(xué)習(xí)之從入門到實踐
Python 想必大家都已經(jīng)很熟悉了,甚至關(guān)于它有用或者無用的論點大家可能也已經(jīng)看膩了。但是無論如何,它作為一個廣受關(guān)注的語言還是有它獨到之處的,今天我們就再展開聊聊 Python。
Python 是一門動態(tài)強類型語言
《流暢的 Python》一書中提到,如果一門語言很少隱式轉(zhuǎn)換類型,說明它是強類型語言,例如 Java、C++ 和 Python 就是強類型語言。
Python 的強類型體現(xiàn)
同時如果一門語言經(jīng)常隱式轉(zhuǎn)換類型,說明它是弱類型語言,PHP、JavaScript 和 Perl 是弱類型語言。
動態(tài)弱類型語言:JavaScript
當(dāng)然上面這種簡單的示例對比,并不能確切的說 Python 是一門強類型語言,因為 Java 同樣支持 integer 和 string 相加操作,且 Java 是強類型語言。因此《流暢的 Python》一書中還有關(guān)于靜態(tài)類型和動態(tài)類型的定義:在編譯時檢查類型的語言是靜態(tài)類型語言,在運行時檢查類型的語言是動態(tài)類型語言。靜態(tài)語言需要聲明類型(有些現(xiàn)代語言使用類型推導(dǎo)避免部分類型聲明)。
綜上所述,關(guān)于 Python 是動態(tài)強類型語言是比較顯而易見沒什么爭議的。
Type Hints 初探
Python 在 PEP 484(Python Enhancement Proposals,Python 增強建議書)[https://www.python.org/dev/peps/pep-0484/]中提出了 Type Hints(類型注解)。進一步強化了 Python 是一門強類型語言的特性,它在 Python3.5 中第一次被引入。使用 Type Hints 可以讓我們編寫出帶有類型的 Python 代碼,看起來更加符合強類型語言風(fēng)格。
這里定義了兩個 greeting 函數(shù):
普通的寫法如下:
name = "world" def greeting(name): return "Hello " + name greeting(name)
加入了 Type Hints 的寫法如下:
name: str = "world" def greeting(name: str) -> str: return "Hello " + name greeting(name)
以 PyCharm 為例,在編寫代碼的過程中 IDE 會根據(jù)函數(shù)的類型標(biāo)注,對傳遞給函數(shù)的參數(shù)進行類型檢查。如果發(fā)現(xiàn)實參類型與函數(shù)的形參類型標(biāo)注不符就會有如下提示:
常見數(shù)據(jù)結(jié)構(gòu)的 Type Hints 寫法
上面通過一個 greeting 函數(shù)展示了 Type Hints 的用法,接下來我們就 Python 常見數(shù)據(jù)結(jié)構(gòu)的 Type Hints 寫法進行更加深入的學(xué)習(xí)。
默認(rèn)參數(shù)
Python 函數(shù)支持默認(rèn)參數(shù),以下是默認(rèn)參數(shù)的 Type Hints 寫法,只需要將類型寫到變量和默認(rèn)參數(shù)之間即可。
def greeting(name: str = "world") -> str: return "Hello " + name greeting()
自定義類型
對于自定義類型,Type Hints 同樣能夠很好的支持。它的寫法跟 Python 內(nèi)置類型并無區(qū)別。
class Student(object): def __init__(self, name, age): self.name = name self.age = age def student_to_string(s: Student) -> str: return f"student name: {s.name}, age: {s.age}." student_to_string(Student("Tim", 18))
當(dāng)類型標(biāo)注為自定義類型時,IDE 也能夠?qū)︻愋瓦M行檢查。
容器類型
當(dāng)我們要給內(nèi)置容器類型添加類型標(biāo)注時,由于類型注解運算符 [] 在 Python 中代表切片操作,因此會引發(fā)語法錯誤。所以不能直接使用內(nèi)置容器類型當(dāng)作注解,需要從 typing 模塊中導(dǎo)入對應(yīng)的容器類型注解(通常為內(nèi)置類型的首字母大寫形式)。
from typing import List, Tuple, Dict l: List[int] = [1, 2, 3] t: Tuple[str, ...] = ("a", "b") d: Dict[str, int] = { "a": 1, "b": 2, }
不過 PEP 585[https://www.python.org/dev/peps/pep-0585/]的出現(xiàn)解決了這個問題,我們可以直接使用 Python 的內(nèi)置類型,而不會出現(xiàn)語法錯誤。
l: list[int] = [1, 2, 3] t: tuple[str, ...] = ("a", "b") d: dict[str, int] = { "a": 1, "b": 2, }
類型別名
有些復(fù)雜的嵌套類型寫起來很長,如果出現(xiàn)重復(fù),就會很痛苦,代碼也會不夠整潔。
config: list[tuple[str, int], dict[str, str]] = [ ("127.0.0.1", 8080), { "MYSQL_DB": "db", "MYSQL_USER": "user", "MYSQL_PASS": "pass", "MYSQL_HOST": "127.0.0.1", "MYSQL_PORT": "3306", }, ] def start_server(config: list[tuple[str, int], dict[str, str]]) -> None: ... start_server(config)
此時可以通過給類型起別名的方式來解決,類似變量命名。
Config = list[tuple[str, int], dict[str, str]] config: Config = [ ("127.0.0.1", 8080), { "MYSQL_DB": "db", "MYSQL_USER": "user", "MYSQL_PASS": "pass", "MYSQL_HOST": "127.0.0.1", "MYSQL_PORT": "3306", }, ] def start_server(config: Config) -> None: ... start_server(config)
這樣代碼看起來就舒服多了。
可變參數(shù)
Python 函數(shù)一個非常靈活的地方就是支持可變參數(shù),Type Hints 同樣支持可變參數(shù)的類型標(biāo)注。
def foo(*args: str, **kwargs: int) -> None: ... foo("a", "b", 1, x=2, y="c")
IDE 仍能夠檢查出來。
泛型
使用動態(tài)語言少不了泛型的支持,Type Hints 針對泛型也提供了多種解決方案。
TypeVar
使用 TypeVar 可以接收任意類型。
from typing import TypeVar T = TypeVar("T") def foo(*args: T, **kwargs: T) -> None: ... foo("a", "b", 1, x=2, y="c")
Union
如果不想使用泛型,只想使用幾種指定的類型,那么可以使用 Union 來做。比如定義 concat 函數(shù)只想接收 str 或 bytes 類型。
from typing import Union T = Union[str, bytes] def concat(s1: T, s2: T) -> T: return s1 + s2 concat("hello", "world") concat(b"hello", b"world") concat("hello", b"world") concat(b"hello", "world")
IDE 的檢查提示如下圖:
TypeVar 和 Union 區(qū)別
TypeVar 不只可以接收泛型,它也可以像 Union 一樣使用,只需要在實例化時將想要指定的類型范圍當(dāng)作參數(shù)依次傳進來來即可。跟 Union 不同的是,使用 TypeVar 聲明的函數(shù),多參數(shù)類型必須相同,而 Union 不做限制。
from typing import TypeVar T = TypeVar("T", str, bytes) def concat(s1: T, s2: T) -> T: return s1 + s2 concat("hello", "world") concat(b"hello", b"world") concat("hello", b"world")
以下是使用 TypeVar 做限定類型時的 IDE 提示:
Optional
Type Hints 提供了 Optional 來作為 Union[X, None] 的簡寫形式,表示被標(biāo)注的參數(shù)要么為 X 類型,要么為 None,Optional[X] 等價于 Union[X, None]。
from typing import Optional, Union # None => type(None) def foo(arg: Union[int, None] = None) -> None: ... def foo(arg: Optional[int] = None) -> None: ...
Any
Any 是一種特殊的類型,可以代表所有類型。未指定返回值與參數(shù)類型的函數(shù),都隱式地默認(rèn)使用 Any,所以以下兩個 greeting 函數(shù)寫法等價:
from typing import Optional, Union # None => type(None) def foo(arg: Union[int, None] = None) -> None: ... def foo(arg: Optional[int] = None) -> None: ...
當(dāng)我們既想使用 Type Hints 來實現(xiàn)靜態(tài)類型的寫法,也不想失去動態(tài)語言特有的靈活性時,即可使用 Any。
Any 類型值賦給更精確的類型時,不執(zhí)行類型檢查,如下代碼 IDE 并不會有錯誤提示:
from typing import Any a: Any = None a = [] # 動態(tài)語言特性 a = 2 s: str = '' s = a # Any 類型值賦給更精確的類型
可調(diào)用對象(函數(shù)、類等)
Python 中的任何可調(diào)用類型都可以使用 Callable 進行標(biāo)注。如下代碼標(biāo)注中 Callable[[int], str],[int] 表示可調(diào)用類型的參數(shù)列表,str 表示返回值。
from typing import Callable def int_to_str(i: int) -> str: return str(i) def f(fn: Callable[[int], str], i: int) -> str: return fn(i) f(int_to_str, 2)
自引用
當(dāng)我們需要定義樹型結(jié)構(gòu)時,往往需要自引用。當(dāng)執(zhí)行到 __init__ 方法時 Tree 類型還沒有生成,所以不能像使用 str 這種內(nèi)置類型一樣直接進行標(biāo)注,需要采用字符串形式“Tree”來對未生成的對象進行引用。
class Tree(object): def __init__(self, left: "Tree" = None, right: "Tree" = None): self.left = left self.right = right tree1 = Tree(Tree(), Tree())
IDE 同樣能夠?qū)ψ砸妙愋瓦M行檢查。
此形式不僅能夠用于自引用,前置引用同樣適用。
鴨子類型
Python 一個顯著的特點是其對鴨子類型的大量應(yīng)用,Type Hints 提供了 Protocol 來對鴨子類型進行支持。定義類時只需要繼承 Protocol 就可以聲明一個接口類型,當(dāng)遇到接口類型的注解時,只要接收到的對象實現(xiàn)了接口類型的所有方法,即可通過類型注解的檢查,IDE 便不會報錯。這里的 Stream 無需顯式繼承 Interface 類,只需要實現(xiàn)了 close 方法即可。
from typing import Protocol class Interface(Protocol): def close(self) -> None: ... # class Stream(Interface): class Stream: def close(self) -> None: ... def close_resource(r: Interface) -> None: r.close() f = open("a.txt") close_resource(f) s: Stream = Stream() close_resource(s)
由于內(nèi)置的 open 函數(shù)返回的文件對象和 Stream 對象都實現(xiàn)了 close 方法,所以能夠通過 Type Hints 的檢查,而字符串“s”并沒有實現(xiàn) close 方法,所以 IDE 會提示類型錯誤。
Type Hints 的其他寫法
實際上 Type Hints 不只有一種寫法,Python 為了兼容不同人的喜好和老代碼的遷移還實現(xiàn)了另外兩種寫法。
使用注釋編寫
來看一個 tornado 框架的例子(tornado/web.py)。適用于在已有的項目上做修改,代碼已經(jīng)寫好了,后期需要增加類型標(biāo)注。
使用單獨文件編寫(.pyi)
可以在源代碼相同的目錄下新建一個與 .py 同名的 .pyi 文件,IDE 同樣能夠自動做類型檢查。這么做的優(yōu)點是可以對原來的代碼不做任何改動,完全解耦。缺點是相當(dāng)于要同時維護兩份代碼。
Type Hints 實踐
基本上,日常編碼中常用的 Type Hints 寫法都已經(jīng)介紹給大家了,下面就讓我們一起來看看如何在實際編碼中中應(yīng)用 Type Hints。
dataclass——數(shù)據(jù)類
dataclass 是一個裝飾器,它可以對類進行裝飾,用于給類添加魔法方法,例如 __init__() 和 __repr__() 等,它在 PEP 557中被定義。
from dataclasses import dataclass, field @dataclass class User(object): id: int name: str friends: list[int] = field(default_factory=list) data = { "id": 123, "name": "Tim", } user = User(**data) print(user.id, user.name, user.friends) # > 123 Tim []
以上使用 dataclass 編寫的代碼同如下代碼等價:
class User(object): def __init__(self, id: int, name: str, friends=None): self.id = id self.name = name self.friends = friends or [] data = { "id": 123, "name": "Tim", } user = User(**data) print(user.id, user.name, user.friends) # > 123 Tim []
注意:dataclass 并不會對字段類型進行檢查。
可以發(fā)現(xiàn),使用 dataclass 來編寫類可以減少很多重復(fù)的樣板代碼,語法上也更加清晰。
Pydantic
Pydantic 是一個基于 Python Type Hints 的第三方庫,它提供了數(shù)據(jù)驗證、序列化和文檔的功能,是一個非常值得學(xué)習(xí)借鑒的庫。以下是一段使用 Pydantic 的示例代碼:
from datetime import datetime from typing import Optional from pydantic import BaseModel class User(BaseModel): id: int name = 'John Doe' signup_ts: Optional[datetime] = None friends: list[int] = [] external_data = { 'id': '123', 'signup_ts': '2021-09-02 17:00', 'friends': [1, 2, '3'], } user = User(**external_data) print(user.id) # > 123 print(repr(user.signup_ts)) # > datetime.datetime(2021, 9, 2, 17, 0) print(user.friends) # > [1, 2, 3] print(user.dict()) """ { 'id': 123, 'signup_ts': datetime.datetime(2021, 9, 2, 17, 0), 'friends': [1, 2, 3], 'name': 'John Doe', } """
注意:Pydantic 會對字段類型進行強制檢查。
Pydantic 寫法上跟 dataclass 非常類似,但它做了更多的額外工作,還提供了如 .dict() 這樣非常方便的方法。
再來看一個 Pydantic 進行數(shù)據(jù)驗證的示例,當(dāng) User 類接收到的參數(shù)不符合預(yù)期時,會拋出 ValidationError 異常,異常對象提供了 .json() 方法方便查看異常原因。
from pydantic import ValidationError try: User(signup_ts='broken', friends=[1, 2, 'not number']) except ValidationError as e: print(e.json()) """ [ { "loc": [ "id" ], "msg": "field required", "type": "value_error.missing" }, { "loc": [ "signup_ts" ], "msg": "invalid datetime format", "type": "value_error.datetime" }, { "loc": [ "friends", 2 ], "msg": "value is not a valid integer", "type": "type_error.integer" } ] """
所有報錯信息都保存在一個 list 中,每個字段的報錯又保存在嵌套的 dict 中,其中 loc 標(biāo)識了異常字段和報錯位置,msg 為報錯提示信息,type 則為報錯類型,這樣整個報錯原因一目了然。
MySQLHandler
MySQLHandler[https://github.com/jianghushinian/python-scripts/blob/main/scripts/mysql_handler_type_hints.py]是我對 pymysql 庫的封裝,使其支持使用 with 語法調(diào)用 execute 方法,并且將查詢結(jié)果從 tuple 替換成 object,同樣也是對 Type Hints 的應(yīng)用。
class MySQLHandler(object): """MySQL handler""" def __init__(self): self.conn = pymysql.connect( host=DB_HOST, port=DB_PORT, user=DB_USER, password=DB_PASS, database=DB_NAME, charset=DB_CHARSET, client_flag=CLIENT.MULTI_STATEMENTS, # execute multi sql statements ) self.cursor = self.conn.cursor() def __del__(self): self.cursor.close() self.conn.close() @contextmanager def execute(self): try: yield self.cursor.execute self.conn.commit() except Exception as e: self.conn.rollback() logging.exception(e) @contextmanager def executemany(self): try: yield self.cursor.executemany self.conn.commit() except Exception as e: self.conn.rollback() logging.exception(e) def _tuple_to_object(self, data: List[tuple]) -> List[FetchObject]: obj_list = [] attrs = [desc[0] for desc in self.cursor.description] for i in data: obj = FetchObject() for attr, value in zip(attrs, i): setattr(obj, attr, value) obj_list.append(obj) return obj_list def fetchone(self) -> Optional[FetchObject]: result = self.cursor.fetchone() return self._tuple_to_object([result])[0] if result else None def fetchmany(self, size: Optional[int] = None) -> Optional[List[FetchObject]]: result = self.cursor.fetchmany(size) return self._tuple_to_object(result) if result else None def fetchall(self) -> Optional[List[FetchObject]]: result = self.cursor.fetchall() return self._tuple_to_object(result) if result else None
運行期類型檢查
Type Hints 之所以叫 Hints 而不是 Check,就是因為它只是一個類型的提示而非真正的檢查。上面演示的 Type Hints 用法,實際上都是 IDE 在幫我們完成類型檢查的功能,但實際上,IDE 的類型檢查并不能決定代碼執(zhí)行期間是否報錯,僅能在靜態(tài)期做到語法檢查提示的功能。
要想實現(xiàn)在代碼執(zhí)行階段強制對類型進行檢查,則需要我們通過自己編寫代碼或引入第三方庫的形式(如上面介紹的 Pydantic)。下面我通過一個 type_check 函數(shù)實現(xiàn)了運行期動態(tài)檢查類型,來供你參考:
from inspect import getfullargspec from functools import wraps from typing import get_type_hints def type_check(fn): @wraps(fn) def wrapper(*args, **kwargs): fn_args = getfullargspec(fn)[0] kwargs.update(dict(zip(fn_args, args))) hints = get_type_hints(fn) hints.pop("return", None) for name, type_ in hints.items(): if not isinstance(kwargs[name], type_): raise TypeError(f"expected {type_.__name__}, got {type(kwargs[name]).__name__} instead") return fn(**kwargs) return wrapper # name: str = "world" name: int = 2 @type_check def greeting(name: str) -> str: return str(name) print(greeting(name)) # > TypeError: expected str, got int instead
只要給 greeting 函數(shù)打上 type_check 裝飾器,即可實現(xiàn)運行期類型檢查。
附錄
如果你想繼續(xù)深入學(xué)習(xí)使用 Python Type Hints,以下是一些我推薦的開源項目供你參考:
Pydantic?
FastAPI?
Tornado?
Flask?
Chia-pool?
MySQLHandler?
以上就是Python Type Hints 學(xué)習(xí)之從入門到實踐的詳細(xì)內(nèi)容,更多關(guān)于Python Type Hints的資料請關(guān)注本站其它相關(guān)文章!
版權(quán)聲明:本站文章來源標(biāo)注為YINGSOO的內(nèi)容版權(quán)均為本站所有,歡迎引用、轉(zhuǎn)載,請保持原文完整并注明來源及原文鏈接。禁止復(fù)制或仿造本網(wǎng)站,禁止在非www.sddonglingsh.com所屬的服務(wù)器上建立鏡像,否則將依法追究法律責(zé)任。本站部分內(nèi)容來源于網(wǎng)友推薦、互聯(lián)網(wǎng)收集整理而來,僅供學(xué)習(xí)參考,不代表本站立場,如有內(nèi)容涉嫌侵權(quán),請聯(lián)系alex-e#qq.com處理。