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 " + namegreeting(name)
  • 加入了 Type Hints 的写法如下:
name: str = "world"def greeting(name: str) -> str:return "Hello " + namegreeting(name)

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

常见数据结构的 Type Hints 写法

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

默认参数

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

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

自定义类型

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

class Student(object):def __init__(self, name, age):self.name = nameself.age = agedef 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, Dictl: 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 TypeVarT = 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 UnionT = Union[str, bytes]def concat(s1: T, s2: T) -> T:return s1 + s2concat("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 TypeVarT = TypeVar("T", str, bytes)def concat(s1: T, s2: T) -> T:return s1 + s2concat("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 Anydef greeting(name):return "Hello " + namedef greeting(name: Any) -> Any:return "Hello " + name

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

Any 类型值赋给更精确的类型时,不执行类型检查,如下代码 IDE 并不会有错误提示:

from typing import Anya: Any = None
a = []  # 动态语言特性
a = 2s: str = ''
s = a  # Any 类型值赋给更精确的类型

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

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

from typing import Callabledef 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 = leftself.right = righttree1 = Tree(Tree(), Tree())

IDE 同样能够对自引用类型进行检查。

此形式不仅能够用于自引用,前置引用同样适用。

鸭子类型

Python 一个显著的特点是其对鸭子类型的大量应用,Type Hints 提供了 Protocol 来对鸭子类型进行支持。定义类时只需要继承 Protocol 就可以声明一个接口类型,当遇到接口类型的注解时,只要接收到的对象实现了接口类型的所有方法,即可通过类型注解的检查,IDE 便不会报错。这里的 Stream 无需显式继承 Interface 类,只需要实现了 close 方法即可。

from typing import Protocolclass 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: intname: strfriends: 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 = idself.name = nameself.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 Optionalfrom pydantic import BaseModelclass User(BaseModel):id: intname = 'John Doe'signup_ts: Optional[datetime] = Nonefriends: 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 ValidationErrortry: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()@contextmanagerdef execute(self):try:yield self.cursor.executeself.conn.commit()except Exception as e:self.conn.rollback()logging.exception(e)@contextmanagerdef executemany(self):try:yield self.cursor.executemanyself.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_listdef fetchone(self) -> Optional[FetchObject]:result = self.cursor.fetchone()return self._tuple_to_object([result])[0] if result else Nonedef fetchmany(self, size: Optional[int] = None) -> Optional[List[FetchObject]]:result = self.cursor.fetchmany(size)return self._tuple_to_object(result) if result else Nonedef 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_hintsdef 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/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 Type Hints 从入门到实践相关推荐

  1. python数据分析案例2-1:Python练习-Python爬虫框架Scrapy入门与实践

    本文建立在学习完大壮老师视频Python最火爬虫框架Scrapy入门与实践,自己一步一步操作后做一个记录(建议跟我一样的新手都一步一步进行操作). 主要介绍: 1.scrapy框架简介.数据在框架内如 ...

  2. 《Python编程:从入门到实践》项目1:外星人入侵(完整版)

    本文主要是在第13章的基础上添加了第14章的记分环节. 在第14章,主要是添加了游戏启动前的一个Play按钮.游戏等级的设置.击杀外星人后的得分记录并在屏幕上显示当前得分.最高得分.当前等级以及余下的 ...

  3. python快速编程入门课后简答题答案-Python编程:从入门到实践(第2版)第1章习题答案...

    <Python编程:从入门到实践>是一本不错的书.第2版已经公开预售,预计会在10月份正式上市. 动手试一试 本章的练习都是探索性的,但从第2章开始将要求你用那一章学到的知识来解决问题. ...

  4. python编程入门到实践 百度云-python网络爬虫从入门到实践pdf

    python网络爬虫从入门到实践pdf是一本非常热门的编程教学.这本书籍详细讲解了Python以及网络爬虫相关知识,非常适合新手阅读,需要的用户自行下载吧. Python网络爬虫从入门到实践电子书介绍 ...

  5. python网络爬虫教程-终于明了python网络爬虫从入门到实践

    Python是一款功能强大的脚本语言,具有丰富和强大的库,重要的是,它还具有很强的可读性,易用易学,非常适合编程初学者入门.以下是小编为你整理的python网络爬虫从入门到实践 环境配置:下载Pyth ...

  6. python编程入门课程视频-带学《Python编程:从入门到实践》

    以<Python编程:从入门到实践>为教材,以吕老师+几位同学学习串讲为主线,系统的进行python的入门,并手把手带着大家做课后习题. 教材简介: <python编程从入门到实践& ...

  7. python编程入门p-读书笔记 - 《Python编程:从入门到实践》

    Tag:看<Python编程:从入门到实践>读书笔记 基础知识: 字符串str 改变大小写(临时):title首字母大写,upper全大写,lower全小写 删除空白(临时):rstrip ...

  8. python起步输入-《Python编程:从入门到实践》第一章:起步

    Python编程:从入门到实践 这篇文章主要知识点是关于Python编程,从入门到实践,起步,Python编程:从入门到实践,的内容,如果大家想对相关知识点有系统深入的学习,可以参阅以下电子书 搭建编 ...

  9. python编程入门到实践笔记-python基础(《Python编程:从入门到实践》读书笔记)...

    注: 本文的大部分代码示例来自书籍<Python编程:从入门到实践>. 一.变量: 命名: (1)变量名只能包含字母.数字和下划线.变量名可以字母或下划线打头,但不能以数字打头 (2)变量 ...

最新文章

  1. Linux无法启动(一)
  2. Prim算法生成迷宫
  3. ORACLE 中为什么要把列名都转换成大写字母?
  4. 区块链BaaS云服务(31) 吉利 Concordium区块链
  5. MySQL主主复制 外键_MySQL 组复制介绍
  6. CMD各段定义与分配方法指引
  7. 腾讯优图提出LCVR-MQVI算法,勇夺NTIRE 2021双赛道冠亚军
  8. 如何限制修改IP地址
  9. 利用syslinux制作Dos、WinPE、Slax Linux集成u盘
  10. 如何理解CPU上下文切换(二)
  11. 权值线段树BZOJ3224
  12. iOS codeview
  13. Python学习之路 第3次笔记!
  14. centos7字体颜色改变_CentOS7.3中设置Shell终端文本外观自定义字体
  15. android手机变windows8,你真没有看错!Android手机一秒变Win10
  16. 使用百度地图API在页面添加百度地图应用
  17. 澳门大学计算机qs排名,澳门大学世界排名(澳门科技大学qs世界排名2021)
  18. 黑马程序员JS学习第一天
  19. 设置iframe高度的问题
  20. 乐视腾讯深度合作 超级电视26日易迅网首发

热门文章

  1. 编写Eureka入门案例
  2. spring整合junit测试
  3. html语言 input button,Html-button和input的区别
  4. Versions maven plugin 修改版本
  5. ConditionObject源码
  6. etcd mysql集群_Etcd单节点扩容为三节点集群
  7. arcgis api for js共享干货系列之一自写算法实现地图量算工具
  8. Mysql---之Mysqladmin修改账号密码方式
  9. 倒排文件索引(Inverted File Index)的建立
  10. [原]Console小技巧——Console版贪食蛇