Python 3.7 的一些新特性

file

Python 3.7 发布 正式版本啦! 新版本的Python 从 2016年 九月开发至今,现在我们终于能享受核心开发者辛勤劳动的成果了。

新版本的 Python 将带来什么? 这篇 文档 可以让我们很好的了解Python3.7 中出现的新特性,也会深入带我们了解以下一些有价值的内容:

  • 通过 breakpoint()  可以更容易的访问调试器
  • 使用data classes模块创建一个简单类
  • 支持模块属性的定制化访问
  • 改进了对类型提示的支持
  • 更高精度的时间表示

更重要的是,Python 3.7 更快了!

在本文的结尾部分,你会看到 Python 3.7 中一些非常酷的关于速度提升的新特性,同时也会得到一些关于升级到新版本的建议。

内置的“断点” breakpoint() 

我们总是想努力的写出完美的代码,但往往会有一些纰漏。所以调试器成了代码编写过程中重要的一部分。 Python 3.7 中增加了新的内置函数 breakpoint()。 这并没有为 Python 增加任何新的功能,但它使得调试器更加的直观和灵活。

假如你有类似下文 bugs.py 中的错误代码:

def divide(e, f):
    return f / e

a, b = 0, 1
print(divide(a, b))

运行这段代码会导致  divide() 函数产生 ZeroDivisionError 错误。假设你想在divide()的开头中断代码并直接进入 调试器 。你可以在代码中打上一个所谓的“断点”:

def divide(e, f):
    # Insert breakpoint here
    return f / e

断点(breakpoint)是一个让程序执行暂时中止的信号,好让你可以观察程序当前的状态。那么如何添加断点呢?在 Python 3.6 及之前的版本中是通过一行神秘的代码来实现的:

def divide(e, f):
    import pdb; pdb.set_trace()
    return f / e

在这里,pdb 是 Python 标准库中的调试器。在 Python 3.7 中,你可以调用新的 breakpoint() 函数作为快捷方法:

def divide(e, f):
    breakpoint()
    return f / e

breakpoint() 会在后台首先导入 pdb 然后帮你调用 pdb.set_trace()。显而易见的好处是 breakpoint() 更容易记住,只需要打 12 个字符,而原来需要 27个。 而它真正的优势是 breakpoint() 可自定义。

运行包含 breakpoint()bugs.py 脚本:

$ python3.7 bugs.py
> /home/gahjelle/bugs.py(3)divide()
-> return f / e
(Pdb)

当脚本运行到 breakpoint() 的位置时会中断, 进入一个 PDB 的调试会话。你可以敲 c 然后回车使脚本继续。如果你想学习更多 PDB和调试的知识,可参阅 Nathan Jennings 的 PDB 指引

现在假设你已经修正了这个 bug,你希望再跑一遍这个脚本但不会中止并进入调试模式。你当然可以注释掉 breakpoint() 这一行,但另一种方法是使用 PYTHONBREAKPOINT 环境变量。这个变量控制breakpoint() 的行为, 把 PYTHONBREAKPOINT 置成 0 会忽略所有 breakpoint() 的调用:

$ PYTHONBREAKPOINT=0 python3.7 bugs.py
ZeroDivisionError: division by zero

哎呀,好像你还没修好这个 bug。

还有个方法是用 PYTHONBREAKPOINT 来指定一个 PDB 以外的调试器。比如要使用 PuDB(一个终端中的可视化调试器),你可以这样:

$ PYTHONBREAKPOINT=pudb.set_trace python3.7 bugs.py

要让这个工作,你要先安装好 pudbpip install pudb)。 Python 会帮你导入 pudb。通过这个方法你也可以设置默认调试器,只要将 PYTHONBREAKPOINT 环境变量设成你喜欢的调试器就可以了。 阅读这篇指南来学习如何在系统中设置环境变量。

新 breakpoint() 函数不仅对调试器有用。一个很方便的选项是在代码中启动一个交互式执行环境。例如,要启动一个 IPython 会话,你可以这样:

$ PYTHONBREAKPOINT=IPython.embed python3.7 bugs.py
IPython 6.3.1 -- An enhanced Interactive Python. Type '?' for help.

In [1]: print(e / f)
0.0

