关于后端:Python-Type-Hints-从入门到实践

3次阅读

共计 9540 个字符,预计需要花费 24 分钟才能阅读完成。

Python 想必大家都曾经很相熟了,甚至对于它有用或者无用的论点大家可能也曾经看腻了。然而无论如何,它作为一个将退出高考科目的语言还是有它独到之处的,明天咱们就再开展聊聊 Python。

Python 是一门动静强类型语言

《晦涩的 Python》一书中提到,如果一门语言很少隐式转换类型,阐明它是强类型语言,例如 Java、C++ 和 Python 就是强类型语言。

同时如果一门语言常常隐式转换类型,阐明它是弱类型语言,PHP、JavaScript 和 Perl 是弱类型语言。

当然下面这种简略的示例比照,并不能确切的说 Python 是一门强类型语言,因为 Java 同样反对 integer 和 string 相加操作,且 Java 是强类型语言。因而《晦涩的 Python》一书中还有对于动态类型和动静类型的定义:在编译时查看类型的语言是动态类型语言,在运行时查看类型的语言是动静类型语言。动态语言须要申明类型(有些古代语言应用类型推导防止局部类型申明)。

综上所述,对于 Python 是动静强类型语言是比拟不言而喻没什么争议的。

Type Hints 初探

Python 在 PEP 484(Python Enhancement Proposals,Python 加强建议书)[https://www.python.org/dev/pe…] 中提出了 Type Hints(类型注解)。进一步强化了 Python 是一门强类型语言的个性,它在 Python3.5 中第一次被引入。应用 Type Hints 能够让咱们编写出带有类型的 Python 代码,看起来更加合乎强类型语言格调。

这里定义了两个 greeting 函数:

  • 一般的写法如下:
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 会依据函数的类型标注,对传递给函数的参数进行类型查看。如果发现实参类型与函数的形参类型标注不符就会有如下提醒:

常见数据结构的 Type Hints 写法

下面通过一个 greeting 函数展现了 Type Hints 的用法,接下来咱们就 Python 常见数据结构的 Type Hints 写法进行更加深刻的学习。

默认参数

Python 函数反对默认参数,以下是默认参数的 Type Hints 写法,只须要将类型写到变量和默认参数之间即可。

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

greeting()

自定义类型

对于自定义类型,Type Hints 同样可能很好的反对。它的写法跟 Python 内置类型并无区别。

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))

当类型标注为自定义类型时,IDE 也可能对类型进行查看。

容器类型

当咱们要给内置容器类型增加类型标注时,因为类型注解运算符 [] 在 Python 中代表切片操作,因而会引发语法错误。所以不能间接应用内置容器类型当作注解,须要从 typing 模块中导入对应的容器类型注解(通常为内置类型的首字母大写模式)。

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/pe…] 的呈现解决了这个问题,咱们能够间接应用 Python 的内置类型,而不会呈现语法错误。

l: list[int] = [1, 2, 3]

t: tuple[str, ...] = ("a", "b")

d: dict[str, int] = {
    "a": 1,
    "b": 2,
}

类型别名

有些简单的嵌套类型写起来很长,如果呈现反复,就会很苦楚,代码也会不够整洁。

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)

这样代码看起来就难受多了。

可变参数

Python 函数一个非常灵活的中央就是反对可变参数,Type Hints 同样反对可变参数的类型标注。

def foo(*args: str, **kwargs: int) -> None:
    ...

foo("a", "b", 1, x=2, y="c")

IDE 仍可能查看进去。

泛型

应用动静语言少不了泛型的反对,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 函数只想接管 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 区别

TypeVar 不只能够接管泛型,它也能够像 Union 一样应用,只须要在实例化时将想要指定的类型范畴当作参数顺次传进来来即可。跟 Union 不同的是,应用 TypeVar 申明的函数,多参数类型必须雷同,而 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] 的简写模式,示意被标注的参数要么为 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 是一种非凡的类型,能够代表所有类型。未指定返回值与参数类型的函数,都隐式地默认应用 Any,所以以下两个 greeting 函数写法等价:

from typing import Any

def greeting(name):
    return "Hello" + name


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

当咱们既想应用 Type Hints 来实现动态类型的写法,也不想失去动静语言特有的灵活性时,即可应用 Any。

Any 类型值赋给更准确的类型时,不执行类型查看,如下代码 IDE 并不会有谬误提醒:

from typing import Any

