Python 语言旨在使复杂任务变得简单,所以更新迭代的速度比较快,需要我们紧跟其步伐!
新版本的发布,总是会伴随着新特性和新功能的产生,我们在升级版本之前首先就是需要了解和属性这些要点,才可能会在之后的编程中灵活的使用到。迫不及待,蓄势待发,那现在就让我们开始吧!
1. PEP 563
新的语法特性:类型标注延迟求值
因为 Python
是动态类型语言,所以一直都没有类型注释这个功能。但是面对越来越多、越来越大的项目,有时调试很久却发现是因为参数类型导致的,很是尴尬和伤脑筋。所以在 Python3.5
开始类型注释就开始收到关注和欢迎,直到现在它来了。可以简单理解为,其是 Python
对自身弱类型语言的强化,希望获得一定的类型可靠和健壮度。当然,我们也可以使用像 PyCharm
或 mypy
这类第三方工具进行类型检查,类型注解只是一种内置开发工具而已,并非语法强制要求。
之前的版本对于类型注解支持的比较有限,存在以下三个问题,然而类型标注延迟求值的出现很好地解决了这些问题。但是,使用类型标注延迟求值的话,需要从 __future__
中导入 annotations
,且官方宣布将在未来的 Python3.10
中,默认会允许前向引用这种行为
- 性能问题 -> 导入
typing
模块慢的问题 - 前向引用 -> 不能用尚未声明的类型来注解(引发
NameError
报错) - 导入执行 -> 类型注释中的语句将会在导入时直接被执行从而产生不利影响
# [前向引用]
# 因为Tree类在__init__()方法定义的时候还没完成定义
In [1]: class Tree:
...: def __init__(self, left: Tree, right: Tree) -> None:
...: self.left = left
...: self.right = right
...:
--------------------------------------------------------------------------
NameError: name 'Tree' is not defined
# 然后为了使其不报错需要引入__future__模块
In [2]: from __future__ import annotations
# 再次执行就可以正常运行了
In [3]: class Tree:
...: def __init__(self, left: Tree, right: Tree) -> None:
...: self.left = left
...: self.right = right
...:
# [导入执行]
In [1]: def guess_name_game(myname: print('this is a string')):
...: guess_name: str = input("Try to guess my name: ")
...:
this is a string
# 然后为了使其不报错需要引入__future__模块
In [2]: from __future__ import annotations
# 再次执行就可以正常运行了
In [3]: def guess_name_game(myname: print('this is a string')):
...: guess_name: str = input("Try to guess my name: ")
...:
类型提示(Type hints
)只是 annotations 的一个实际应用场景,旨在方便排除和维护。而且 Python
程序在运行时,默认并没有做任何类型的检查,所以添加类型不会影响代码性能。需要注意的是,如果引用额外的话类型会需要引入 typing
模块(标准库中最慢的模块之一),信号在 Python3.7
中优化和提升 typing
模块的速度,所以不必太担心性能问题。
# type_hits_test.py
def guess_name_game(myname: str):
guess_name: str = input("Try to guess my name: ")
if guess_name == myname:
print("Wuoos, Good!")
else:
print("Sorry, =_=!")
if __name__ == '__main__':
myname: str = 'escape'
guess_name_game(myname)
2. PEP 553
新的内置特性:新内置的断点 breakpoint() 函数
以往我们都是使用 pdb
模块来进行代码调试的,在需要设置断点的地方调用 pdb.set_trace()
函数即可打断点。而在新版本的 Python
中,我们只需要调用 breakpoint()
函数就可以设置断点让程序停止,手动执行语句进行单步调试。当然,这里进入调试模式,还是我们熟悉的 pdb
调试命令。
# breakpoint_test.py
def guess_name_game(myname):
guess_name = input("Try to guess my name: ")
breakpoint()
if guess_name == myname:
print("Wuoos, Good!")
else:
print("Sorry, =_=!")
if __name__ == '__main__':
myname = 'escape'
guess_name_game(myname)
➜ python breakpoint_test.py
Try to guess my name: tom
> /Users/escape/fuckcode/demo/breakpoint_test.py(5)guess_name_game()
-> if guess_name == myname:
(Pdb) locals()
{'myname': 'escape', 'guess_name': 'tom'}
(Pdb)
虽然我们可以通过快速调用 print()
函数或 logging
模块来打信息或日志进行排错,但是现在我们可以使用 breakpoint()
内置函数在任何时候快速的插入调试器,进行代码调试。我们知道 pdb
只是众多可用调试器之一,我们还可以通过设置环境变量 PYTHONBREAKPOINT
来配置想要使用的调试器,如果将其设置为 0
的话,则会忽略此函数。
➜ PYTHONBREAKPOINT=0 breakpoint_test.py
Try to guess my name: tom
Sorry, =_=!
3. PEP 567
新的库模块:contextvars - 上下文变量
上下文变量是根据其上下文可以具有不同值的变量。它们类似于本地线程存储,一个变量在每个执行线程可能具有不同的变量值。但是,对于上下文变量,在一个执行线程中可能存在多个上下文。上下文变量的主要用例是跟踪并发异步任务中的变量。而新增 contextvars
模块,就是针对异步任务提供上下文变量的。
# contextvars_test.py
# 示例构造了三个上下文,每个上下文都有自己的name值
# 调用greet()函数在之后的每一个上下文中都可以使用name的值
import contextvars
name = contextvars.ContextVar("name")
contexts = list()
def greet():
print(f"Hello {name.get()}")
# 构造上下文并设置上下文变量名称
for first_name in ["Steve", "Dina", "Harry"]:
ctx = contextvars.copy_context()
ctx.run(name.set, first_name)
contexts.append(ctx)
# 在每个上下文中运行greet函数
for ctx in reversed(contexts):
ctx.run(greet)
# 运行输出三个上下文变量名称
$ python3.7 context_demo.py
Hello Harry
Hello Dina
Hello Steve
4. PEP 557
新的库模块:dataclasses - 数据类
在 Python3.7
的更新特性中,最受关注的就是 dataclasses
了。它是 Python3.7
版本中新增的内置模块,引入的主要目就是为了简化储存简单的数据的类的创建流程,减少代码冗余。
- 解决创建类初始化参数过多问题 -
__init__()
- 解决打印类对象的方式很不友好问题 -
__repr__()
- 解决对象比较问题 -
__order__()
- 解决对象去重问题 -
__hash__()
- 解决对象返回问题 -
to_dict
、to_json
# 手动安装
$ pip install dataclasses
新的 dataclass()
装饰器提供了,一种声明数据类的新方式,其使用变量标注来描述对应属性。它的构造器提供了一个装饰器和一些函数,用于自动添加生成特殊的魔术方法,例如 __init__()
,__repr__()
, __eq__()
以及 __hash__()
等魔术方法。
from dataclasses import dataclass
@dataclass
class InventoryItem:
"""Class for keeping track of an item in inventory."""
name: str
unit_price: float
quantity_on_hand: int = 0
def total_cost(self) -> float:
return self.unit_price * self.quantity_on_hand
def to_dict(self) -> dict:
return {
'name': self.name,
'unit_price': self.unit_price,
'quantity_on_hand': self.quantity_on_hand
}
# 类似给InventoryItem类添加__init__()方法
def __init__(self, name: str, unit_price: float, quantity_on_hand: int=0):
self.name = name
self.unit_price = unit_price
self.quantity_on_hand = quantity_on_hand
# 在使用dataclass装饰器时,还可以按需指定需要的特性
@dataclass(order=False)
@dataclass(init=True, repr=False, unsafe_hash=False)
@dataclass(init=True, eq=True, hash=False, frozen=False)
大部分情况下,我们可以直接使用 dataclass
方法来完成类的创建,但是有时我们还需要为具体字段指定具体行为。在这种情况下,就需要使用 dataclasses.field
方法。因为 Dataclasses
使用字段 field
来完提供默认值,而修改字段的具体行为就需要修改 field
了,从而更改默认值。
# 我们使用field来限制了name变量的值
# 使其不会被调用方修改掉,始终会用我们设置的Tom作为默认值
@dataclass
class GoodName:
name: str = field(init=False, default="Tom")
from datetime import datetime
from dataclasses import dataclass, field
@dataclass(hash=True, order=True)
class Product(object):
id: int
author_id: int
title: str = field(hash=False, repr=False, compare=False)
creation_time: datetime = field(default=None, repr=False, compare=False,hash=False)
update_time: datetime = field(default=None, repr=False, compare=False, hash=False)
source: str = field(default='', repr=False, compare=False, hash=False)
# 提示用户输入其名字
from dataclasses import dataclass, field
class GoodName:
name: str = field(default_factory=lambda: input("enter name"))
在 field
的源代码定义中,定义了各个参数是否将字段传入相关方法。除此之外,它的 default
参数指定了字段的默认值,default_factory
参数指定了生成默认值的函数,metadata
用于第三方扩展。
# 代码中定义的field函数参数
def field(*, default=MISSING, default_factory=MISSING, repr=True,
hash=None, init=True, compare=True, metadata=None)
field
同时具有 default
和 default_factory
参数。一般情况下,当我们为字段指定的默认只是不可变对象时,直接赋值给字段或在 field
的 default
参数中指定都可以。然而,如果我们要为字段指定一个可变对象(例如列表)作为默认值,我们需要将这个可变对象的构造函数指定为参数 default_factory
的值。如果我们将可变对象作为字段或 default
参数的值,会弹出 ValueError
错误。这样设置的主要目的,是为了回避 Python
中以可变对象作为默认参数的陷阱。
# None or List
class GoodNames:
def __init__(self, names=[]):
self.names = names
n1 = GoodNames()
n2 = GoodNames()
在某些情况下,我们的某些字段的值由其它字段生成,这种情况怎么办呢?幸运的是,dataclasses
为我们提供了 __post_init__
魔术方法。
@dataclass
class Pototo:
price: int = 1
quantity: int = 10
total: int = field(init=False)
def __post_init__(self):
self.total = self.price * self.total
5. PEP 562
对数据模型的改进:定制对模块属性的访问
PEP 562
中主要做是就是,能在模块下定义 __getattr__
和 __dir__
方法,实现定制访问模块属性。官网给出的用途就是 弃用某些属性/函数 和 **懒加载(lazy loading
)**。
- 弃用某些属性/函数
当我们在旧版本的 Python
程序中,如果弃用某些属性/函数时,成本很高。因为存在大量的调用方,需要我们手动一一进行修改,但很可能漏到或者考虑不全。如果修改补全,也会存在找不到对应属性/函数,而直接抛错误了且还不能做特殊处理。然而在,新版本的 Python3.7
就没这个问题,因为 __getattr__
让模块属性的访问非常灵活。
# getattr_test.py
def new_name_function(name):
print(f'Hello {name}!')
_deprecated_map = {
'old_name_function': new_name_function
}
def __getattr__(name):
if name in _deprecated_map:
switch_to = _deprecated_map[name]
return switch_to
raise AttributeError(f"module {__name__} has no attribute {name}.")
# python not is ipython
>>> from getattr_test import old_name_function
>>> old_name_function
<function new_name_function at 0x100fcdb80>
>>> old_name_function('escape')
Hello escape!
- 懒加载
简单来说,就是按需加载,即需要的时候才运行。我们都知道 Python
在导入很复杂逻辑的时候,import
很慢,然后通过 PEP 562
能够极大的提升 import
的执行效率。
# lib/__init__.py
import importlib
__all__ = ['geturl', ...]
def __getattr__(module):
if module in __all__:
return importlib.import_module("." + module, __name__)
raise AttributeError(f"module {__name__!r} has no attribute {module!r}")
# lib/geturl.py
print("Get URL loaded!")
class GetURL:
pass
# 可以看到导入lib的时候,GetURL并没有加的
# 当第一次使用lib.geturl.GetURL的时候才会加载
>>> import lib
>>> lib.geturl.GetURL
Get URL loaded!
在标准库里面也有应用,比如 bpo-32596
中对 concurrent.futures
模块的修改,可以让 import asyncio
时可以快 15%
以上。
def __dir__():
return __all__ + ('__author__', '__doc__')
def __getattr__(name):
global ProcessPoolExecutor, ThreadPoolExecutor
if name == 'ProcessPoolExecutor':
from .process import ProcessPoolExecutor as pe
ProcessPoolExecutor = pe
return pe
if name == 'ThreadPoolExecutor':
from .thread import ThreadPoolExecutor as te
ThreadPoolExecutor = te
return te
raise AttributeError(f"module {__name__} has no attribute {name}")
6. Note
主要记录语法等的变更新和说明
- [1] 向后不兼容的语法更改
为了避免向后兼容问题,现在 async
和 await
是保留的关键字。
- [2] 对数据模型的改进
dict
对象会保持插入时的顺序这个特性成为 Python
语言官方规范。
- [3] 标准库中的重大改进
asyncio
模块添加了新的功能,重大改进请参阅可用性与性能提升。time
模块现在提供纳秒级精度函数的支持 - 新增 6 个纳秒级精度时间函数
time.clock_gettime_ns() # 返回指定时钟时间
time.clock_settime_ns() # 设置指定时钟时间
time.monotonic_ns() # 返回不能倒退的相对时钟的时间(例如由于夏令时)
time.perf_counter_ns() # 返回性能计数器的值;专门用于测量短间隔的时钟
time.process_time_ns() # 返回当前进程系统和用户CPU时间的总和(不包括休眠时间)
time.time_ns() # 返回自1970年1月1日以来的纳秒数
- [4] CPython 实现的改进
新的 Python
开发模式 - Python3.7 Development Mode
在启动 Python
的命令行加上 -X importtime
参数可以在每次导入模块时显示耗费的时间
在启动 Python
的命令行加上 -X utf8
参数可以使 CPython
无视本地环境,强制使用 utf8
模式
# 导入模块耗费的时间
$ python3.7 -X importtime cpython_test.py
import time: self [us] | cumulative | imported package
import time: 2607 | 2607 | _frozen_importlib_external
......
# 激活开发者模式(debug和运行时检查)
$ python3.7 -X dev cpython_test.py
# 启用UTF-8模式
$ python3.7 -X utf8 cpython_test.py
7. Links
送人玫瑰,手有余香!