<kbd id="afajh"><form id="afajh"></form></kbd>
<strong id="afajh"><dl id="afajh"></dl></strong>
    <del id="afajh"><form id="afajh"></form></del>
        1. <th id="afajh"><progress id="afajh"></progress></th>
          <b id="afajh"><abbr id="afajh"></abbr></b>
          <th id="afajh"><progress id="afajh"></progress></th>

          Python Type Hints 從入門(mén)到實(shí)踐

          共 11972字,需瀏覽 24分鐘

           ·

          2021-10-26 20:52

          Python 想必大家都已經(jīng)很熟悉了,甚至關(guān)于它有用或者無(wú)用的論點(diǎn)大家可能也已經(jīng)看膩了。但是無(wú)論如何,它作為一個(gè)將加入高考科目的語(yǔ)言還是有它獨(dú)到之處的,今天我們就再展開(kāi)聊聊 Python。



          Python 是一門(mén)動(dòng)態(tài)強(qiáng)類型語(yǔ)言


          《流暢的 Python》一書(shū)中提到,如果一門(mén)語(yǔ)言很少隱式轉(zhuǎn)換類型,說(shuō)明它是強(qiáng)類型語(yǔ)言,例如 Java、C++ 和 Python 就是強(qiáng)類型語(yǔ)言。

          延伸閱讀:Python到底是強(qiáng)類型語(yǔ)言,還是弱類型語(yǔ)言?

          △ ?Python 的強(qiáng)類型體現(xiàn)

          同時(shí)如果一門(mén)語(yǔ)言經(jīng)常隱式轉(zhuǎn)換類型,說(shuō)明它是弱類型語(yǔ)言,PHP、JavaScript 和 Perl 是弱類型語(yǔ)言。

          △ ?動(dòng)態(tài)弱類型語(yǔ)言:JavaScript

          當(dāng)然上面這種簡(jiǎn)單的示例對(duì)比,并不能確切的說(shuō) Python 是一門(mén)強(qiáng)類型語(yǔ)言,因?yàn)?Java 同樣支持 integer 和 string 相加操作,且 Java 是強(qiáng)類型語(yǔ)言。因此《流暢的 Python》一書(shū)中還有關(guān)于靜態(tài)類型和動(dòng)態(tài)類型的定義:在編譯時(shí)檢查類型的語(yǔ)言是靜態(tài)類型語(yǔ)言,在運(yùn)行時(shí)檢查類型的語(yǔ)言是動(dòng)態(tài)類型語(yǔ)言。靜態(tài)語(yǔ)言需要聲明類型(有些現(xiàn)代語(yǔ)言使用類型推導(dǎo)避免部分類型聲明)。

          綜上所述,關(guān)于 Python 是動(dòng)態(tài)強(qiáng)類型語(yǔ)言是比較顯而易見(jiàn)沒(méi)什么爭(zhēng)議的。



          Type Hints 初探


          Python 在 PEP 484(Python Enhancement Proposals,Python 增強(qiáng)建議書(shū))[https://www.python.org/dev/peps/pep-0484/]中提出了 Type Hints(類型注解)。進(jìn)一步強(qiáng)化了 Python 是一門(mén)強(qiáng)類型語(yǔ)言的特性,它在 Python3.5 中第一次被引入。使用 Type Hints 可以讓我們編寫(xiě)出帶有類型的 Python 代碼,看起來(lái)更加符合強(qiáng)類型語(yǔ)言風(fēng)格。

          這里定義了兩個(gè) greeting 函數(shù):

          • 普通的寫(xiě)法如下:

          name = "world"
          def greeting(name): return "Hello " + name
          greeting(name)
          • 加入了 Type Hints 的寫(xiě)法如下:

          name: str = "world"
          def greeting(name: str) -> str: return "Hello " + name
          greeting(name)

          以 PyCharm 為例,在編寫(xiě)代碼的過(guò)程中 IDE 會(huì)根據(jù)函數(shù)的類型標(biāo)注,對(duì)傳遞給函數(shù)的參數(shù)進(jìn)行類型檢查。如果發(fā)現(xiàn)實(shí)參類型與函數(shù)的形參類型標(biāo)注不符就會(huì)有如下提示:



          常見(jiàn)數(shù)據(jù)結(jié)構(gòu)的 Type Hints 寫(xiě)法


          上面通過(guò)一個(gè) greeting 函數(shù)展示了 Type Hints 的用法,接下來(lái)我們就 Python 常見(jiàn)數(shù)據(jù)結(jié)構(gòu)的 Type Hints 寫(xiě)法進(jìn)行更加深入的學(xué)習(xí)。

          默認(rèn)參數(shù)

          Python 函數(shù)支持默認(rèn)參數(shù),以下是默認(rèn)參數(shù)的 Type Hints 寫(xiě)法,只需要將類型寫(xiě)到變量和默認(rèn)參數(shù)之間即可。

          def greeting(name: str = "world") -> str:    return "Hello " + name
          greeting()

          自定義類型

          對(duì)于自定義類型,Type Hints 同樣能夠很好的支持。它的寫(xiě)法跟 Python 內(nèi)置類型并無(wú)區(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)注為自定義類型時(shí),IDE 也能夠?qū)︻愋瓦M(jìn)行檢查。

          容器類型

          當(dāng)我們要給內(nèi)置容器類型添加類型標(biāo)注時(shí),由于類型注解運(yùn)算符 [] 在 Python 中代表切片操作,因此會(huì)引發(fā)語(yǔ)法錯(cuò)誤。所以不能直接使用內(nèi)置容器類型當(dāng)作注解,需要從 typing 模塊中導(dǎo)入對(duì)應(yīng)的容器類型注解(通常為內(nèi)置類型的首字母大寫(xiě)形式)。

          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,}

          不過(guò) PEP 585[https://www.python.org/dev/peps/pep-0585/]的出現(xiàn)解決了這個(gè)問(wèn)題,我們可以直接使用 Python 的內(nèi)置類型,而不會(huì)出現(xiàn)語(yǔ)法錯(cuò)誤。

          l: list[int] = [1, 2, 3]
          t: tuple[str, ...] = ("a", "b")
          d: dict[str, int] = { "a": 1, "b": 2,}

          類型別名

          有些復(fù)雜的嵌套類型寫(xiě)起來(lái)很長(zhǎng),如果出現(xiàn)重復(fù),就會(huì)很痛苦,代碼也會(huì)不夠整潔。

          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)

          此時(shí)可以通過(guò)給類型起別名的方式來(lái)解決,類似變量命名。

          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)

          這樣代碼看起來(lái)就舒服多了。

          可變參數(shù)

          Python 函數(shù)一個(gè)非常靈活的地方就是支持可變參數(shù),Type Hints 同樣支持可變參數(shù)的類型標(biāo)注。

          def foo(*args: str, **kwargs: int) -> None:    ...
          foo("a", "b", 1, x=2, y="c")

          IDE 仍能夠檢查出來(lái)。

          泛型

          使用動(dòng)態(tài)語(yǔ)言少不了泛型的支持,Type Hints 針對(duì)泛型也提供了多種解決方案。

          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 來(lái)做。比如定義 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 一樣使用,只需要在實(shí)例化時(shí)將想要指定的類型范圍當(dāng)作參數(shù)依次傳進(jìn)來(lái)來(lái)即可。跟 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 做限定類型時(shí)的 IDE 提示:

          Optional

          Type Hints 提供了 Optional 來(lái)作為 Union[X, None] 的簡(jiǎn)寫(xiě)形式,表示被標(biāo)注的參數(shù)要么為 X 類型,要么為 None,Optional[X] 等價(jià)于 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,所以以下兩個(gè) greeting 函數(shù)寫(xiě)法等價(jià):

          from typing import Any
          def greeting(name): return "Hello " + name

          def greeting(name: Any) -> Any: return "Hello " + name

          當(dāng)我們既想使用 Type Hints 來(lái)實(shí)現(xiàn)靜態(tài)類型的寫(xiě)法,也不想失去動(dòng)態(tài)語(yǔ)言特有的靈活性時(shí),即可使用 Any。

          Any 類型值賦給更精確的類型時(shí),不執(zhí)行類型檢查,如下代碼 IDE 并不會(huì)有錯(cuò)誤提示:

          from typing import Any
          a: Any = Nonea = [] # 動(dòng)態(tài)語(yǔ)言特性a = 2
          s: str = ''s = a # Any 類型值賦給更精確的類型

          可調(diào)用對(duì)象(函數(shù)、類等)

          Python 中的任何可調(diào)用類型都可以使用 Callable 進(jìn)行標(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)我們需要定義樹(shù)型結(jié)構(gòu)時(shí),往往需要自引用。當(dāng)執(zhí)行到 __init__ 方法時(shí) Tree 類型還沒(méi)有生成,所以不能像使用 str 這種內(nèi)置類型一樣直接進(jìn)行標(biāo)注,需要采用字符串形式“Tree”來(lái)對(duì)未生成的對(duì)象進(jìn)行引用。

          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(jìn)行檢查。

          此形式不僅能夠用于自引用,前置引用同樣適用。

          鴨子類型

          Python 一個(gè)顯著的特點(diǎn)是其對(duì)鴨子類型的大量應(yīng)用,Type Hints 提供了 Protocol 來(lái)對(duì)鴨子類型進(jìn)行支持。定義類時(shí)只需要繼承 Protocol 就可以聲明一個(gè)接口類型,當(dāng)遇到接口類型的注解時(shí),只要接收到的對(duì)象實(shí)現(xiàn)了接口類型的所有方法,即可通過(guò)類型注解的檢查,IDE 便不會(huì)報(bào)錯(cuò)。這里的 Stream 無(wú)需顯式繼承 Interface 類,只需要實(shí)現(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ù)返回的文件對(duì)象和 Stream 對(duì)象都實(shí)現(xiàn)了 close 方法,所以能夠通過(guò) Type Hints 的檢查,而字符串“s”并沒(méi)有實(shí)現(xiàn) close 方法,所以 IDE 會(huì)提示類型錯(cuò)誤。



          Type Hints 的其他寫(xiě)法


          實(shí)際上 Type Hints 不只有一種寫(xiě)法,Python 為了兼容不同人的喜好和老代碼的遷移還實(shí)現(xiàn)了另外兩種寫(xiě)法。

          使用注釋編寫(xiě)

          來(lái)看一個(gè) tornado 框架的例子(tornado/web.py)。適用于在已有的項(xiàng)目上做修改,代碼已經(jīng)寫(xiě)好了,后期需要增加類型標(biāo)注。

          使用單獨(dú)文件編寫(xiě)(.pyi)

          可以在源代碼相同的目錄下新建一個(gè)與 .py 同名的 .pyi 文件,IDE 同樣能夠自動(dòng)做類型檢查。這么做的優(yōu)點(diǎn)是可以對(duì)原來(lái)的代碼不做任何改動(dòng),完全解耦。缺點(diǎn)是相當(dāng)于要同時(shí)維護(hù)兩份代碼。



          Type Hints 實(shí)踐


          基本上,日常編碼中常用的 Type Hints 寫(xiě)法都已經(jīng)介紹給大家了,下面就讓我們一起來(lái)看看如何在實(shí)際編碼中中應(yīng)用 Type Hints。

          dataclass——數(shù)據(jù)類

          dataclass 是一個(gè)裝飾器,它可以對(duì)類進(jìn)行裝飾,用于給類添加魔法方法,例如 __init__() 和 __repr__() 等,它在 PEP 557[https://www.python.org/dev/peps/pep-0557/]中被定義。

          from dataclasses import dataclass, field

          @dataclassclass 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 編寫(xiě)的代碼同如下代碼等價(jià):

          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 并不會(huì)對(duì)字段類型進(jìn)行檢查。

          可以發(fā)現(xiàn),使用 dataclass 來(lái)編寫(xiě)類可以減少很多重復(fù)的樣板代碼,語(yǔ)法上也更加清晰。

          Pydantic

          Pydantic 是一個(gè)基于 Python Type Hints 的第三方庫(kù),它提供了數(shù)據(jù)驗(yàn)證、序列化和文檔的功能,是一個(gè)非常值得學(xué)習(xí)借鑒的庫(kù)。以下是一段使用 Pydantic 的示例代碼:

          from datetime import datetimefrom 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)# > 123print(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 會(huì)對(duì)字段類型進(jìn)行強(qiáng)制檢查。

          Pydantic 寫(xiě)法上跟 dataclass 非常類似,但它做了更多的額外工作,還提供了如 .dict() 這樣非常方便的方法。

          再來(lái)看一個(gè) Pydantic 進(jìn)行數(shù)據(jù)驗(yàn)證的示例,當(dāng) User 類接收到的參數(shù)不符合預(yù)期時(shí),會(huì)拋出 ValidationError 異常,異常對(duì)象提供了 .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" }]"""

          所有報(bào)錯(cuò)信息都保存在一個(gè) list 中,每個(gè)字段的報(bào)錯(cuò)又保存在嵌套的 dict 中,其中 loc 標(biāo)識(shí)了異常字段和報(bào)錯(cuò)位置,msg 為報(bào)錯(cuò)提示信息,type 則為報(bào)錯(cuò)類型,這樣整個(gè)報(bào)錯(cuò)原因一目了然。

          MySQLHandler

          MySQLHandler[https://github.com/jianghushinian/python-scripts/blob/main/scripts/mysql_handler_type_hints.py]是我對(duì) pymysql 庫(kù)的封裝,使其支持使用 with 語(yǔ)法調(diào)用 execute 方法,并且將查詢結(jié)果從 tuple 替換成 object,同樣也是對(duì) 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

          運(yùn)行期類型檢查

          Type Hints 之所以叫 Hints 而不是 Check,就是因?yàn)樗皇且粋€(gè)類型的提示而非真正的檢查。上面演示的 Type Hints 用法,實(shí)際上都是 IDE 在幫我們完成類型檢查的功能,但實(shí)際上,IDE 的類型檢查并不能決定代碼執(zhí)行期間是否報(bào)錯(cuò),僅能在靜態(tài)期做到語(yǔ)法檢查提示的功能。

          要想實(shí)現(xiàn)在代碼執(zhí)行階段強(qiáng)制對(duì)類型進(jìn)行檢查,則需要我們通過(guò)自己編寫(xiě)代碼或引入第三方庫(kù)的形式(如上面介紹的 Pydantic)。下面我通過(guò)一個(gè) type_check 函數(shù)實(shí)現(xiàn)了運(yùn)行期動(dòng)態(tài)檢查類型,來(lái)供你參考:

          from inspect import getfullargspecfrom functools import wrapsfrom 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_checkdef greeting(name: str) -> str: return str(name)
          print(greeting(name))# > TypeError: expected str, got int instead

          只要給 greeting 函數(shù)打上 type_check 裝飾器,即可實(shí)現(xiàn)運(yùn)行期類型檢查。


          附錄

          如果你想繼續(xù)深入學(xué)習(xí)使用 Python Type Hints,以下是一些我推薦的開(kāi)源項(xiàng)目供你參考:

          • Pydantic [https://github.com/samuelcolvin/pydantic]

          • FastAPI [https://github.com/tiangolo/fastapi]

          • Tornado [https://github.com/tornadoweb/tornado]

          • Flask [https://github.com/pallets/flask]

          • Chia-pool [https://github.com/Chia-Network/pool-reference]

          • MySQLHandler [https://github.com/jianghushinian/python-scripts/blob/main/scripts/mysql_handler_type_hints.py]

          Python貓技術(shù)交流群開(kāi)放啦!群里既有國(guó)內(nèi)一二線大廠在職員工,也有國(guó)內(nèi)外高校在讀學(xué)生,既有十多年碼齡的編程老鳥(niǎo),也有中小學(xué)剛剛?cè)腴T(mén)的新人,學(xué)習(xí)氛圍良好!想入群的同學(xué),請(qǐng)?jiān)诠?hào)內(nèi)回復(fù)『交流群』,獲取貓哥的微信(謝絕廣告黨,非誠(chéng)勿擾!)~


          還不過(guò)癮?試試它們




          Python 神器 Celery 源碼解析(2)

          Django3 使用 WebSocket 實(shí)現(xiàn) WebShell

          一個(gè)小破網(wǎng)站,居然比 Python 官網(wǎng)還牛逼

          Python 數(shù)值中的下劃線是怎么回事?

          Python向左,數(shù)學(xué)向右:烏拉姆的素?cái)?shù)研究

          Python到底是強(qiáng)類型語(yǔ)言,還是弱類型語(yǔ)言?


          如果你覺(jué)得本文有幫助
          請(qǐng)慷慨分享點(diǎn)贊,感謝啦
          瀏覽 50
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          評(píng)論
          圖片
          表情
          推薦
          點(diǎn)贊
          評(píng)論
          收藏
          分享

          手機(jī)掃一掃分享

          分享
          舉報(bào)
          <kbd id="afajh"><form id="afajh"></form></kbd>
          <strong id="afajh"><dl id="afajh"></dl></strong>
            <del id="afajh"><form id="afajh"></form></del>
                1. <th id="afajh"><progress id="afajh"></progress></th>
                  <b id="afajh"><abbr id="afajh"></abbr></b>
                  <th id="afajh"><progress id="afajh"></progress></th>
                  日本激情视频 | 情趣在线91蜜桃 | 国产免费操逼视频 | 日韩无码高清视频 | 牛牛高清无码在线观看视频 |