你也可以自己编写函数让 breakpoint() 来调用它。下面的代码会打印本地作用域的所有变量。将它添加到 bp_utils.py 文件中:

from pprint import pprint
import sys

def print_locals():
    caller = sys._getframe(1)  # Caller is 1 frame up.
    pprint(caller.f_locals)

要使用这个函数,就像之前一样,把 PYTHONBREAKPOINT 设成 <module>.<function> 的表示:

$ PYTHONBREAKPOINT=bp_utils.print_locals python3.7 bugs.py
{'e': 0, 'f': 1}
ZeroDivisionError: division by zero

一般 breakpoint() 用来调用不需要参数的函数或方法,但其实也可以传入参数。 把 bugs.pybreakpoint() 那行改为:

breakpoint(e, f, end="<-END\n")

注意:默认的 PDB 调试器会在这行抛出 TypeError, 因为 pdb.set_trace() 不接受任何参数。

运行这个把 breakpoint() 伪装成了 print() 函数的代码,作为一个给函数传入参数的示例:

$ PYTHONBREAKPOINT=print python3.7 bugs.py
0 1<-END
ZeroDivisionError: division by zero

参阅 PEP 553 与breakpoint() 的文档,以及 sys.breakpointhook() 来获取更多信息。

数据类

新的 dataclasses 模块让你在编写你自己的类时更加方便,如 .__init__(), .__repr__(), 和 .__eq__() 会被自动添加。 使用 @dataclass 装饰器,你可以写出这样的代码:

from dataclasses import dataclass, field

@dataclass(order=True)
class Country:
    name: str
    population: int
    area: float = field(repr=False, compare=False)
    coastline: float = 0

    def beach_per_person(self):
        """每人平均海岸线长度"""
        return (self.coastline * 1000) / self.population

这9行代码代表了相当多的样板代码和最佳实践。想想要把 Country 类作为一个常规类来实现需要哪些工作:__init__() 方法,一个 repr,6个不同的比较方法还有 beach_per_person 方法。你可以扩展下方的代码块来证明 Country 的实现大致相当于数据类:

“Country” 类的另一种实现,显示/隐藏
此处原文代码如下,如不需要,请告知我删除

class Country:

    def __init__(self, name, population, area, coastline=0):
        self.name = name
        self.population = population
        self.area = area
        self.coastline = coastline

    def __repr__(self):
        return (
            f"Country(name={self.name!r}, population={self.population!r},"
            f" coastline={self.coastline!r})"
        )

    def __eq__(self, other):
        if other.__class__ is self.__class__:
            return (
                (self.name, self.population, self.coastline)
                == (other.name, other.population, other.coastline)
            )
        return NotImplemented

    def __ne__(self, other):
        if other.__class__ is self.__class__:
            return (
                (self.name, self.population, self.coastline)
                != (other.name, other.population, other.coastline)
            )
        return NotImplemented

    def __lt__(self, other):
        if other.__class__ is self.__class__:
            return ((self.name, self.population, self.coastline) < (
                other.name, other.population, other.coastline
            ))
        return NotImplemented

    def __le__(self, other):
        if other.__class__ is self.__class__:
            return ((self.name, self.population, self.coastline) <= (
                other.name, other.population, other.coastline
            ))
        return NotImplemented

    def __gt__(self, other):
        if other.__class__ is self.__class__:
            return ((self.name, self.population, self.coastline) > (
                other.name, other.population, other.coastline
            ))
        return NotImplemented

    def __ge__(self, other):
        if other.__class__ is self.__class__:
            return ((self.name, self.population, self.coastline) >= (
                other.name, other.population, other.coastline
            ))
        return NotImplemented

    def beach_per_person(self):
        """Meters of coastline per person"""
        return (self.coastline * 1000) / self.population

创建后,一个数据类是一个普通的类。比如说你可以以常规方式继承一个数据类。数据类的主要目的是更加快速简单地编写健壮的类,特别是那些主要用来存储数据的小型类。

你可以像使用其他类一样使用 Country :

>>> norway = Country("Norway", 5320045, 323802, 58133)
>>> norway
Country(name='Norway', population=5320045, coastline=58133)

>>> norway.area
323802

>>> usa = Country("United States", 326625791, 9833517, 19924)
>>> nepal = Country("Nepal", 29384297, 147181)
>>> nepal
Country(name='Nepal', population=29384297, coastline=0)

