您现在的位置是:首页> 编程文章 个人博客模板
Python 3.7 的一些新特性
无痕小Q个人博客 2020-02-05 22:33:36【 主页】 1669人已围观
简介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
要让这个工作,你要先安装好 pudb
(pip 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.py
中 breakpoint()
那行改为:
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=True
,Country
类就可以被排序:
>>> 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__()
包含下列的步骤。以下列表中的编号对应代码注释中的编号:
- 首先,该函数乐观地从
PLUGINS
字典中返回命名的插件。如果名为name
的插件存并且已经导入,这将成功返回。 - 如果这个插件在
PLUGINS
字典中没有找到,我们将确保导入所有插件。 - 如果在导入之后该插件变得可用了,那就成功返回。
- 如果在导入之后还是没有找到这个插件,我们将引发
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
并未添加到保留关键字列表。换句话说,仍然可以定义名为 async
和 await
的变量或者函数。
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.resources
。self
列显示不包括嵌套导入的导入时间。
你现在可以使用 -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 协议,如果我们的工作有侵犯到您的权益,请及时联系我们。
阅读量! (1669)
点击排行
网站公告
- 欢迎来到我的博客
1:欢迎来到我的博客
2:博客免费api接口现已上线
3:博客会定期更新文章
4:欢迎大家来捧场
站点信息
- 建站时间:2019-8-30
- 网站程序:php,laravel-swoole框架
- 今日流量:612(10分钟统计一次)
- 本月流量:9331
- 浏览总量:388258
- 统计方式:中间件,redis消息队列,定时任务