前言

这是一篇补作业的文章,本文以一个资深 Python 开发的角度分析这个版本添加了那些有用的新功能和特性。

新的上下文管理器语法

其实就是可以使用 with 把多个上下文管理器 (Context Manager) 用括号括起来。在之前的版本,有时候我们想要在一个 with 里面放多个上下文,放在一行很长,一个常见的方式是使用反斜杠,可以把 open 对齐:

In : with open('input', mode='rb') as input, \
...:      open('output', mode='rb') as output:
...:     ...
...:

但是这样写,会让 pep8、black 等代码检查工具报错:它们都认为不应该使用反斜杠显式续行。

现在可以给这些上下文管理器加个括号就完美了 (代码洁癖者的福音):

In : with (
...:     open('input', mode='rb') as input,
...:     open('output', mode='rb') as output
...: ):
...:     ...
...:

更好的错误提示

在官方中列举了一些之前版本中不够明显友好的错误信息例子 (延伸阅读链接 1)。,这个版本集中的处理了它们。本文就列出 2 个我感受最明显的,其他的不挨个展示了。

对于初学者,第一次写 hello world 时就可能这么写:

In : print("Hello, World!)
  File "<ipython-input-7-bc0808c61f64>", line 1
    print("Hello, World!)
                         ^
SyntaxError: EOL while scanning string literal

而新的版本提示如下:

In : print("Hello, World!)
  Input In []
    print("Hello, World!)
          ^
SyntaxError: unterminated string literal (detected at line 1)

同样是 SyntaxError,是不是新版本的提示更容易理解呢?

再看判断时误用了单个等号 (正确语法是双等号 ==):

In : if b = 1: pass
  File "<ipython-input-8-e4cfea4b3624>", line 1
    if b = 1: pass
         ^
SyntaxError: invalid syntax

而新的版本:

In : if b = 1: pass
  Input In []
    if b = 1: pass
       ^
SyntaxError: invalid syntax. Maybe you meant '==' or ':=' instead of '='?

不仅提示错误类型,还给了提示。

当然大家也没必要具体了解每个错误提示的改进,反正知道从 Python 3.10 开始错误提示更友好了就可以了。

PEP 634: Structural Pattern Matching

这是这个版本最大的也是最重要的新Match-Case语法,具体的可以看我之前写的文章: Python 3.10 里面的 Match-Case 语法详解

类型系统

具体的可以看我之前写的文章: Python 3.10 新加入的四个和类型系统相关的新特性

zip 函数的参数

相信每个工作经验多的 Python 开发都经历过这个坑:

In : list(zip(['a', 'b', 'c'], [1, 2]))
Out: [('a', 1), ('b', 2)]

当然参数内的元素长度不同时,长度长的那部分会被忽略,在没有任何提示的情况下。其实本来它的文档中提到了:

This continues until the shortest argument is exhausted.

但是对于大部分开发者来说没人真的能注意这个文档说明,这个问题是很隐性的,需要开发者了解 zip 的问题。而新的版本在参数内元素长度不同时,使用strict=True会报错:

In : list(zip(['a', 'b', 'c'], [1, 2], strict=True))
---------------------------------------------------------------------------
ValueError                                Traceback (most recent call last)
Input In , in <cell line: 1>()
----> 1 list(zip(['a', 'b', 'c'], [1, 2], strict=True))

ValueError: zip() argument 2 is shorter than argument 1

模块新增特性

这小节介绍一些模块新增的特性,分析其用途和用法。

itertools.pairwise

itertools.pairwise是把一个可迭代对象中的元素按照顺序两个挨着的元素放一组的形式返回迭代器。

在之前的版本中可以使用 more-itertools 这个包,现在已经把它加入到标准库了。

这么用:

In : list(pairwise([1, 2, 3, 4, 5]))
Out: [(1, 2), (2, 3), (3, 4), (4, 5)]

In : list(pairwise('ABCDE'))
Out: [('A', 'B'), ('B', 'C'), ('C', 'D'), ('D', 'E')]

如果还不理解可以看函数文档:

Return an iterator of overlapping pairs taken from the input iterator. s -> (s0,s1), (s1,s2), (s2, s3), ...

contextlib.aclosing

在之前已经一个标准的contextlib.closing装饰器,而contextlib.aclosing其实就是 async 的版本罢了。

我们先了解下contextlib.closing的作用。如官方介绍,它其实的作用是实现下面的逻辑:

f = <module>.open(<arguments>)
try:
    <block>
finally:
    f.close()

使用 Python 读写文件一个好的习惯是操作完成后应该要关闭文件句柄,内置的 open 配合 with 是一个很好地实践:

In : with open('new.txt', 'w') as f:
...:     f.write('A\n')
...:

它的原理是在块代码执行完成后,会通过调用ff.__exit__自动关闭文件句柄:

In : ff = open('new.txt', 'w')

In : ff.closed
Out: False

In : ff.__exit__()  # with就会调用它

In : ff.closed
Out: True

但是这里要注意,不是所有的 open 方法都原生支持 with (或者说提供__exit__以及__enter__这个方法),当然还有其他类型的操作也需要被确保关闭 (即便发生了异常)。

事实上,一般标准库或者知名项目都会支持 with 语法,但是当自己写或者公司项目里就可能没有了,例如我这样定义一个类:

In : class Database:
...:     def close(self):
...:         print('Closed')
...:

In : with Database() as d:
...:     ...
...:
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-23-f0ea085f7488> in <module>
----> 1 with Database() as d:
      2     ...
      3

AttributeError: __enter__

这样就抛错了。所以可以使用contextlib.closing包装一下:

In : import contextlib

In : with contextlib.closing(Database()) as d:
...:     ...
...:
Closed

contextlib.aclosing其实就是 asyncio 的版本:

from contextlib import aclosing

async with aclosing(my_generator()) as values:
    async for value in values:
        if value == 42:
            break

contextlib.AsyncContextDecorator

在 Python3.2 时加入了contextlib.ContextDecorator,它是contextlib.contextmanager的基础,如类名的表达,它的作用让上下文管理器能用作装饰器。举个例子就能理解了,下面是一个最基本的、不带参数的上下文管理器:

class mycontext:
    def __enter__(self):
        print('Starting')
        return self

    def __exit__(self, *exc):
        print('Finishing')
        return False

所以可以使用 with 语法这么用:

In : with mycontext():
...:     print('Inner')
...:
Starting
Inner
Finishing

如果你想把这个上下文的管理改成装饰器怎么办呢?只需要让 mycontext 继承contextlib.ContextDecorator这个基类就可以了:

In : class mycontext(contextlib.ContextDecorator):  # 注意这里继承
...:     def __enter__(self):
...:         print('Starting')
...:         return self
...:
...:     def __exit__(self, *exc):
...:         print('Finishing')
...:         return False
...:

In : @mycontext()
...: def p():
...:     print('Inner')
...:

In : p()
Starting
Inner
Finishing

是不是很方便?所以contextlib.AsyncContextDecorator其实就是 asyncio 的版本:

class mycontext(AsyncContextDecorator):
    async def __aenter__(self):
        print('Starting')
        return self

    async def __aexit__(self, *exc):
        print('Finishing')
        return False

延伸阅读

  1. https://docs.python.org/3/whatsnew/3.10.html#better-error-messages
  2. https://realpython.com/python310-new-features/
  3. https://pymotw.com/3/contextlib/
  4. https://peps.python.org/pep-0634/
  5. https://peps.python.org/pep-0635/
  6. https://peps.python.org/pep-0636/