>>> usa.beach_per_person()
0.06099946957342386

>>> norway.beach_per_person()
10.927163210085629

请注意所有的域 .name, .population, .area, 和 .coastline 会在初始化类时被使用 (尽管 .coastline 是可选的, 如 landlocked Nepal 例子所示)。  在定义与其他常规类方法功能相同的函数时,Country 类有一个合理的 repr

默认,数据类能做相等性比较。一旦我们在 @dataclass 装饰器中指定 order=TrueCountry 类就可以被排序:

>>> norway == norway
True

>>> nepal == usa
False

>>> sorted((norway, usa, nepal))
[Country(name='Nepal', population=29384297, coastline=0),
Country(name='Norway', population=5320045, coastline=58133),
Country(name='United States', population=326625791, coastline=19924)]

排序是在字段值上进行,首先是 .name 接着是 .population, 等等。然而,如果你使用 field(),你可以 自定义 被用来比较的字段。在这个例子中 .area 字段在 repr 被省略。

注:国家数据来自于 2017 年 7 月 CIA World Factbook 的人口估计。

在你们预订挪威的下一个海滩假期之前,这里有关于 挪威气候 的Factbook:“温带沿海,随着北大西洋海流改边;内陆较冷,降水增加,夏季较冷;西海岸常年有雨。”

数据类与 命名元组做了一些相同的事情。然而,他们从 attrs 项目 中吸取了最大的灵感。有关更多示例和更多信息,请参阅我们的 数据类的完整指南,以及 PEP 557 的官方描述。

自定义模块属性

Python 中处处都有属性!虽然类属性很常见,但是事实上,属性可以被任意对象所拥有,包括函数和模块。 Python 的一些基本特性使用属性来实现,包括大部分的内置函数,文档字符串以及命名空间等。模块内的函数可以被视作模块属性。

检索对象的属性时经常使用点来表示: 对象.属性 。但是,你也可以使用 getattr() 函数来获取对象运行时属性。

import random

random_attr = random.choice(("gammavariate", "lognormvariate", "normalvariate"))
random_func = getattr(random, random_attr)

print(f"A {random_attr} random value: {random_func(1, 1)}")

运行这段代码将会输出如下信息:

A gammavariate random value: 2.8017715125270618

对于类而言,当调用 对象.属性 时,解释器将会第一时间查找该对象 是否有定义该 属性 。如果没有找到,则会调用特定的方法 对象.__getattr__("attr") (以上时简化说明,详细请参考 这篇文章)。 __getattr__() 方法可以用于自定义访问对象的属性。

在 Python 3.7 之前,要实现相同效果的模块属性定制并非这么简单。但是, PEP 562 文档中介绍了如何使用 __getattr__()__dir__() 函数达到相同效果。 __dir__() 可以自定义 在模块中调用 dir() 时返回的结果。

PEP 上有几个样例展示以上函数的用法。包括如何给函数添加警告,以及如何实现子模块的延迟加载等。以下我们介绍一个简单的插件系统,可以实现动态地向模块添加函数。该样例需要导入 Python 库。如果你需要更新这些库,请参考 这篇文章

创建一个新的 plugins 目录,然后在该目录下创建 plugins/__init__.py 文件:

from importlib import import_module
from importlib import resources

PLUGINS = dict()

def register_plugin(func):
    """注册插件的装饰器"""
    name = func.__name__
    PLUGINS[name] = func
    return func

def __getattr__(name):
    """返回对应名称的插件"""
    try:
        return PLUGINS[name]
    except KeyError:
        _import_plugins()
        if name in PLUGINS:
            return PLUGINS[name]
        else:
            raise AttributeError(
                f"module {__name__!r} has no attribute {name!r}"
            ) from None

def __dir__():
    """返回可用插件列表"""
    _import_plugins()
    return list(PLUGINS.keys())

def _import_plugins():
    """导入所有资源来注册插件"""
    for name in resources.contents(__name__):
        if name.endswith(".py"):
            import_module(f"{__name__}.{name[:-3]}")

在我们看这块代码做了什么之前,我们在 plugins 目录下再添加两个文件。首先,我们来看看 plugins/plugin_1.py:

from . import register_plugin

