Python3.8有什么新变化


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

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

Python3.8于2019年10月14日发布


1. PEP 572

新的特性:赋值表达式(海象运算符) - Assignment Expressions

PEP 572 的标题就是 Assignment Expressions,顾名思义就是赋值表达式,也称为命名表达式。新增的语法 := 可在表达式内部为变量赋值,因为它很像是海象的眼睛和长牙,所以被昵称为“海象运算符”。其主要作用就是赋值给中间变量,通过下面的示例代码,相信很快我们就可以理解了。

# 写正则匹配条件的通常写法
# 这样会重复地执行2次re.match(data)方法,影响执行效率
# 第一次检测用于匹配是否发生,而第二次用于提取子分组数据
discount = 0.0
data = 123.456
pattern = re.compile(r'(\d+)% discount')
if pattern.match(data):
    discount = float(pattern.match(data).group(1)) / 100.0

# 在没有赋值表达式之前,正确的写法应该这样
discount = 0.0
data = 123.456
pattern = re.compile(r'(\d+)% discount')
match = pattern.match(data)
if match:
    discount = float(match(data).group(1)) / 100.0

上面的示例,我们可以简单理解为:在求值的过程中赋值了新的中间变量,而这些中间变量(match)可以在代码块中被继续使用。即这是一种新的代码编写风格,在使用上需要开发者慢慢写法这种用法。

# 使用赋值表达式可以解决上面的问题(当然可以预编译)
# 本来if这种控制结构语句只是求值表达式,只看结果是不是符合条件
# 而这里则是re.match()求值之后把结果赋值给了match变量并对其进行判断
discount = 0.0
data = 123.456
if (match := re.match(r'(\d+)% discount', data)) is not None:
    discount = float(match.group(1)) / 100.0

而且这种赋值表达式也可以用在列表解析里面,极大地丰富了使用的场景。但是需要注意的是,尽量将海象运算符的使用限制在清晰的场合中,以降低复杂性并提升可读性。

# 赋值表达式可以避免调用len()两次
if (n := len(a)) > 10:
    print(f"List is too long ({n} elements, expected <= 10)")

# 适用于配合while循环计算一个值来检测循环是否终止,而同一个值又在循环体中再次被使用的情况
while (block := f.read(256)) != '':
    process(block)

# 列表推导式中使用
[[y := f(x), x/y] for x in range(5)]

# 一行实现斐波那契数列
[(t:=(t[1], sum(t)) if i else (0,1))[1] for i in range(10)]

2. PEP 570

新的特性:强制使用者使用位置参数 - Positional-Only Parameters

新增了一个函数形参语法,主要是用来指明某些函数形参必须使用仅限位置而非关键字参数的调用形式。可以简单理解为,强制使用者使用位置参数。

