Python 3.10中的新改变介绍((附代码实例)

136 阅读5分钟

Python 3.10有什么变化,其中哪些变化对你很重要?

这周我一直在玩Python 3.10。 我主要是在解决Python Morsels练习的问题,这些练习包含了Python 3.10的新特性。 我想分享我的发现。

通过改进的错误信息更容易排除故障

到目前为止,Python 3.10最大的改进是与改进的错误信息有关的。 我经常打错字,能够帮助我快速找出问题所在的错误信息非常重要。

我已经习惯了破译许多Python中比较神秘的错误信息的过程。 因此,尽管改进的错误信息我来说很好,但这个变化对新的Python学习者尤其重要。

当我教授Python入门课程时,我帮助人们调试的一些最常见的错误是:

  1. 在代码块的结尾处缺少冒号
  2. 代码块中缺少缩进或缩进不正确
  3. 变量名称拼写错误
  4. 没有闭合的大括号和小括号

Python 3.10 使所有这些错误 (以及更多) 对 Python 学习者来说更加清晰。

新的Python用户经常忘记在他们的代码块的开头加上一个: 。在Python 3.9中,用户会看到这个神秘的错误信息:

$ python3.9 temp.py 70
  File "/home/trey/temp.py", line 4
    if temperature < 65
                       ^
SyntaxError: invalid syntax

而Python 3.10则让这一点变得更加清晰:

$ python3.10 temp.py 70
  File "/home/trey/temp.py", line 4
    if temperature < 65
                       ^
SyntaxError: expected ':'

缩进错误也更清楚了(那个after 'if' statement on line 4 是新的):

$ python3.10 temp.py 70
  File "/home/trey/temp.py", line 5
    print("Too cold")
    ^
IndentationError: expected an indented block after 'if' statement on line 4

不正确的变量和属性名称现在显示了一个建议:

$ python3.10 temp.py 70
Traceback (most recent call last):
  File "/home/trey/temp.py", line 4, in <module>
    if temparature < 65:
NameError: name 'temparature' is not defined. Did you mean: 'temperature'?

我对这一点非常兴奋,因为我几乎每天都会在变量名中犯错。

对于未封闭的大括号、小括号和小括号显示的错误信息也有帮助。

Python 以前会显示未封闭的大括号后的一行代码:

$ python3.9 temp.py 70
  File "/home/trey/temp.py", line 6
    elif temperature > 80:
    ^
SyntaxError: invalid syntax

现在它转而指向未闭合的大括号的开头:

$ python3.10 temp.py 70
  File "/home/trey/temp.py", line 5
    print("Too cold"
         ^
SyntaxError: '(' was never closed

你可以在 "Python 3.10 的新特性 "文档的更好的错误信息部分找到关于这些改进的错误信息的更多细节。

虽然 Python 3.10 确实包括了其他的变化 (如果你感兴趣,请继续阅读),但这些改进的错误信息是所有 Python 用户都会注意到的 3.10 改进。

IDLE 在视觉上更加一致

这里还有一个影响到Python新用户的特性:IDLE的外观有了一些改进。 IDLE现在使用空格来缩进,而不是制表符(与内置的REPL不同),在REPL延续行前面的熟悉的... ,现在出现在IDLE的侧边栏内。

以前IDLE看起来像这样:

现在IDLE看起来像这样:

看起来更像命令提示符上的Python REPL,对吗?

zip函数的长度检查

有一个叫做strict_zip 的 Python Morsels 练习。它现在变成了一个 "重新实现这个已经内置的功能 "的练习。对于学习zip 是如何实现的仍然有用,但不再是有用的日常代码。

为什么没有用呢?因为zip 现在接受一个strict 的参数!所以如果你正在处理可能是不同长度但不应该是不同长度的迭代表,现在建议在使用zip时传递strict=True

结构模式匹配

每个人都在谈论的 Python 3.10 大特性是结构模式匹配。 这个特性非常强大,但对大多数 Python 用户来说可能不是很重要。

关于这个特性的一个重要说明:matchcase 仍然是可允许的变量名,所以你现有的所有代码应该继续工作(它们是软关键字)。

匹配iterable的形状和内容

你可以把新的match/case 语句看成是元组解包,不仅仅是长度检查

比较一下这个来自Django模板标签的代码片断。

args = token.split_contents()
if len(args) != 5 or args[1] != 'for' or args[3] != 'as':
    raise TemplateSyntaxError("'%s' requires 'for string as variable' (got %r)" % (args[0], args[1:]))
return GetLanguageInfoNode(parser.compile_filter(args[2]), args[4])

与使用结构模式匹配重构后的相同代码片段:

match token.split_contents():
    case [name, "for", code, "as", info]:
        return GetLanguageInfoNode(parser.compile_filter(code), info)
    case [name, *rest]:
        raise TemplateSyntaxError(f"'{name}' requires 'for string as variable' (got {rest!r})")

请注意,第二种方法允许我们描述我们要解压的变量的数量和要解压的名称(就像元组解压一样),同时也将第二和第三个值与字符串foras 进行匹配。如果这些字符串没有出现在预期的位置,我们会引发一个适当的异常。

结构模式匹配对于实现简单的解析器来说真的很方便,比如Django的模板语言。 我很期待在2025年看到Django重构的模板代码(在Python 3.9支持结束后)。

复杂的类型检查

结构模式匹配在类型检查方面也很出色。 强类型检查在Python中通常是不被鼓励的,但它确实时常出现。

我最常看到isinstance 检查的地方是在操作符重载dunder方法中 (__eq__,__lt__,__add__,__sub__, 等等)。我已经升级了一些Python Morsels解决方案,以比较和对比match-caseisinstance ,我发现它在某些情况下更加冗长,但偶尔也有些清晰。

例如这个代码片段(同样来自Django):

if isinstance(value, str):  # Handle strings first for performance reasons.
    return value
elif isinstance(value, bool):  # Make sure booleans don't get treated as numbers
    return str(value)
elif isinstance(value, (decimal.Decimal, float, int)):
    if use_l10n is False:
        return str(value)
    return number_format(value, use_l10n=use_l10n)
elif isinstance(value, datetime.datetime):
    return date_format(value, 'DATETIME_FORMAT', use_l10n=use_l10n)
elif isinstance(value, datetime.date):
    return date_format(value, use_l10n=use_l10n)
elif isinstance(value, datetime.time):
    return time_format(value, 'TIME_FORMAT', use_l10n=use_l10n)
return value

可以用这个代码片断代替:

match value:
    case str():  # Handle strings first for performance reasons.
        return value
    case bool():  # Make sure booleans don't get treated as numbers
        return str(value)
    case decimal.Decimal() | float() | int():
        if use_l10n is False:
            return str(value)
        return number_format(value, use_l10n=use_l10n)
    case datetime.datetime():
        return date_format(value, 'DATETIME_FORMAT', use_l10n=use_l10n)
    case datetime.date():
        return date_format(value, use_l10n=use_l10n)
    case datetime.time():
        return time_format(value, 'TIME_FORMAT', use_l10n=use_l10n)
    case _:
        return value

请注意每个条件都要短得多。这种case 语法确实需要一些时间来适应,但我确实发现在像这样的长的isinstance 链中,它更容易阅读。

用一个键进行分割

Python 的bisect 模块对于在一个排序的列表中快速找到一个项目非常方便。

对我来说,bisect 模块主要是提醒我,我很少需要关心我在计算机科学课上学到的二进制搜索算法。但对于那些你确实需要在一个排序列表中找到一个项目的时候,bisect 是很好的。

从 Python 3.10 开始,bisect 模块中的所有二进制搜索助手现在都接受一个key 参数。所以你现在可以在一个无大小写排序的字符串列表中快速搜索你要找的字符串:

>>> fruits = sorted(['Watermelon','loquat', 'Apple', 'jujube'], key=str.lower)
>>> fruits
['Apple', 'jujube', 'loquat', 'Watermelon']
>>> import bisect
>>> bisect.insort(fruits, 'Lemon', key=str.lower)
>>> fruits
['Apple', 'jujube', 'Lemon', 'loquat', 'Watermelon']
>>> i = bisect.bisect(fruits, 'lime', key=str.lower)
>>> fruits[i] == 'lime'
False
>>> fruits[i]
'loquat'

在 Python 3.10 之前,做一个涉及key 函数的搜索是非常棘手的

数据类的插槽

有一个数据类(尤其是一个冻结的数据类),想让它更节省内存? 你可以添加一个__slots__ 属性,但你需要自己打出所有的字段名:

from dataclasses import dataclass

@dataclass
class Point:
    __slots__ = ('x', 'y')
    x: float
    y: float

在 Python 3.10 中,你现在可以用slots=True 来代替:

from dataclasses import dataclass

@dataclass(slots=True)
class Point:
    x: float
    y: float

这个特性实际上包含在最初的数据类实现中,但在Python 3.7发布之前被删除了(如果用户表示有兴趣,Guido建议在以后的Python版本中包含它,我们也这样做了)。

创建一个手动添加了__slots__ 的数据类将不允许使用默认字段值,这就是为什么slots=True 如此方便。不过slots=True 有一个非常小的怪癖:当使用slots=True 时,super 的调用会中断,因为这会导致一个新的类对象被创建,从而破坏super的魔力。但除非你在一个冻结的数据类的__post_init__ 方法中调用super().__setattr__ 而不是调用object.__setattr__ ,否则这个怪癖可能不会影响你。

类型注解的改进

如果你使用类型注解,现在使用| 操作符(除了typing.Union ),类型联合就更容易了。类型注解方面的其他重大改进包括参数规范变量类型别名用户定义的类型保护。 我仍然不经常使用类型注解,但这些特性对那些使用类型注解的 Python 开发人员来说是个大问题。

另外,如果你要自省注解,建议调用inspect.get_annotations 函数而不是直接访问__annotations__ 或调用typing.get_type_hints 函数。

检查默认文件的编码问题

现在你也可以要求Python在你没有指定明确的文件编码时发出警告 (这在编写跨操作系统兼容的代码时是非常重要的)。

只要用-X warn_default_encoding 运行Python,如果你在打开文件时没有指定编码,你就会看到一个响亮的错误信息:

$ python3.10 -X warn_default_encoding count_lines.py declaration-of-independence.txt
/home/trey/count_lines.py:3: EncodingWarning: 'encoding' argument not specified
  with open(sys.argv[1]) as f:
67

另外还有很多

上面的变化是我在上周更新Python Morsels练习时发现的主要变化。 不过Python 3.10中还有很多变化。

下面是我研究的另外一些东西,并打算以后再玩:

  • 仅有关键字的数据类字段
  • fileinput.input (方便处理标准输入文件)函数现在接受一个encoding 参数。
  • importlib 弃用:我的一些动态模块导入代码使用了在Python 3.10中已经弃用的功能(如果你的代码也需要更新,你会注意到明显的弃用警告)。
  • 字典视图现在有一个mapping 属性:如果你正在制作你自己的类似字典的对象,你可能也应该为你的keys/values/items 视图添加一个mapping 属性 (这在将来的 Python Morsels 练习中肯定会出现)
  • 当在一个with 块中使用多个上下文管理器时,现在可以用圆括号把它们包到下一行 (这实际上是在 Python 3.9 中添加的,但不是正式的)
  • 标准库模块和内置模块的名称现在包含在sys.stdlib_module_namessys.builtin_module_names 中:我偶尔需要动态地区分第三方模块和标准库模块,这让我的工作变得更加容易。
  • sys.orig_argv 包括命令行参数的完整列表(包括 Python 解释器和传递给它的所有参数),这在检查你的 Python 进程是如何启动的或用同样的参数重新启动你的 Python 进程时可能很有用。

总结

结构模式匹配很好,其他各种语法、标准库和内置程序的改进也很好。 但到目前为止,最大的改进是新的错误信息。

你知道比 Python 3.10 中的新错误更棒的消息是什么吗?Python 3.11 将包括更好的错误信息