Python3.7有什么新变化


Python 语言旨在使复杂任务变得简单,所以更新迭代的速度比较快,需要我们紧跟其步伐!

新版本的发布,总是会伴随着新特性和新功能的产生,我们在升级版本之前首先就是需要了解和属性这些要点,才可能会在之后的编程中灵活的使用到。迫不及待,蓄势待发,那现在就让我们开始吧!

Python3.7于2018年6月27日发布


1. PEP 563

新的语法特性:类型标注延迟求值

因为 Python 是动态类型语言,所以一直都没有类型注释这个功能。但是面对越来越多、越来越大的项目,有时调试很久却发现是因为参数类型导致的,很是尴尬和伤脑筋。所以在 Python3.5 开始类型注释就开始收到关注和欢迎,直到现在它来了。可以简单理解为,其是 Python 对自身弱类型语言的强化,希望获得一定的类型可靠和健壮度。当然,我们也可以使用像 PyCharmmypy 这类第三方工具进行类型检查,类型注解只是一种内置开发工具而已,并非语法强制要求。

之前的版本对于类型注解支持的比较有限,存在以下三个问题,然而类型标注延迟求值的出现很好地解决了这些问题。但是,使用类型标注延迟求值的话,需要从 __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 版本中新增的内置模块,引入的主要目就是为了简化储存简单的数据的类的创建流程,减少代码冗余。

  1. 解决创建类初始化参数过多问题 - __init__()
  2. 解决打印类对象的方式很不友好问题 - __repr__()
  3. 解决对象比较问题 - __order__()
  4. 解决对象去重问题 - __hash__()
  5. 解决对象返回问题 - to_dictto_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 同时具有 defaultdefault_factory 参数。一般情况下,当我们为字段指定的默认只是不可变对象时,直接赋值给字段或在 fielddefault 参数中指定都可以。然而,如果我们要为字段指定一个可变对象(例如列表)作为默认值,我们需要将这个可变对象的构造函数指定为参数 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] 向后不兼容的语法更改

为了避免向后兼容问题,现在 asyncawait 是保留的关键字。

  • [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

送人玫瑰,手有余香!


文章作者: Escape
版权声明: 本博客所有文章除特別声明外,均采用 CC BY 4.0 许可协议。转载请注明来源 Escape !
  目录