@register_plugin
def hello_1():
    print("Hello from Plugin 1")

接下来,添加相同的代码在  plugins/plugin_2.py 文件中:

from . import register_plugin

@register_plugin
def hello_2():
    print("Hello from Plugin 2")

@register_plugin
def goodbye():
    print("Plugin 2 says goodbye")

这些插件现在可以被按照下面的方式应用:

>>> import plugins
>>> plugins.hello_1()
Hello from Plugin 1

>>> dir(plugins)
['goodbye', 'hello_1', 'hello_2']

>>> plugins.goodbye()
Plugin 2 says goodbye

这看起来好像并不是什么革命性的技术,但是让我们来看看这里实际上发生了什么。正常情况下,为了能够调用 plugins.hello_1()hello_1() 函数必须定义在 plugins 模块或者显示地导入到 plugins 包的 __init__.py 文件中。但是这里再也不是这样了。

而这里,hello_1() 被定义在 plugins 包的任意一个文件中,通过使用  @register_plugin装饰器 将它自己注册成为 plugins 包的一部分。

差异很微妙,各个函数将自己注册成为包的一部分,而不是由包来检测哪些函数是可用的。这将给你提供了一个简单的结构,可以独立于其余代码添加函数,而不用在包里面集中保留一个可用函数列表。

让我们快速看下  plugins/__init__.py 文件中的  __getattr__() 做了什么。当你请求访问 plugins.hello_1() 时,Python 首先在  plugins/__init__.py  文件中查找一个名为 hello_1() 的函数。由于这个函数不存在,Python 调用了  __getattr__("hello_1") 。回顾下  __getattr__() 函数的源代码:

def __getattr__(name):
    """Return a named plugin"""
    try:
        return PLUGINS[name]        # 1) 尝试返回插件
    except KeyError:
        _import_plugins()           # 2)  导入所有的插件
        if name in PLUGINS:
            return PLUGINS[name]    # 3) 在此尝试返回插件
        else:
            raise AttributeError(   # 4) 引发错误
                f"module {__name__!r} has no attribute {name!r}"
            ) from None

__getattr__()  包含下列的步骤。以下列表中的编号对应代码注释中的编号:

  1. 首先,该函数乐观地从  PLUGINS 字典中返回命名的插件。如果名为 name 的插件存并且已经导入,这将成功返回。
  2. 如果这个插件在  PLUGINS 字典中没有找到,我们将确保导入所有插件。
  3. 如果在导入之后该插件变得可用了,那就成功返回。
  4. 如果在导入之后还是没有找到这个插件,我们将引发 AttributeError 错误,告诉调用者 name 不是当前模块一个可用的属性(插件)。

PLUGINS 字典是怎样填充的呢?_import_plugins() 函数导入了 plugins 包所有的 Python 文件,但是它好像并没有改变 PLUGINS

def _import_plugins():
    """Import all resources to register plug-ins"""
    for name in resources.contents(__name__):
        if name.endswith(".py"):
            import_module(f"{__name__}.{name[:-3]}")

不要忘记每个插件函数都被 @register_plugin 装饰器装饰过。装饰器当插件被导入时调用,并且在这个时候实际填充 PLUGINS 字典。如果你手动导入其中一个插件就会看到以下信息:

>>> import plugins
>>> plugins.PLUGINS
{}

>>> import plugins.plugin_1
>>> plugins.PLUGINS
{'hello_1': <function hello_1 at 0x7f29d4341598>}

我们继续看这个例子,在模块上调用 dir() 也将导入剩余的插件:

>>> dir(plugins)
['goodbye', 'hello_1', 'hello_2']

>>> plugins.PLUGINS
{'hello_1': <function hello_1 at 0x7f29d4341598>,
'hello_2': <function hello_2 at 0x7f29d4341620>,
'goodbye': <function goodbye at 0x7f29d43416a8>}

dir() 通常用于列出一个对象所有可用的属性。通常情况下,在一个模块上使用 dir() 将会导致如下所示的结果:

>>> import plugins
>>> dir(plugins)
['PLUGINS', '__builtins__', '__cached__', '__doc__',
'__file__', '__getattr__', '__loader__', '__name__',
'__package__', '__path__', '__spec__', '_import_plugins',
'import_module', 'register_plugin', 'resources']