a: Any = None
a = []  # 动静语言个性
a = 2

s: str = ''
s = a  # Any 类型值赋给更准确的类型 

可调用对象(函数、类等)

Python 中的任何可调用类型都能够应用 Callable 进行标注。如下代码标注中 Callable[[int], str],[int] 示意可调用类型的参数列表,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)

自援用

当咱们须要定义树型构造时,往往须要自援用。当执行到 init 办法时 Tree 类型还没有生成,所以不能像应用 str 这种内置类型一样间接进行标注,须要采纳字符串模式“Tree”来对未生成的对象进行援用。

class Tree(object):
    def __init__(self, left: "Tree" = None, right: "Tree" = None):
        self.left = left
        self.right = right

tree1 = Tree(Tree(), Tree())

IDE 同样可能对自援用类型进行查看。

此模式不仅可能用于自援用,前置援用同样实用。

鸭子类型

Python 一个显著的特点是其对鸭子类型的大量利用,Type Hints 提供了 Protocol 来对鸭子类型进行反对。定义类时只须要继承 Protocol 就能够申明一个接口类型,当遇到接口类型的注解时,只有接管到的对象实现了接口类型的所有办法,即可通过类型注解的查看,IDE 便不会报错。这里的 Stream 无需显式继承 Interface 类,只须要实现了 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)

因为内置的 open 函数返回的文件对象和 Stream 对象都实现了 close 办法,所以可能通过 Type Hints 的查看,而字符串“s”并没有实现 close 办法,所以 IDE 会提醒类型谬误。

Type Hints 的其余写法

实际上 Type Hints 不只有一种写法,Python 为了兼容不同人的爱好和老代码的迁徙还实现了另外两种写法。

应用正文编写

来看一个 tornado 框架的例子(tornado/web.py)。实用于在已有的我的项目上做批改,代码曾经写好了,前期须要减少类型标注。

应用独自文件编写(.pyi)

能够在源代码雷同的目录下新建一个与 .py 同名的 .pyi 文件,IDE 同样可能主动做类型查看。这么做的长处是能够对原来的代码不做任何改变,齐全解耦。毛病是相当于要同时保护两份代码。

Type Hints 实际

基本上,日常编码中罕用的 Type Hints 写法都曾经介绍给大家了,上面就让咱们一起来看看如何在理论编码中中利用 Type Hints。

dataclass——数据类

dataclass 是一个装璜器,它能够对类进行装璜,用于给类增加魔法办法,例如 __init__() 和 __repr__() 等,它在 PEP 557[https://www.python.org/dev/pe…] 中被定义。

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 并不会对字段类型进行查看。

能够发现,应用 dataclass 来编写类能够缩小很多反复的样板代码,语法上也更加清晰。

Pydantic

Pydantic 是一个基于 Python Type Hints 的第三方库,它提供了数据验证、序列化和文档的性能,是一个十分值得学习借鉴的库。以下是一段应用 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 进行数据验证的示例,当 User 类接管到的参数不合乎预期时,会抛出 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 标识了异样字段和报错地位,msg 为报错提示信息,type 则为报错类型,这样整个报错起因高深莫测。

MySQLHandler

MySQLHandler[https://github.com/jianghushi…] 是我对 pymysql 库的封装,使其反对应用 with 语法调用 execute 办法,并且将查问后果从 tuple 替换成 object,同样也是对 Type Hints 的利用。

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 的类型查看并不能决定代码执行期间是否报错,仅能在动态期做到语法查看提醒的性能。

要想实现在代码执行阶段强制对类型进行查看,则须要咱们通过本人编写代码或引入第三方库的模式(如下面介绍的 Pydantic)。上面我通过一个 type_check 函数实现了运行期动静查看类型,来供你参考:

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 函数打上 type_check 装璜器,即可实现运行期类型查看。

附录

如果你想持续深刻学习应用 Python Type Hints,以下是一些我举荐的开源我的项目供你参考:

  • Pydantic [https://github.com/samuelcolv…]
  • FastAPI [https://github.com/tiangolo/f…]
  • Tornado [https://github.com/tornadoweb…]
  • Flask [https://github.com/pallets/flask]
  • Chia-pool [https://github.com/Chia-Netwo…]
  • MySQLHandler [https://github.com/jianghushi…]

举荐浏览

TypeScript 枚举指南

实战经验分享:应用 PyO3 来构建你的 Python 模块

正文完
 0