Python 3.11 你应该试试的超酷特性:一、更明确的错误回溯信息

Python 3.11 你应该试试的超酷特性

Python 3.11在10月24日发布。它是Python最新版本,运行速度更快且更佳友好。在经过17个月的研发,终于到了可以使用的黄金时期。

和每个发布版本一样,Python 3.11做了大量的变更及提高。你可以通过查看文档来了解大部分内容。这里,我们会阐述最酷且最有影响力的新特性。

教程里将涵盖如下内容:

  • 更好的错误信息来帮助代码追踪
  • Faster CPython项目加速代码执行
  • 在异步代码中使用Task和exception groups
  • Python静态类型新增的几个类型特性
  • 原生支持TOML格式配置文件

如果你想运行本教程所提供的示例文件,你需要先安装Python 3.11.

更明确的错误回溯信息

Python通常被认为对新手友好的编程语言,原于它具备可读性语法强,内置大量的数据结构。对于大部分编码人员的挑战,尤其是新手来说,在Python遇到错误时该如何更好的显示信息来辅助追溯。

在Python 3.10中,错误信息有很强的提高。同样,Python 3.11最期待的特性,它极大的提高了开发体验。通过在回溯中添加信息注解来辅助你快速去识别错误信息。

我们通过例子来快速体验一下回溯改善,通过添加如下代码至inverse.py文件:

# inverse.py


def inverse(number: int) -> int:
    return 1 / number


print(inverse(0))

这里使用inverse()来获取一个整数类型的倒数。0不存在倒数,所以在执行该代码我们将获得如下异常:

$ python inverse.py 
Traceback (most recent call last):
  File "/home/aidan/workbench/projects/new-features/inverse.py", line 8, in <module>
    print(inverse(0))
          ^^^^^^^^^^
  File "/home/aidan/workbench/projects/new-features/inverse.py", line 5, in inverse
    return 1 / number
           ~~^~~~~~~~
ZeroDivisionError: division by zero

注意在回溯中嵌入了^和~符号。这里注意到实际引发错误的代码。和其它回溯一样,我们应该从下往上来溯源。本例子中,1/number引发了ZeroDivisionError异常。罪魁祸首是调用了inverse(0)这个方法,0不存在倒数。

这里添加的辅助信息对于明确错误非常有用。甚至,在代码更复杂时,它的价值越是突显。这在之前在错误回溯中是看不到的。

为了进一步体会到回溯改善所带来的价值,我们尝试构建一个开发者信息解析器。假定我们有一个名为programmers.json文件,它包含如下内容

[
    {"name": {"first": "Uncle Barry"}},
    {
        "name": {"first": "Ada", "last": "Lovelace"},
        "birth": {"year": 1815},
        "death": {"month": 11, "day": 27}
    },
    {
        "name": {"first": "Grace", "last": "Hopper"},
        "birth": {"year": 1906, "month": 12, "day": 9},
        "death": {"year": 1992, "month": 1, "day": 1}
    },
    {
        "name": {"first": "Ole-Johan", "last": "Dahl"},
        "birth": {"year": 1931, "month": 10, "day": 12},
        "death": {"year": 2002, "month": 6, "day": 29}
    },
    {
        "name": {"first": "Guido", "last": "Van Rossum"},
        "birth": {"year": 1956, "month": 1, "day": 31},
        "death": null
    }
]

注意到我们程序员信息格式并不统一。Grace Hopper和Ole-Johan Dhl具有完整信息,Ada Lovelace的出生和逝世信息都有不同的缺失。Guido van Rossum仅包含出生信息。Uncle Barry仅包含它的名字。

我们将创建一个类来包装这些信息。先从JSON文件读取信息:

# programmers.py

import json
import pathlib

programmers = json.loads(
    pathlib.Path("programmers.json").read_text(encoding="utf-8")
)

我们使用pathlib来读取JSON文件,使用json将内容解析为包含字典内容的Python的列表。
接下来,我们使用data class来包装每个程序员的信息

from dataclasses import dataclass

#...

@dataclass
class Person:
    name: str
    life_span: tuple[int, int]

    @classmethod
    def from_dict(cls, info):
        return cls(
            name=f"{info['name']['first']} {info['name']['last']}",
            life_span=(info["birth"]["year"], info["death"]["year"]),
        )