虽然这些是可用的信息,但是我们可能更感兴趣于暴露的有用插件。Python 3.7 中,你可以通过 __dir__() 特殊函数自定义在模块上调用 dir() 函数的结果。对于 plugins/__init__.py,这个函数首先确保所有的插件被导入然后列出它们的名称:

def __dir__():
    """List available plug-ins"""
    _import_plugins()
    return list(PLUGINS.keys())

在最后结束这个例子之前,我们将用 Python 3.7 中另一个牛逼的功能。为了导入 plugins 目录所有的模块,我们可以用新的  importlib.resources  模块。这个模块可以访问模块和包内所有的文件和资源,而不用 __file__ 这种 hack 方式(并不总是很有效)或者 pkg_resources (太慢)。importlib.resources 其他功能将被 后续介绍

类型提示的强化

在整个 Python 3 系列的发行版中,类型提示和注解都一直在不断发展。Python 的 类型系统 现在十分健壮。尽管如此,Python 3.7 仍然带来了一些增强功能,更好的性能,核心支持以及前向引用。

Python 在运行时没有做任何类型检查(除非你故意使用了像enforce 这样的包)。因此,添加类型不会影响代码性能。

不幸的是,并不总是这样,因为大多数类型提示需要 typing 模块。而typing 模块是标准库中  最慢的模块 之一。在 Python 3.7 中,PEP 560  为类型添加了核心支持,它能够有效地提升 typing 模块的速度。一般来说这个的详细细节是没必要去了解的。只需要简单地回顾下并且享受这个性能提升。

虽然 Python 的类型系统具有很强的表现力,但是导致非常痛苦的一个问题是前向引用。类型提示,或者更通用的注解,它们是在模块被导入时进行计算。因此所有的名称必须已经在他们使用之前被定义。下面这段代码就是不对的:

class Tree:
    def __init__(self, left: Tree, right: Tree) -> None:
        self.left = left
        self.right = right

运行这段代码将会引发 NameError,因为类 Tree.__init__() 方法定义的时候还没完成定义:

Traceback (most recent call last):
  File "tree.py", line 1, in <module>
    class Tree:
  File "tree.py", line 2, in Tree
    def __init__(self, left: Tree, right: Tree) -> None:
NameError: name 'Tree' is not defined

为了避免这个,你需要将 "Tree" 作为一个字符串:

class Tree:
    def __init__(self, left: "Tree", right: "Tree") -> None:
        self.left = left
        self.right = right

可以在 PEP 484 中看最初的讨论。

在未来的 Python 4.0 中,这种所谓的前向引用将被允许。这将通过在明确要求之前不计算注解来处理,PEP 563 对这个目的做了详细的描述。Python 3.7 中,前向引用已经可以通过 __future__ import 使用。现在你可以这样写了:

from __future__ import annotations

class Tree:
    def __init__(self, left: Tree, right: Tree) -> None:
        self.left = left
        self.right = right

记着这里除了避免了看起来有点怪怪的 "Tree" 语法之外,对注解的延迟计算也会加快代码的速度,因为类型提示不会被执行。前向引用已经被  mypy 支持。

到目前为止,注解通常用作类型提示。尽管如此,您仍可以在运行时完全访问注解,并可以根据需要使用它们。如果直接处理注解,则需要明确处理可能的前向引用。

让我们来看一个有点傻的例子,它展示了注解是何时被求值的。首先我们用经典的风格,这时注解在导入时求值。anno.py包含以下代码:

def greet(name: print("Now!")):
    print(f"Hello {name}")

注意,name 的注解是print()。这只是为了确切地看到注解何时被求值。这时导入此模块:

>>> import anno
Now!

>>> anno.greet.__annotations__
{'name': None}

>>> anno.greet("Alice")
Hello Alice

正如您所看到的,注解在导入时进行了求值。注意到 name 最终的注解是 None,这是因为 print() 的返回值为 None

增加 __future__ 的导入能够延迟求值的注解:

from __future__ import annotations

def greet(name: print("Now!")):
    print(f"Hello {name}")

导入更新后的代码,将不会对注解进行求值:

>>> import anno

>>> anno.greet.__annotations__
{'name': "print('Now!')"}

>>> anno.greet("Marty")
Hello Marty