def divmod(a, b, /):
    return (a // b, a % b)

上面实例中,我们可以看到函数参数列表中多了一个 / 参数,用途就是在 / 左面的这些参数,只能是位置参数,而不能是关键字参数。如果使用关键字的话,则会如下所示的报错。

>>> divmod(3, 2)
(1, 1)

>>> divmod(x=3, y=2)
TypeError: divmod() takes no keyword arguments

是不是奇怪为什么要这样做呢?究其原因,就是便于我们进行函数参数的管理和使用(搭配 * 食用更佳)。

# 多种可选组合
people(name, ages, /)
people(name, ages=None, /)
people(name, ages, /, address)
people(name, ages, /, address, *, city)
people(name, ages, /, address=None, *, city)

# 可变参数
people(name, /, *args, **kwargs)

# 简化函数和方法的实现
class Counter(dict):
    def __init__(self, iterable=None, /, **kwds):
        ...

3. PEP 578

功能修复和增强:给 Python 运行时添加审计钩子

查看现在支持的审计事件名字以及 API 的话可以查看 PEP 文档,当然也是支持自定义事件的。

In [1]: import sys

In [2]: import urllib.request

In [3]: def audit_hook(event, args):
   ...:     if event in ['urllib.Request']:
   ...:         print(f'Network {event=} {args=}')
   ...:

In [4]: sys.addaudithook(audit_hook)

In [5]: urllib.request.urlopen('https://httpbin.org/get?a=1')
Network event='urllib.Request' args=('https://httpbin.org/get?a=1', None, {}, 'GET')
Out[5]: <http.client.HTTPResponse at 0x1118a6760>

4. importlib

新增模块:importlib

新增的 importlib.metadata 模块提供了从第三方包读取元数据的(临时)支持。例如,它可以提取一个已安装软件包的版本号、入口点列表等等。

>>> from importlib.metadata import version, requires, files

>>> version('requests')
'2.22.0'

>>> list(requires('requests'))
['chardet (<3.1.0,>=3.0.2)']

>>> list(files('requests'))[:5]
[PackagePath('requests-2.22.0.dist-info/INSTALLER'),
 PackagePath('requests-2.22.0.dist-info/LICENSE'),
 PackagePath('requests-2.22.0.dist-info/METADATA'),
 PackagePath('requests-2.22.0.dist-info/RECORD'),
 PackagePath('requests-2.22.0.dist-info/WHEEL')]

>>> dist.metadata['Requires-Python']
'>=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*'

5. lru_cache

改进的模块:functools.lru_cache()

新版本的 functools.lru_cache() 可以直接作为装饰器,而不是作为返回装饰器的函数,即不用加括号了。同时,装饰器还支持 max_sizetyped2 个参数。

# 默认可不加括号
@lru_cache
def f(x):
    ...
# 设置缓存大小时需要括号
@lru_cache(maxsize=32)
def get_pep(num):
    'Retrieve text of a Python Enhancement Proposal'
    resource = 'http://www.python.org/dev/peps/pep-%04d/' % num
    try:
        with urllib.request.urlopen(resource) as s:
            return s.read()
    except urllib.error.HTTPError:
        return 'Not Found'


>>> for n in 8, 290, 308, 320, 8, 218, 320, 279, 289, 320, 9991:
...     pep = get_pep(n)
...     print(n, len(pep))

>>> get_pep.cache_info()
CacheInfo(hits=3, misses=8, maxsize=32, currsize=8)

6. cached_property

改进的模块:functools.cached_property()

添加了新的 functools.cached_property() 装饰器,用于在实例生命周期内缓存已计算特征属性。这是一个非常常用的功能,很多知名 Python 项目都自己实现过它,现在终于进入版本库了。我们这就简单看看其的使用方法和特性。

下面的实例中,我们创建 Dataset 的实例之后调用 d.variance 时,可以看到执行了 variance 方法的逻辑(可以看到对应输出信息),而再次执行的时候就直接返回缓存的属性的值了。而且标准库中的版本还有一种的特点,就是加了线程锁,防止多个线程一起修改缓存。可以确保在项目中使用多线程并发访问时,不会出现数值的修改,更加安全可靠。需要注意的是其不支持异步调用,如果需要使用在异步中,可以使用 pydanny 提供的 cached-property 这个包。

import functools
import statistics

class Dataset:
   def __init__(self, sequence_of_numbers):
      self.data = sequence_of_numbers

   @functools.cached_property
   def variance(self):
      print('get variance data')
      return statistics.variance(self.data)
>>> from functools_test import Dataset
>>> d = Dataset(40)

>>> d.variance
get variance data
40

>>> d.variance
40

7. singledispatchmethod

改进的模块:functools.singledispatchmethod()

添加了新的 functools.singledispatchmethod() 可以将普通函数转换为泛型函数的装饰器。泛型函数是指由多个函数组成的函数,可以对不同类型实现相同的操作,调用时应该使用哪个实现由分派算法决定。而 Single dispatch 则是一种泛型函数分派形式,基于单个参数的类型来决定。

from functools import singledispatchmethod
from contextlib import suppress

class TaskManager:
    def __init__(self, tasks):
        self.tasks = list(tasks)

    @singledispatchmethod
    def discard(self, value):
        with suppress(ValueError):
            self.tasks.remove(value)

    @discard.register(list)
    def _(self, tasks):
        targets = set(tasks)
        self.tasks = [x for x in self.tasks if x not in targets]

上面是官方文档中给出的示例代码,想必不太好理解吧,那我们一起看一个简单的示例!下面示例代码中,就是通过 json 序列化一个字段数据,但是因为 now 字段是 datetime() 类型不能直接 json 序列化。

In [1]: from datetime import datetime, date

In [2]: now = datetime.now()

In [3]: d = {'now': now, 'name': 'XiaoMing'}

In [4]: import json

In [5]: json.dumps(d)
----------------------------------------------------------------------
TypeError: Object of type datetime is not JSON serializable

而一般常用的解决方案就是,指定 default 参数,使用函数对其进行格式转化。如果一个对象的 objdatetimedate 类型的话,在序列化的时候就直接用 obj.isoformat() 将其转成了字符串,之后在进行显示。

In [7]: def json_encoder(obj):
   ...:     if isinstance(obj, (date, datetime)):
   ...:         return obj.isoformat()
   ...:     raise TypeError(f'{repr(obj)} is not JSON serializable')
   ...:

In [8]: json.dumps(d, default=json_encoder)
Out[8]: '{"now": "2020-10-16T15:42:21.172975", "name": "XiaoMing"}'

如果用 singledispatch 的话,就需要这么写。虽然可以完成任务,但是整体逻辑分散增加了代码行数以及该模式会不必要的增加代码阅读的难度,所以很少在正式环境上面使用到。

In [12]: from functools import singledispatch

In [13]: @singledispatch
    ...: def json_encoder(obj):
    ...:     raise TypeError(f'{repr(obj)} is not JSON serializable')
    ...:

In [14]: @json_encoder.register(date)
    ...: @json_encoder.register(datetime)
    ...: def encode_date_time(obj):
    ...:     return obj.isoformat()
    ...:

In [15]: json.dumps(d, default=json_encoder)
Out[15]: '{"now": "2020-10-16T15:42:21.172975", "name": "XiaoMing"}'

需要知道的是 singledispatch 主要针对的是函数,而对于方法则需要使用 singledispatchmethod 来做。

# 把singledispatch用在方法上就失效了
In [1]: from functools import singledispatch

In [2]: class Dispatch:
   ...:     @singledispatch
   ...:     def foo(self, a):
   ...:         return a
   ...:     @foo.register(int)
   ...:     def _(self, a):
   ...:         return 'int'
   ...:     @foo.register(str)
   ...:     def _(self, a):
   ...:         return 'str'
   ...:

In [3]: cls = Dispatch()

# 没有返回 'int'
In [4]: cls.foo(1)
Out[4]: 1

# 没有返回 'str'
In [5]: cls.foo('s')
Out[5]: 's'
# 使用singledispatchmethod生效了
In [6]: from functools import singledispatchmethod

In [7]: class Dispatch:
   ...:     @singledispatchmethod
   ...:     def foo(self, a):
   ...:         return a
   ...:     @foo.register(int)
   ...:     def _(self, a):
   ...:         return 'int'
   ...:     @foo.register(str)
   ...:     def _(self, a):
   ...:         return 'str'
   ...:

In [8]: cls = Dispatch()

In [9]: cls.foo(1)
Out[9]: 'int'

In [10]: cls.foo('s')
Out[10]: 'str'

这种模式还可以用在 classmethodstaticmethodabstractmethod 等装饰器上,如官网的例子。

class Negator:
    @singledispatchmethod
    @classmethod
    def neg(cls, arg):
        raise NotImplementedError("Cannot negate a")

    @neg.register
    @classmethod
    def _(cls, arg: int):
        return -arg

    @neg.register
    @classmethod
    def _(cls, arg: bool):
        return not arg

8. multiprocessing

改进的模块:multiprocessing

添加了新的 multiprocessing.shared_memory 模块,可以跨进程直接访问同一内存(共享)。

# Python进程A(注意name)
>>> from multiprocessing import shared_memory
>>> process_a = shared_memory.ShareableList([1, 'a', 0.1])
>>> process_a
ShareableList([1, 'a', 0.1], name='psm_d5d6ba1b')

# Python进程B(使用name就可以共享内存)
>>> from multiprocessing import shared_memory
>>> process_b = shared_memory.ShareableList(name='psm_d5d6ba1b')
>>> b
Out: ShareableList([1, 'a', 0.1], name='psm_d5d6ba1b')

9. asyncio

改进的模块:asyncio

REPL,即交互式的编程环境,其对于学习一门新的编程语言非常有帮助。不过,官方自带的 REPL 功能非常有限,通常开发者都会使用 IPython 来学习和调试。而且在 IPython 7.0 开始,已经支持 AsyncREPL 了。而在 Python 3.8 中,官方 REPL 也与时俱进,支持 asyncio REPL 了。

import asyncio

async def main():
    await asyncio.sleep(0)
    return 42

asyncio.run(main())

运行 python -m asyncio 将启动一个原生异步 REPL。简单来说就是,这允许可以直接使用 await,而不必用 asyncio.run()),因为此操作会在每次发起调用时产生一个新事件循环。

