Python Type Hints 从入门到实践

本文涉及的产品
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云数据库 RDS MySQL,集群系列 2核4GB
推荐场景:
搭建个人博客
云数据库 RDS PostgreSQL,集群系列 2核4GB
简介: Python Type Hints 从入门到实践

Python 想必大家都已经很熟悉了,甚至关于它有用或者无用的论点大家可能也已经看腻了。但是无论如何,它作为一个将加入高考科目的语言还是有它独到之处的。今天我们就再展开聊聊 Python。

Python 是一门动态强类型语言

《流畅的Python》一书中提到,如果一门语言很少隐式转换类型,说明它是强类型语言;Java、C++ 和 Python 是强类型语言。

△ Python 的强类型体现

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

△ 动态弱类型语言 :JavaScript

当然上面这种简单的示例对比,并不能确切的说 Python 是一门 强类型 语言,因为 Java 同样支持 integer 和 string 相加操作,且 Java 是 强类型 语言。

因此《流畅的Python》一书中还有关于静态类型和动态类型的定义:在编译时检查类型的语言是静态类型语言,在运行时检查类型的语言是动态类型语言。静态语言需要声明类型(有些现代语言使用类型推导避免部分类型声明)。

△ Python 的动态类型体现

综上所述,关于 Python 是动态强类型语言是比较显而易见没什么争议的。

Type Hints 初探

Python 在 PEP 484(Python Enhancement Proposals,Python 增强建议书)中提出了 Type Hints(类型注解)。进一步强化了 Python 是一门 强类型语言的特性,它在 Python3.5 中第一次被引入。使用 Type Hints 可以让我们编写出带有类型的 Python 代码,看起来更加符合强类型语言风格。

这里定义了两个 greeting 函数,一个是普通的写法,另一个是加入了 Type Hints 的写法

1
2
3
4
5
6
name = "world"
defgreeting(name):
return"Hello " + name
greeting(name)
1
2
3
4
5
6
name: str = "world"
defgreeting(name: str) -> str:
return"Hello " + name
greeting(name)

以 PyCharm 为例,在编写代码的过程中,IDE 会根据函数的类型标注,对传递给函数的参数进行类型检查。如果发现实参类型与函数的形参类型标注不符,就会有提示:

△ PyCharm 类型提示

常见数据结构的 Type Hints 写法

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

默认参数

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

1
2
3
4
defgreeting(name: str = "world") -> str:
return"Hello " + name
greeting()

自定义类型

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

1
2
3
4
5
6
7
8
9
10
classStudent(object):
def__init__(self, name, age):
        self.name = name
        self.age = age
defstudent_to_string(s: Student) -> str:
returnf"student name: {s.name}, age: {s.age}."
student_to_string(Student("Tim", 18))

当类型标注为自定义类型时,IDE 同样能够对类型进行检查。

△ 自定义类型

容器类型

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

1
2
3
4
5
6
7
8
9
10
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 的出现解决了这个问题,我们可以直接使用 Python 的内置类型,而不会出现语法错误。

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

类型别名

有些复杂的嵌套类型写起来很长,当出现重复,就会很痛苦,代码也不够整洁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
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",
    },
]
defstart_server(config: list[tuple[str, int], dict[str, str]]) -> None:
    ...
start_server(config)

此时可以通过给类型起别名的方式来解决,类似变量命名。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
Config = list[tuple[str, int], dict[str, str]]
config: Config = [
    ("127.0.0.1", 8080),
    {
"MYSQL_DB": "zaiwu",
"MYSQL_USER": "root",
"MYSQL_PASS": "pass",
"MYSQL_HOST": "127.0.0.1",
"MYSQL_PORT": "3306",
    },
]
defstart_server(config: Config) -> None:
    ...
start_server(config)

这样代码看起来就舒服多了。

可变参数

Python 函数一个非常灵活的地方就是支持可变参数,Type Hints 同样支持可变参数的类型标注。

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

IDE 仍能够检查出来。

△ 可变参数

泛型

使用动态语言少不了泛型的支持,Type Hints 针对泛型也提供了多种解决方案。