需要注意到根本没有打印 Now! ,并且 __annotations__ 字典中的注解保存成了字符串字面量。为了对注解求值,需要使用typing.get_type_hints() 或 eval()

>>> import typing
>>> typing.get_type_hints(anno.greet)
Now!
{'name': <class 'NoneType'>}

>>> eval(anno.greet.__annotations__["name"])
Now!

>>> anno.greet.__annotations__
{'name': "print('Now!')"}

我们可以看到 __annotations__ 字典不会更新,因此,每次使用时都需要对注解求值。

时间精度

Python 3.7 中,time 模块增加了一些 PEP 564 描述的新函数。具体上来说,添加了下面6个函数:

  • clock_gettime_ns(): 返回指定时钟时间
  • clock_settime_ns(): 设置指定时钟时间
  • monotonic_ns(): 返回不能倒退的相对时钟的时间(例如由于夏令时)
  • perf_counter_ns(): 返回性能计数器的值,专门用于测量短间隔的时钟
  • process_time_ns(): 返回当前进程系统和用户 CPU 时间的总和(不包括休眠时间)
  • time_ns(): 返回自1970年1月1日以来的纳秒数

从某种意义上来说,没有新函数添加。每个函数都与已经存在的没有 _ns 后缀的函数相似。不同的是,新函数返回的是 int 类型的纳秒数而不是 float 类型的秒数。

对于大多数应用来说,新的纳秒函数和旧的同功能函数之间的差异性不是很明显。然而,新函数更容易应用起来更合理,因为返回 int 而不是 float。浮点数  本质上并不准确

>>> 0.1 + 0.1 + 0.1
0.30000000000000004

>>> 0.1 + 0.1 + 0.1 == 0.3
False

这不是 Python 的问题,而是由于计算机需要使用有限位数表示无限十进制数的后果。

Python 的 float 遵循 IEEE 754 标准 使用了53个有效位。结果是,任何超过104天(2⁵³ 或者超过9千万亿纳秒)不能表示为具有纳秒精度的浮点数。相比之下,Python 的 int 是无限的,所以纳秒整数将始终有纳秒精度而不用考虑时间值。

例如,time.time() 返回了自1970年1月1日以来的秒数。这个数字已经非常巨大,它的精度已经处于微妙级别。这个函数显示了 _ns 版本的最大改, time.time_ns()  的比 time.time() 大约好3倍

顺便说一下,什么是纳秒?技术上来说,它是十亿分之一秒,如果你更新欢科学技术,那就是 1e-9。这些仅仅是数字,实际上并没有提供任何直觉。为了更好地视觉辅助,请参与 Grace Hopper's 精彩的 纳秒演示

顺便说一句,如果你想处理纳秒精度的日期时间, datetime 标准库并没有删除它。它显示只处理微妙:

>>> from datetime import datetime, timedelta
>>> datetime(2018, 6, 27) + timedelta(seconds=1e-6)
datetime.datetime(2018, 6, 27, 0, 0, 0, 1)

>>> datetime(2018, 6, 27) + timedelta(seconds=1e-9)
datetime.datetime(2018, 6, 27, 0, 0)

然而,你可以用  astropy 项目。它的 astropy.time 使用两个 float 对象表示日期时间,保证「跨越宇宙时代的亚纳秒精度」。

>>> from astropy.time import Time, TimeDelta
>>> Time("2018-06-27")
<Time object: scale='utc' format='iso' value=2018-06-27 00:00:00.000>

>>> t = Time("2018-06-27") + TimeDelta(1e-9, format="sec")
>>> (t - Time("2018-06-27")).sec
9.976020010071807e-10

 astropy 最新的版本在 Python 3.5 以及后续版本中都可用。

其他酷炫的功能

到目前为止,你已经看到了 Python 3.7 中新功能头条新闻。然而,这儿还有一些新的酷炫功能。这节,我们将简单的了解下它们。

字典顺序得到保证

Python 3.6 的 CPython 实现了有序字典(PyPy 也有这个)。这意味着字典中元素被迭代顺序同它们被插入的顺序相同。第一个例子是使用 Python 3.5,第二个例子是使用 Python 3.6:

>>> {"one": 1, "two": 2, "three": 3}  # Python <= 3.5
{'three': 3, 'one': 1, 'two': 2}