# 注意激活asyncio的REPL环境
$ python -m asyncio
asyncio REPL 3.8.0
Use "await" directly instead of "asyncio.run()".
Type "help", "copyright", "credits" or "license" for more information.
>>> import asyncio
>>> await asyncio.sleep(10, result='hello')
hello

添加 asyncio.run() 已经从暂定状态晋级为稳定 API。此函数可被用于执行一个 coroutine 并返回结果,同时自动管理事件循环。

$ python.exe -m asyncio
asyncio REPL 3.8.0
Use "await" directly instead of "asyncio.run()".
Type "help", "copyright", "credits" or "license" for more information.
>>> import asyncio
>>> async def b():
...     await asyncio.sleep(1)
...     return 'B'
...

>>> await b()
'B'

>>> async def c():
...     await asyncio.sleep(1)
...     return 'C'
...

>>> task = asyncio.create_task(c())
>>> await task
'C'

>>> await asyncio.sleep(1)

10. f-string

功能修复和增强:[语法糖] 字符串支持调试(自动记录表达式和调试文档)

Python 3.6 的时候都已经增加了 f-string 了,虽然 f-string 可以让格式化变的美观而又简单,但依然对代码调试毫无帮助。主要是因为使用还是不是很方便,比如在代码调试的时候,很难区分多个 print() 语句的位置。虽然可以使用转换标志来区分,但是会让输出边长。