TypeVar

使用 TypeVar 可以接收任意类型。

1
2
3
4
5
6
7
8
from typing import TypeVar
T = TypeVar("T")
deffoo(*args: T, **kwargs: T) -> None:
    ...
foo("a", "b", 1, x=2, y="c")
Union

如果不想使用泛型,只想使用几种指定的类型,那么可以使用 Union 来做。比如定义 concat 函数只想接收 strbytes 类型。

1
2
3
4
5
6
7
8
9
10
11
from typing import Union
T = Union[str, bytes]
defconcat(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 的检查提示。

△ Union

TypeVar 和 Union 区别

TypeVar 不只可以接收泛型,它也可以像 Union 一样使用,只需要在实例化时将想要指定的类型范围当作参数依次传进来来即可。跟 Union 不同的是,使用 TypeVar 声明的函数,多参数类型必须相同,Union 不做限制。

1
2
3
4
5
6
7
8
9
10
from typing import TypeVar
T = TypeVar("T", str, bytes)
defconcat(s1: T, s2: T) -> T:
return s1 + s2
concat("hello", "world")
concat(b"hello", b"world")
concat("hello", b"world")

以下是使用 TypeVar 做限定类型时的 IDE 提示。

△ TypeVar

Optional

Type Hints 提供了 Optional 来作为 Union[X, None] 的简写形式,表示被标注的参数要么为 X 类型,要么为 NoneOptional[X] 等价于 Union[X, None]

1
2
3
4
5
6
7
8
9
from typing import Optional, Union
# None => type(None)
deffoo(arg: Union[int, None] = None) -> None:
    ...
deffoo(arg: Optional[int] = None) -> None:
    ...
Any

Any 是一种特殊的类型,可以代表所有类型,未指定返回值与参数类型的函数,都隐式地默认使用 Any,所以以下两个 greeting 函数写法等价。

1
2
3
4
5
6
7
8
from typing import Any
defgreeting(name):
return"Hello " + name
defgreeting(name: Any) -> Any:
return"Hello " + name

当我们既想使用 Type Hints 来实现静态类型的写法,也不想失去动态语言特有的灵活性时,即可使用 AnyAny 类型值赋给更精确的类型时,不执行类型检查,如下代码 IDE 并不会有错误提示。

1
2
3
4
5
6
7
8
from typing import Any
a: Any = None
a = []  # 动态语言特性
a = 2
s: str = ''
s = a  # Any 类型值赋给更精确的类型

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

Python 中的任何可调用类型都可以使用 Callable 进行标注。如下代码标注中 Callable[[int], str][int] 表示可调用类型的参数列表,str 表示返回值。

1
2
3
4
5
6
7
8
9
from typing import Callable
defint_to_str(i: int) -> str:
return str(i)
deff(fn: Callable[[int], str], i: int) -> str:
return fn(i)
f(int_to_str, 2)

自引用

当我们需要定义树型结构时,往往需要自引用。当执行到 __init__ 方法时 Tree 类型还没有生成,所以不能像使用 str 这种内置类型一样直接进行标注,需要采用字符串形式 Tree 来对未生成的对象进行引用。

1
2
3
4
5
6
classTree(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 方法即可。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
from typing import Protocol
classInterface(Protocol):
defclose(self) -> None:
        ...
# class Stream(Interface):
classStream:
defclose(self) -> None:
        ...
defclose_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)。适用于在已有的项目上做修改,代码已经写好了,后期需要增加类型标注。

△ tornado

使用单独文件编写(.pyi)

可以在源代码相同的目录下新建一个与 .py 同名的 .pyi 文件,IDE 同样能够自动做类型检查。

这么做的优点是可以对原来的代码不做任何改动,完全解耦。缺点是相当于要同时维护两份代码。

△ pyi

Type Hints 实践

基本上,日常编码中常用的 Type Hints 写法都已经介绍给大家了,下面就让我们一起来看看如何在实际编码中中应用 Type Hints。

dataclass —— 数据类

dataclass 是一个装饰器,它可以对类进行装饰,用于给类添加魔法方法,例如 __init__()__repr__() 等,它在 PEP 557 中被定义。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
from dataclasses import dataclass, field
@dataclass
classUser(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 编写的代码同如下代码等价。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
classUser(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 的示例代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
from datetime import datetime
from typing import Optional
from pydantic import BaseModel
classUser(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() 方法方便查看异常原因。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
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 是我对 pymysql 库的封装,使其支持使用 with 语法调用 execute 方法,并且将查询结果从 tuple 替换成 object,同样也是对 Type Hints 的应用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
classMySQLHandler(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
defexecute(self):
try:
yield self.cursor.execute
            self.conn.commit()
except Exception as e:
            self.conn.rollback()
            logging.exception(e)
    @contextmanager
defexecutemany(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
deffetchone(self) -> Optional[FetchObject]:
        result = self.cursor.fetchone()
return self._tuple_to_object([result])[0] if result elseNone
deffetchmany(self, size: Optional[int] = None) -> Optional[List[FetchObject]]:
        result = self.cursor.fetchmany(size)
return self._tuple_to_object(result) if result elseNone
deffetchall(self) -> Optional[List[FetchObject]]:
        result = self.cursor.fetchall()
return self._tuple_to_object(result) if result elseNone

运行期类型检查

Type Hints 之所以叫 Hints 而不是 Check,就是因为它只是一个类型的提示而非真正的检查。上面演示的 Type Hints 用法,实际上都是 IDE 在帮我们完成类型检查的功能,但实际上,IDE 的类型检查并不能决定代码执行期间是否报错,仅能在静态期做到语法检查提示的功能。

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
from inspect import getfullargspec
from functools import wraps
from typing import get_type_hints
deftype_check(fn):
    @wraps(fn)
defwrapper(*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():
ifnot 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
defgreeting(name: str) -> str:
return str(name)
print(greeting(name))
# > TypeError: expected str, got int instead

只要给 greeting 函数打上 type_check 装饰器,即可实现运行期类型检查。

附录

如果你想继续深入学习使用 Python Type Hints,以下是一些我推荐的开源项目供你参考。

相关实践学习
如何在云端创建MySQL数据库
开始实验后,系统会自动创建一台自建MySQL的 源数据库 ECS 实例和一台 目标数据库 RDS。
全面了解阿里云能为你做什么
阿里云在全球各地部署高效节能的绿色数据中心,利用清洁计算为万物互联的新世界提供源源不断的能源动力,目前开服的区域包括中国(华北、华东、华南、香港)、新加坡、美国(美东、美西)、欧洲、中东、澳大利亚、日本。目前阿里云的产品涵盖弹性计算、数据库、存储与CDN、分析与搜索、云通信、网络、管理与监控、应用服务、互联网中间件、移动服务、视频服务等。通过本课程,来了解阿里云能够为你的业务带来哪些帮助     相关的阿里云产品:云服务器ECS 云服务器 ECS(Elastic Compute Service)是一种弹性可伸缩的计算服务,助您降低 IT 成本,提升运维效率,使您更专注于核心业务创新。产品详情: https://www.aliyun.com/product/ecs
相关文章
|
19天前
|
数据采集 存储 XML
Python爬虫定义入门知识
Python爬虫是用于自动化抓取互联网数据的程序。其基本概念包括爬虫、请求、响应和解析。常用库有Requests、BeautifulSoup、Scrapy和Selenium。工作流程包括发送请求、接收响应、解析数据和存储数据。注意事项包括遵守Robots协议、避免过度请求、处理异常和确保数据合法性。Python爬虫强大而灵活,但使用时需遵守法律法规。
|
17天前
|
机器学习/深度学习 人工智能 TensorFlow
人工智能浪潮下的自我修养:从Python编程入门到深度学习实践
【10月更文挑战第39天】本文旨在为初学者提供一条清晰的道路,从Python基础语法的掌握到深度学习领域的探索。我们将通过简明扼要的语言和实际代码示例,引导读者逐步构建起对人工智能技术的理解和应用能力。文章不仅涵盖Python编程的基础,还将深入探讨深度学习的核心概念、工具和实战技巧,帮助读者在AI的浪潮中找到自己的位置。
|
17天前
|
机器学习/深度学习 数据挖掘 Python
Python编程入门——从零开始构建你的第一个程序
【10月更文挑战第39天】本文将带你走进Python的世界,通过简单易懂的语言和实际的代码示例,让你快速掌握Python的基础语法。无论你是编程新手还是想学习新语言的老手,这篇文章都能为你提供有价值的信息。我们将从变量、数据类型、控制结构等基本概念入手,逐步过渡到函数、模块等高级特性,最后通过一个综合示例来巩固所学知识。让我们一起开启Python编程之旅吧!
|
17天前
|
存储 Python
Python编程入门:打造你的第一个程序
【10月更文挑战第39天】在数字时代的浪潮中,掌握编程技能如同掌握了一门新时代的语言。本文将引导你步入Python编程的奇妙世界,从零基础出发,一步步构建你的第一个程序。我们将探索编程的基本概念,通过简单示例理解变量、数据类型和控制结构,最终实现一个简单的猜数字游戏。这不仅是一段代码的旅程,更是逻辑思维和问题解决能力的锻炼之旅。准备好了吗?让我们开始吧!
|
7天前
|
设计模式 缓存 开发者
Python中的装饰器:从入门到实践####
本文深入探讨了Python中强大的元编程工具——装饰器,它能够以简洁优雅的方式扩展函数或方法的功能。通过具体实例和逐步解析,文章不仅介绍了装饰器的基本原理、常见用法及高级应用,还揭示了其背后的设计理念与实现机制,旨在帮助读者从理论到实战全面掌握这一技术,提升代码的可读性、可维护性和复用性。 ####
|
16天前
|
设计模式 缓存 开发框架
Python中的装饰器:从入门到实践####
本文深入探讨了Python中装饰器的工作原理与应用,通过具体案例展示了如何利用装饰器增强函数功能、提高代码复用性和可读性。读者将学习到装饰器的基本概念、实现方法及其在实际项目开发中的实用技巧。 ####
26 3
|
18天前
|
机器学习/深度学习 数据挖掘 开发者
Python编程入门:理解基础语法与编写第一个程序
【10月更文挑战第37天】本文旨在为初学者提供Python编程的初步了解,通过简明的语言和直观的例子,引导读者掌握Python的基础语法,并完成一个简单的程序。我们将从变量、数据类型到控制结构,逐步展开讲解,确保即使是编程新手也能轻松跟上。文章末尾附有完整代码示例,供读者参考和实践。
|
19天前
|
人工智能 数据挖掘 程序员
Python编程入门:从零到英雄
【10月更文挑战第37天】本文将引导你走进Python编程的世界,无论你是初学者还是有一定基础的开发者,都能从中受益。我们将从最基础的语法开始讲解,逐步深入到更复杂的主题,如数据结构、面向对象编程和网络编程等。通过本文的学习,你将能够编写出自己的Python程序,实现各种功能。让我们一起踏上Python编程之旅吧!
|
18天前
|
数据采集 IDE 测试技术
Python实现自动化办公:从基础到实践###
【10月更文挑战第21天】 本文将探讨如何利用Python编程语言实现自动化办公,从基础概念到实际操作,涵盖常用库、脚本编写技巧及实战案例。通过本文,读者将掌握使用Python提升工作效率的方法,减少重复性劳动,提高工作质量。 ###
36 1
|
13天前
|
机器学习/深度学习 存储 数据挖掘
Python 编程入门:理解变量、数据类型和基本运算
【10月更文挑战第43天】在编程的海洋中,Python是一艘易于驾驭的小船。本文将带你启航,探索Python编程的基础:变量的声明与使用、丰富的数据类型以及如何通过基本运算符来操作它们。我们将从浅显易懂的例子出发,逐步深入到代码示例,确保即使是零基础的读者也能跟上步伐。准备好了吗?让我们开始吧!
24 0