>>> {"one": 1, "two": 2, "three": 3}  # Python >= 3.6
{'one': 1, 'two': 2, 'three': 3}

Python 3.6 中,这个顺序仅仅是 dict 实现的好结果。然而,在 Python
3.7 中,保留字典的插入顺序是 语言规范的一部分。 因此,现在可以依赖于仅支持 Python >= 3.7 (或者CPython >= 3.6)的项目。

"async" 和 "await" 是关键字

Python 3.5 中介绍了 基于 async 和 await 语法的协程。为了避免向后兼容问题,async  和 await 并未添加到保留关键字列表。换句话说,仍然可以定义名为 asyncawait 的变量或者函数。

Python 3.7 中,再也不可能那样了:

>>> async = 1
  File "<stdin>", line 1
    async = 1
          ^
SyntaxError: invalid syntax

>>> def await():
  File "<stdin>", line 1
    def await():
            ^
SyntaxError: invalid syntax

"asyncio" 重大改进

asyncio 模块被从 Python 3.4 中引入去用事件循环,协程和 futures 的现代化方式处理并发。这里有个详细介绍

Python 3.7 中,asyncio 模块取得了重大进展,包括许多新的函数,支持上下文变量(看这里)以及性能改进。特别值得注意的是 asyncio.run(),它简化了同步代码调用协程。使用 asyncio.run() 你不必再去显示地创建事件循环。一个异步的 Hello World 程序可以这样写了:

import asyncio

async def hello_world():
    print("Hello World!")

asyncio.run(hello_world())

上下文变量

上下文变量是根据其上下文可以具有不同值的变量。它们类似于本地线程存储,一个变量在每个执行线程可能具有不同的变量值。但是,对于上下文变量,在一个执行线程中可能存在多个上下文。上下文变量的主要用例是跟踪并发异步任务中的变量。

下面的示例构造了三个上下文,每个上下文都有自己的 name 值。 greet() 函数在之后的每一个上下文中都可以使用 name 的值:

import contextvars

name = contextvars.ContextVar("name")
contexts = list()

def greet():
    print(f"Hello {name.get()}")

# 构造上下文并设置上下文变量名称
for first_name in ["Steve", "Dina", "Harry"]:
    ctx = contextvars.copy_context()
    ctx.run(name.set, first_name)
    contexts.append(ctx)

# 在每个上下文中运行 greet 函数
for ctx in reversed(contexts):
    ctx.run(greet)

运行此脚本,以相反的顺序与 Steve,Dina和Harry 打招呼:

$ python3.7 context_demo.py
Hello Harry
Hello Dina
Hello Steve

使用 "importlib.resources" 导入数据文件

打包 Python 项目的一个挑战是如何处理项目资源文件,例如项目所需的数据文件。通用的一些处理方式:

  • 硬编码数据文件路径。
  • 把数据文件放在包里面并通过 __file__ 定位。
  • 使用 setuptools.pkg_resources 访问数据文件资源。

这三个方式都有缺点。第一个方式不便于移植。使用 __file__ 具备了可移植性,但是如果 Python 项目被以一个 zip 文件安装,它就没有 __file__ 属性了。第三个方式解决了这写问题,不幸的是太慢了。

更好的解决方案是标准库中的新模块  importlib.resources 。它使用 Python 现有的导入功能导入数据文件。假设你在一个 Python 包里有像下面这样的资源:

data/
│
├── alice_in_wonderland.txt
└── __init__.py

注意的是 data 需要是一个  Python 包。也就是说,这个目录需要包含一个 __init__.py 文件(它可能是空的)。你可以像下面这样读取 alice_in_wonderland.txt 文件:

>>> from importlib import resources
>>> with resources.open_text("data", "alice_in_wonderland.txt") as fid:
...     alice = fid.readlines()
... 
>>> print("".join(alice[:7]))
CHAPTER I. Down the Rabbit-Hole

Alice was beginning to get very tired of sitting by her sister on the
bank, and of having nothing to do: once or twice she had peeped into the
book her sister was reading, but it had no pictures or conversations in
it, 'and what is the use of a book,' thought Alice 'without pictures or
conversations?'