每个Person包含一个name和life_span属性。例外,我们添加一个构造函数用于基于JSON结构化的数据来初始化Person。

我们还添加一个函数,用来一次初始化两个Person对象。

#programmers.py

# ...
def convert_pair(first, second):
    return Person.from_dict(first), Person.from_dict(second)

convert_pair()函数使用.from_dict()两次从JSON结构化数据转换为一对Person对象。

现在可以执行代码,尤其来看看回溯内容。通过使用-i参数来执行Python程序,它将运行程序后保持一个Python交互环境,它环境还带有执行后所包含变量,类和方法。

$ python -i programmers.py 
>>> Person.from_dict(programmers[2])
Person(name='Grace Hopper', life_span=(1906, 1992))

Grace的信息是完整,所以我们可以成功将它包装具有name和life span的Person对象。

要看看回溯情况,我们来试着转换Uncle Barry:


>>> programmers[0]
{'name': {'first': 'Uncle Barry'}}
>>> Person.from_dict(programmers[0])
Traceback (most recent call last):
  File "", line 1, in <module>
  File "/home/aidan/workbench/projects/new-features/programmers.py", line 18, in from_dict
    name=f"{info['name']['first']} {info['name']['last']}",
                                    ~~~~~~~~~~~~^^^^^^^^
KeyError: 'last'

因为键缺失,我们会得到一个KeyError,你可能记得last是name的子项,注解会快速指出问题。

同样,因为Ada也是不完整的。也不能将他转换为Person对象


>>> programmers[1]
{
    'name': {'first': 'Ada', 'last': 'Lovelace'},
    'birth': {'year': 1815},
    'death': {'month': 11, 'day': 27}
}

>>> Person.from_dict(programmers[1])
Traceback (most recent call last):
  File "/home/realpython/programmers.py", line 18, in from_dict
    life_span=(info["birth"]["year"], info["death"]["year"]),
                                      ~~~~~~~~~~~~~^^^^^^^^
KeyError: 'year'

同样这里抛出了KeyError,这次是因为year键的缺失。该例子甚至要比上一个例子更加有用。我们有两个year子项,一个是birth下,一个是death下。回溯里的注解快速指出了缺失为death的year项。

那Guido会发生什么?我们仅包含他的出生信息。


>>> programmers[4]
{
    'name': {'first': 'Guido', 'last': 'Van Rossum'},
    'birth': {'year': 1956, 'month': 1, 'day': 31},
    'death': None
}

>>> Person.from_dict(programmers[4])
Traceback (most recent call last):
  File "/home/realpython/programmers.py", line 18, in from_dict
    life_span=(info["birth"]["year"], info["death"]["year"]),
                                      ~~~~~~~~~~~~~^^^^^^^^
TypeError: 'NoneType' object is not subscriptable

本例子中,TypeError抛出。你可能之前没有见过NoneType类型错误。众所周知它非常难以debug,因为我们不知道具体哪个对象是None导致。但是,从注解中,我们可以清晰的看到info["death"]是None。

最后一个例子,我们来看看嵌套函数调用会发生什么。还记得我们convert_pair()会调用两次Person.from_dict()。我们试试来解开Ada和Ole-Johan:


>>> convert_pair(programmers[3], programmers[1])
Traceback (most recent call last):
  File "/home/realpython/programmers.py", line 24, in convert_pair
    return Person.from_dict(first), Person.from_dict(second)
                                    ^^^^^^^^^^^^^^^^^^^^^^^^
  File "/home/realpython/programmers.py", line 18, in from_dict
    life_span=(info["birth"]["year"], info["death"]["year"]),
                                      ~~~~~~~~~~~~~^^^^^^^^
KeyError: 'year'

我们在之前已经知道尝试包装Ada时将引发KeyError异常。然后,在convert_pair()中,因为函数内执行了两次.from_dict(),通常我们需要做更多的努力才能确定具体是first还是second处理时引发的异常。而在当前,Python立马就能指出问题反生在second。

这些回溯信息使得Python 3.11中更容易做代码调试。你可以查看文章学习到更多关于Python 3.11调试内容。更多技术细节,可以查看PEP 657。

回溯注解将极大提高Python开发者生产力。一个更振奋人心的信息是Python 3.11是至今最快的Python版本。

你可能感兴趣的:(python3.11,python,开发语言)