Python3.12有哪些新变化?

转载请注明出处❤️

作者:测试蔡坨坨

原文链接:caituotuo.top/1ddf9b8f.html


你好,我是测试蔡坨坨。

距离Python3.11的正式发布已经过去了约一年的时间,而在2023年10月2日,Python迎来了全新版本——Python3.12。按照惯例,本篇我们就来盘一盘Python3.12相对于Python3.11的新增功能和改进,而关于Python3.11的介绍可以在之前的文章「Python3.11 正式版,它来了!」中查看。

Python3.12的发布也意味着距离Python3.14只差两个版本,那时应该称Python为π-thon?(谐音哏扣钱 O(∩_∩)O哈哈~

Improved Error Messages

Improved Error Messages

首先,Python3.12改进的第一件事是错误信息的优化。

例如标准库中的模块会在错误信息中给出明确的提示,如果你使用sys的某项功能,但是没有导入sys模块,它就会在报错信息中直接提示“Did you forget to import ‘sys’?”(虽然是件小事,并且现在的集成开发工具也会给出一些语法错误的提示,但是对于新手同学来说还是比较友好的。

另外,错误信息也更聪明了,例如一些同学经常犯的错误from y import x写成import x from y,错误信息更加明确,并且会给出正确操作建议:

另外一个比较智能的提示就是如果你引用的某个变量属于self,但是忘写self.,名字也会在错误信息中给出明确的提示:

最后,是ImportError的改进,例如导入模块中的某些东西未生效,那么它会建议你做更正,并给出更正建议:

以上错误信息提示都是一些相对较小的改进,但总体来说,它们确实让Python的编码体验更好,对于新手入坑也更加友好。(将来也许可以用AI来生成更加有针对性的错误信息数据库?

性能提升

第二件事就是性能的提升,性能几乎是Python每个版本都会提及的话题。

在Python3.11中,官方表示在性能提升方面是一个巨大的飞跃(官方文档原话:*Python 3.11 is between 10-60% faster than Python 3.10. On average, we measured a 1.25x speedup on the standard benchmark suite. See Faster CPython for details.*)。

而现在从Python3.11到Python3.12差别不大,但还是有几处调整。

Comprehension inlining

PEP 709: Comprehension inlining

其中一项改进是解析式上下文,字典、列表和集合推导式使用内联,而不是为推导式的每次执行创建一个新的一次性函数对象。这可以将解析的执行速度提高两倍。推导式是Python一个很好的功能,允许我们创建一个列表只用几条简单的规则,在实际应用中也非常广泛。

immortal objects

PEP 683 – Immortal Objects, Using a Fixed Refcount

Python3.12中添加的另一个功能immortal objects(不朽对象),原则上使用Python每个对象都有一个引用计数,用于跟踪对象仍在使用,有些对象是用户创建的,随时都有可能不再需要,而有些对象是系统创建,总是被系统需要,这就是immortal objects,所以它们的引用计数从未更新,保持不变。

现在看来,可能是件微不足道的小事,但它可能对将来的Python有重大影响。我们可以用immortal objects表示一个对象是基本上永远不会改变,如果要避免缓存失效(cache invalidations),避免竞态条件(data race),在这种情况下多个代码要修改相同的数据,如果它们是immortal objects,这就不会改变,这样程序就变得简单多了。

A Per-Interpreter GIL

PEP 684: A Per-Interpreter GIL

Python3.12的下一个新特性A Per-Interpreter GIL给每个子解释器创建GIL

那么,什么是GIL?

Global Interpreter Lock 全局解释器锁。

GIL是Python解释器中的全局锁,它是一种机制,确保同一时刻只有一个线程在执行Python代码。在多线程程序中,因为GIL的存在,多线程并不能真正并行执行,而是通过竞争GIL的方式来实现对CPU的占用。这也是为什么Python中的多线程程序并不比单线程程序更快的原因之一。

这实际上是Python工作方式的重大改变,PEP 684 由“香农计划”的作者Eric Snow提出,主要是给每个子解释器创建GIL。这使得Python程序能够充分利用多个CPU内核,允许Python实现真正的并行处理。

但是目前只能通过C-API获得,预计3.13中将提供Python API,将会有一个新模块叫做Stdlib。(通过从每个进程一个GIL过渡到每个子解释器一个GIL,其实是Python一个很大的更新,GIL长久以来广受诟病,但没人提出一个合理的解决方案,现在有了,可以看出Python正在采取措施改善多线程并行性。

Optimizations

文档:https://docs.python.org/3/whatsnew/3.12.html#optimizations

还有一些其他的优化也可能有助于改善性能。例如wstr,从Unicode对象中删除wstr和wstr_length成员。它在64位平台上将对象大小减少了8或16字节。这样就可以节省内存,并有可能避免缓存失误及类似的情况。

f-strings

Python3.12的更新使得f-strings的使用变得不那么挑剔,新版取消了最初制定f-strings时的一些限制(最初设置f-strings限制是为了能够在不修改现有词法分析器的情况下将f-strings的解析实现到CPython中,但目前看来,这些限制反而带来了复杂性)。

Python3.12的这些优化将会为终端用户和库开发者带来较大优势,同时也大大降低用于解析f-strings代码的维护成本。

  • 允许重用相同的引号

    在3.12之前的版本中以下代码是不被允许的,也就是重用与封闭的f-strings相同的引号会引发SyntaxError(如果f-strings使用单引号,则内部的字符串需要使用双引号或三引号),这意味着我们不得不用不同类型的引号,实际上是一个颇为困扰的问题。

    1
    2
    3
    4
    def main() -> None:
    # my_str = f'Hello,{input("Enter your name: ")}!'' # Python3.12之前用法
    my_str = f"Hello,{input("Enter your name: ")}!"
    print(my_str)

    但是现在,这种形式是被允许的,同时也有助于嵌套字符串的运用。虽然是小修小改,但有这个功能还是很不错的。

    嵌套字符串:

    1
    2
    3
    4
    5
    6
    7
    def main() -> None:
    print(f"""{f'''{f'{f"{1 + 1}"}'}'''}""")
    print(f"{f"{f"{f"{f"{f"{1 + 1}"}"}"}"}"}")


    if __name__ == '__main__':
    main()

    (但是这样一旦出错就很难查找引号的匹配问题了?

  • 多行表达式和注释

    在Python3.11中,f-strings表达式必须在单行中定义,而Python3.12支持定义多行的f-strings,并且可以添加内联注释:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    # author: 测试蔡坨坨
    # datetime: 2023/11/5 12:06
    # function: Python3.12

    def main() -> None:
    my_str = f"This is the playlist: {", ".join([
    '《天外来物》', # 你就像天外来物一样,求之不得
    '《陪你去流浪》', # 我会攥着小糖,眺望你方向
    '《小尖尖》' # 我口袋只剩玫瑰一片 此行又山高路远
    ])}"
    print(my_str)


    if __name__ == '__main__':
    main()

  • 支持反斜杠“\”和Unicode字符

    在Python3.12之前,f-strings表达式不能包含任何\字符。这也影响了Unicode转义序列,因为它们包含以前不能成为f-strings表达式组件一部分的“\N”部分。现在Python3.12中f-strings表达式可以包含反斜杠,因此也就可以使用Unicode字符,如下所示:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    # author: 测试蔡坨坨
    # datetime: 2023/11/5 12:06
    # function: Python3.12

    def main() -> None:
    names = ['测试蔡坨坨', '小趴蔡', '薛之谦', 'joker']
    print(f"My name is: {"\n".join(names)}")
    print(f"My name is: {"\N{BLACK HEART SUIT}".join(names)}")


    if __name__ == '__main__':
    main()

TypedDict

PEP 692: Using TypedDict for more precise **kwargs typing

Python3.12引入了TypedDict类型构造函数,支持字符串键可能不同类型的值组成字典类型。

如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
# author: 测试蔡坨坨
# datetime: 2023/11/5 12:06
# function: Python3.12

from typing import TypedDict, Unpack


class Movie(TypedDict):
name: str
year: int


def foo(**kwargs: Unpack[Movie]):
print(type(kwargs))
print(kwargs)


def main() -> None:
movie: dict[str, object] = {"name": "《演员》", "year": 2015}
foo(**movie)

type_movie: Movie = {"name": "《念》", "year": 2023}
foo(**type_movie)


if __name__ == '__main__':
main()

装饰器@override

PEP 698: Override Decorator for Static Typing

Python3.12中还出现的另一个功能,就是新增的装饰器@override关键字,这个功能的作用就是明确指出一个方法覆盖另一个方法,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
from typing import override

class Base:
def get_color(self) -> str:
return "blue"

class GoodChild(Base):
@override # ok: overrides Base.get_color
def get_color(self) -> str:
return "yellow"

class BadChild(Base):
@override # type checker error: does not override Base.get_color
def get_colour(self) -> str:
return "red"

注意同名覆盖,函数名字不同则不会覆盖。

新泛型语法

PEP 695: Type Parameter Syntax

PEP 484 下的泛型类和函数是使用冗长的语法来声明的,这使得类型参数的范围不明确,并且需要显示的声明。

PEP 695 引入了一种新的、更紧凑和明确的方法来创建泛型类和函数。

先举个例子看看效果,例如定义一个向量数据类型:

1
2
3
4
5
6
7
8
9
10
11
12
type Vector = list[float]


def scale[T](scalar: float, vector: Vector) -> Vector:
return [scalar * num for num in vector]


# passes type checking; a list of floats qualifies as a Vector.
new_vector = scale(2.0, [1.0, -4.2, 5.4])

print(new_vector)
# [2.0, -8.4, 10.8]

将以上代码抽象出来,之前定义泛型类的做法如下:

1
2
3
4
5
6
7
from typing import Generic, TypeVar

_T_co = TypeVar("_T_co", covariant=True, bound=str)

class ClassA(Generic[_T_co]):
def method1(self) -> _T_co:
...

而在新语法中,我们只需要如下:

1
2
3
class ClassA[T: str]:
def method1(self) -> T:
...

之前定义泛型函数:

1
2
3
4
5
6
from typing import TypeVar

_T = TypeVar("_T")

def func(a: _T, b: _T) -> _T:
...

新语法定义泛型函数:

1
2
def func[T](a: T, b: T) -> T:
...

在新语法中不再需要TypeVar类型强制,只需要用[T]指定,与其他一些编程语言类似。

另一个例子,定义泛型类别名的例子,之前的语法:

1
2
3
4
5
from typing import TypeAlias

_T = TypeVar("_T")

ListOrSet: TypeAlias = list[_T] | set[_T]

新语法使用type语句声明一个类型别名,即typing.TypeAliasType的实例:

1
type ListOrSet[T] = list[T] | set[T]

其他

另外Python3.12还有一些小的改动,例如:pathlib新增了pathlib.Path.walk()方法,用于遍历目录树并生成其中的所有文件和目录名称,类似os.walk();CPython支持监控调用的功能,用于监视CPython中的事件,包括调用、返回、异常等;pdb添加方便的变量来临时保存调试会话的值,并提供对当前帧和返回值等值的快速访问(pdb其实就是Python官方提供的命令行debugger工具,在以后的文章中再做详细介绍,这里就不展开了)……

最后就是Python3.12中移除了多个已经弃用的模块,比如asynchat和asyncore已经被asyncio取代;distutils被删除,因为它在Python3.10已经被弃用,而现在通常会使用setuptools来替代;还有一些单元测试unittest中的一些长期弃用的内容也被删除了,不过也不用担心,因为有些方法你可能见都没见过。(更多被删除的内容可以参考官方文档:https://docs.python.org/3/whatsnew/3.12.html#removed

以上就是我认为Python3.12一些比较有意思的更新点,更多更新内容可参考官方文档:https://docs.python.org/3/whatsnew/3.12.html