一个相似的函数  resources.open_binary() 用于以二进制模式打开文件。在前面  「插件作为模块属性」的例子中 ,我们用  importlib.resources 中的  resources.contents() 去发现可用的插件。阅读 Barry Warsaw's PyCon 2018 演讲  查看更多信息。

通过 backport 在 Python 2.7 和 Python 3.4 中使用  importlib.resources  已成为可能。 从 pkg_resources  到  importlib.resources 的迁移指导 也已经可用了。

开发者技巧

Python 3.7 添加了几个针对您作为开发人员的功能。你已经看到了新的内建函数 breakpoint() 。此外,Python 解释器中添加了一些新的 -X 命令行选项 。

您可以使用-X importtime 轻松了解脚本中的导入时间:

$ python3.7 -X importtime my_script.py
import time: self [us] | cumulative | imported package
import time: 2607 | 2607 | _frozen_importlib_external
...
import time: 844 | 28866 | importlib.resources
import time: 404 | 30434 | plugins

cumulative 列显示累计导入时间(以微秒为单位)。在这个示例中,导入 plugins 花费了大概 0.03 秒,其中大部分用于导入 importlib.resourcesself 列显示不包括嵌套导入的导入时间。

你现在可以使用 -X dev 来激活“开发者模式”。开发者模式将增加一节 debug 特性和运行时检查,这些功能被认为太慢而无法在默认情况下启用。这些包括启用 faulthandler显示严重崩溃的追溯,以及更多警告和调试钩子。

最后,-X utf8 启用 UTF-8 模式。 (参阅 PEP 540.) 在这个模式下,无论当前的语言环境如何,UTF-8 都将用于文本编码。

优化

每个 Python 新版本都会带来一些优化。Python 3.7 中,这里有一些有意义的加速,包括:

  • 调用标准库中的许多函数将会有更少的开销。
  • 一般而言,方法调用快了 20%。
  • Python 解释器的启动时间减少了 10-30%。
  • 导入 typing 比以前快了 7 倍。

除此之外,还有更多专业的优化。有关详细概述,请看 这个列表 。

这些优化的结果是 Python 3.7 很快。它只是截至目前 CPython 最快的版本 。

所以,我该升级吗?

让我们从简单的答案开始吧,如果你想用这里任何的新功能,你需要去使用 Python 3.7。使用 pyenv 或者 Anaconda 这样的工具可以很容易同时安装多个 Python 版本。安装 Python 3.7 并且使用它你并不吃亏。

现在,对于更复杂的问题,你是否应该将你的生产环境升级到 Python 3.7 ?你是否应该使用 Python 3.7 开发自己的项目而使用它的新功能?

显而易见的警告是,你应该在升级你的生产环境之前对你的代码进行彻底的测试,Python 3.7 很少会破坏之前的代码(async 和 await 成为关键字是一个例子)。如果你已经在使用现代化 Python,升级到 3.7 应该十分平滑。如果你比较保守一点,你应该等一个维护版本的发布--Python 3.7.1--暂定于2018年7月的某个时间

去争论是否应该在你的项目中使用 3.7 很难。Python 3.7 中许多新功能 (数据类,importlib.resources)或者便利性(快速的启动和方法调用,更容易的调试以及 -X 选项)可以反向移植到 Python 3.6。再往后,你可以通过自己运行 Python 3.7,同时保持代码与 Python 3.6(或更低版本)兼容来利用新版本的优势。

将代码锁定到 Python 3.7 的重要功能是  模块上的 __getattr__()类型提示中的前向引用纳秒 time 函数。如果你真需要这些,你应该考虑升级。否则,如果你的项目在 Python 3.6 再运行一段时间对于其他人可能更有用。

有关升级需要的详细信息,请阅读 Python 3.7 移植指南 。

本文中的所有译文仅用于学习和交流目的,转载请务必注明文章译者、出处、和本文链接
我们的翻译工作遵照 CC 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。

原文地址:https://realpython.com/python37-new-feat...

译文地址:https://learnku.com/python/t/22994/some-...

本帖已被设为精华帖!
本文为协同翻译文章,如您发现瑕疵请点击「改进」按钮提交优化建议
讨论数量: 2
Michael001

怎么分享到朋友圈

5年前 评论

讨论应以学习和精进为目的。请勿发布不友善或者负能量的内容,与人为善,比聪明更重要!