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/peps/pep-0484/]中提出了 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/peps/pep-0585/]的出现解决了这个问题,我们可以直接使用 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/peps/pep-0557/]中被定义。

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/jianghushinian/python-scripts/blob/main/scripts/mysql_handler_type_hints.py]是我对 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,以下是一些我推荐的开源项目供你参考:

推荐阅读

TypeScript 枚举指南

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

Python Type Hints 从入门到实践的更多相关文章

  1. <<Python编程:从入门到实践>>踩坑记 Django

    <<Python编程:从入门到实践>>踩坑记 Django Django Python 19.1.1.5 模板new_topic 做完书上的步骤后,对主题添加页面经行测试,但是 ...

  2. Python编程:从入门到实践(选记)

    本文参考< Python 编程:从入门到实践>一书,作者: [ 美 ] Eric Matthes 第1章 起步 1.1     搭建python环境 在不同的操作系统中, Python 存 ...

  3. Book - 《Python编程:从入门到实践》

    Tag:看<Python编程:从入门到实践>学习笔记 数据类型相关: 字符串str 改变大小写(临时):title首字母大写,upper全大写,lower全小写 删除空白(临时):rstr ...

  4. python编程:从入门到实践----第六章>字典

    一.一个简单的字典:alien_0存储外星人的颜色和点数,使用print打印出来 alien_0 = {'color': 'green','points': 5} print(alien_0['col ...

  5. python编程:从入门到实践----第五章>if 语句

    一.一个简单示例 假设有一个汽车列表,并想将其每辆汽车的名称打印出来.遇到汽车名‘bmw’,以全大写打印:其他汽车名,首字母大写 cars=['audi','bmw','subaru','toyota ...

  6. python编程:从入门到实践----第四章>操作列表

    一.遍历整个列表 1-1.假设有一个魔术师名单,需要将其中每个魔术师的名字都打印出来. # 用for循环来打印魔术师名单中的名字 magicians=['alice','david','carolin ...

  7. 《Python编程:从入门到实践》基础知识部分学习笔记整理

    简介 此笔记为<Python编程:从入门到实践>中前 11 章的基础知识部分的学习笔记,不包含后面的项目部分. 书籍评价 从系统学习 Python 的角度,不推荐此书,个人更推荐使用< ...

  8. 《python编程:从入门到实践》课后习题及答案

    转载: <Python编程:从入门到实践>课后习题及答案-码农之家 (xz577.com) <Python编程:从入门到实践>课后习题及答案 - 信德维拉 - 博客园 (cnb ...

  9. 《Python编程:从入门到实践》第十八章笔记:Django最基本用法笔记

    最近在看Python编程:从入门到实践,这是这本书"项目3 Web应用程序"第18章的笔记.记录了django最基本的一些日常用法,以便自己查阅. 可能是我的这本书版本比较老,书上 ...

  10. Python type hints 之 Optional,Union

    1,前言 type hint 在pep484加入,我个人觉得这种类似于类型约束的(机制)有点违背了python简单.简洁的初衷,在慢慢向c# java 这种强类型语言看齐的节奏. 不过好在不强制使用, ...

随机推荐

  1. Vite4+Typescript+Vue3+Pinia 从零搭建(3) - vite配置

    项目代码同步至码云 weiz-vue3-template 关于vite的详细配置可查看 vite官方文档,本文简单介绍vite的常用配置. 初始内容 项目初建后,vite.config.ts 的默认内 ...

  2. day01预习-基本语法

    typora-copy-images-to: media 基本语法 JavaScript的历史: ​ 在95年以前,就有很多上网的用户了,当时的带宽只有28.8kb/s,用户要进行表单的验证时,点击提 ...

  3. odoo17.0 快递鸟模块

    快递鸟是国内使用较为广泛的快递集成查询平台之一,提供了600+的物流公司对接接口,是比较不错的物流查询服务选择.随着odoo17.0的发布,我们最近也将快递鸟模块升级到了17.0.下面我们来详细看一下 ...

  4. vim的各种快捷键

    目录 一.准备一份用于练习的文件 二.命令模式下的快捷键 1.移动光标 2.删除文字 3.复制粘贴 4.撤销上一次操作 5.恢复上一次撤销的操作 6.查找 7.替换单个字符 8.光标移动到最左边 9. ...

  5. Go 获取 IP 地址

    1. 获取本地IP地址 使用 net 包可以获取本地机器的 IP 地址.以下是一个获取本地 IP 地址的简单示例: package main import ( "fmt" &quo ...

  6. maven 配置(cmd 黑窗口执行 mvn 时默认的 settings 文件和 idea maven 相关配置)

    写在前面: 本文章用于记录博主平时遇到的问题,步骤略粗糙,目的在于记录一边后续博主自己查找,如果能帮助到其他人更好.文章中用到的链接均为自行引入,侵删,谢谢(2I2Rc*@JY8) 问题说明:在一次使 ...

  7. C++ Qt开发:TabWidget实现多窗体功能

    Qt 是一个跨平台C++图形界面开发库,利用Qt可以快速开发跨平台窗体应用程序,在Qt中我们可以通过拖拽的方式将不同组件放到指定的位置,实现图形化开发极大的方便了开发效率,本章将重点介绍TabWidg ...

  8. Java操作Word修订功能:启用、接受、拒绝、获取修订

    Word的修订功能是一种在文档中进行编辑和审阅的功能.它允许多个用户对同一文档进行修改并跟踪这些修改,以便进行审查和接受或拒绝修改.修订功能通常用于团队合作.专业编辑和文件审查等场景. 本文将从以下几 ...

  9. 华企盾DSC手机app注册保存成功登录时一直转圈

    可能是花生壳等映射软件不稳定,重启一下花生壳等工具

  10. NC65二开经验总结

    公式相关 1.显示公式没执行 列表界面显示,卡片界面不显示: Handler的onBoCard执行: getBillCardPanel().execHeadLoadFormulas(); Contro ...