# 转换标志: !a
>>> value = "哈哈"
>>> print(f'"A ascii": {value!a}, value="哈哈"')
"A ascii": '\u54c8\u54c8', value="哈哈"

# 转换标志: !s
>>> value = "哈哈"
>>> print(f'"A string": {value!s}, value="哈哈"')
"A string": 哈哈, value="哈哈"

# 转换标志: !r
>>> value = "哈哈"
>>> print(f'"A repr": {value!r}, value="哈哈"')
"A repr": '哈哈', value="哈哈"

而在新版本中 f-string 加了调试功能,使用 = 符号,即自身支持变量计算。

# 计算
>>> value = 10
>>> print(f'{value / 3 + 15=}')


# 显示
>>> name = 'tom'
>>> print(f'{name=:*^20}')
name=********tom*********

# 显示
>>> name = 'tom'
>>> print(f'*{name=:^20}*')
*name=        tom         *
# 混合转换标识
# DEBUG模式可以和转换标识一起用
>>> say = '哈哈'
>>> f'{say=}'
"say='哈哈'"

>>> f'{say=!s}'
'say=哈哈'

>>> f'{say=!r}'
"say='哈哈'"

>>> f'{say=!a}'
"say='\\u54c8\\u54c8'"

>>> f'{say=!s:^20}'
'say=         哈哈         '

11. Note

主要记录语法等的变更新和说明

  • [1] CPython 实现的改进

新增的 PYTHONPYCACHEPREFIX 变量,用于已编译字节码文件的并行文件系统缓存。
增加了一个新的 C API 用来配置 Python 初始化,提供对整个配置过程的更细致控制以及更好的错误报告。

# 可将隐式的字节码缓存配置为使用单独的并行文件系统树
# 而不再是默认的每个源代码目录下的__pycache__子目录
$ python3.8 -X pycache_prefix test.py
$ PYTHONPYCACHEPREFIX python3.8 test.py
  • [2] 标准库中的重大改进

Async Mock => 单元测试模块 unittest 添加了 mock 异步代码的类。

>>> import asyncio
>>> from unittest.mock import AsyncMock, MagicMock

>>> mock = AsyncMock(return_value={'json': 123})
>>> await mock()
{'json': 123}

>>> asyncio.run(mock())
{'json': 123}

Python3.8 中对可迭代解包的改进,修复了解包的问题。

# 单个返回
In [1]: def a():
   ...:     rest = (4, 5, 6)
   ...:     return 1, 2, 3, *rest
   ...:

In [2]: [print(i) for i in a()]
1
2
3
4
5
6
Out[2]: [None, None, None, None, None, None]

# 返回元组
In [3]: def b():
   ...:     rest = (4, 5, 6)
   ...:     yield 1, 2, 3, *rest
   ...:

In [4]: [print(i) for i in b()]
(1, 2, 3, 4, 5, 6)
Out[4]: [None]
  • [3] 性能改进

许多内置方法和函数的速度都提高了 20%~50%,因为之前许多函数都需要进行不必要的参数转换。
新创建的列表现在平均比以前小了 12%,这要归功于列表构造函数如果能提前知道列表长度的情况下,可以进行优化。
文件复制操作如 shutil.copyfile()shutil.copytree() 现在使用平台特定的调用和其他优化措施,来提高操作速度。


送人玫瑰,手有余香!


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