按照 Python3.7 的发布时间表,明天 Python 3.7.0 就发布了,最近各大开源项目都在做 3.7 相关的调整,之后我还会写文章更详细的介绍 Python 3.7 都带来了什么,敬请关注!Python 3.7 是一个比较中庸的版本,我比较关注的是 PEP 557 和 本文要提到的 PEP 562

PEP557 是 Data Classes,之前我已经在 attrs 和 Python3.7 的 dataclasses 里面专门介绍了。

PEP 562 主要做的是什么呢?

Customization of Access to Module Attributes

就是能在模块下定义__getattr__和__dir__方法,实现定制访问模块属性了。有什么用呢?其实官网已经给出了答案:

  1. 弃用某些属性 / 函数
  2. 懒加载 (lazy loading)

__getattr__让模块属性的访问非常灵活,我分别举几个例子:

弃用某些属性 / 函数时

有时候会修改一些函数或者属性,会写新的版本,旧版本的在一段时间之后会弃用。在大型项目中调用者有很多,不了解业务挨处修改成本很高,通常会在旧版本的函数中加入 DeprecationWarning,有 3 个问题:

  1. 使用新的属性是没法提示 DeprecationWarning,只能在模块级别加 warn
  2. 如果找不到属性 / 函数直接抛错误了,不能做特殊处理
  3. 模块下弃用的函数多了,只能在每个函数内部加入 warn,再执行新函数逻辑

而 Python 3.7 就没这个问题了:

# lib.py

import warnings

warnings.filterwarnings('default')  # Python 3.2开始默认会隐藏DeprecationWarning


def new_function(arg, other):
    print('plz use me!')


_deprecated_map = {
    'old_function': new_function
}


def __getattr__(name):
    if name in _deprecated_map:
        switch_to = _deprecated_map[name]
        warnings.warn(f'{name} is deprecated. Switch to {__name__}.{switch_to.__name__}.',
             DeprecationWarning)
        return switch_to
    raise AttributeError(f"module {__name__} has no attribute {name}")

看一下效果吧:

>>> from lib import old_function
/Users/dongwm/test/lib.py:18: DeprecationWarning: old_function is deprecated. Switch to lib.new_function.
  DeprecationWarning)
>>> old_function
<function new_function at 0x10ad30f28>
>>> old_function(1, 2)
plz use me!

懒加载

懒加载是指从一个数据对象通过方法获得里面的一个属性对象时,这个对应对象实际并没有随其父数据对象创建时一起保存在运行空间中,而是在其读取方法第一次被调用时才从其他数据源中加载到运行空间中,这样可以避免过早地导入过大的数据对象但并没有使用的空间占用浪费。

简单地说,按需才加载。这是一种设计模式。

Python3.7 之前想要 import 模块成功,就得在模块里面把相关属性 / 函数 / 类等都准备好,其实 import 模块时候是很重的,现在可以通过 PEP 562,能够极大的提升 import 的效率,尤其是导入了很重的逻辑。就如 PEP 中提的例子:

# lib/__init__.py

import importlib

__all__ = ['submod', ...]

def __getattr__(name):
    if name in __all__:
        return importlib.import_module("." + name, __name__)
    raise AttributeError(f"module {__name__!r} has no attribute {name!r}")

# lib/submod.py

print("Submodule loaded")


class HeavyClass:
    ...

# main.py

import lib
lib.submodule.HeavyClass  # prints "Submodule loaded"

可以看到,import lib的时候,HeavyClass 还没有没有加载的,当第一次使用 lib.submodule 的时候才会加载。

在标准库里面也有应用,比如 bpo-32596 中对 concurrent.futures 模块的修改:

def __dir__():
    return __all__ + ('__author__', '__doc__')


def __getattr__(name):
    global ProcessPoolExecutor, ThreadPoolExecutor

    if name == 'ProcessPoolExecutor':
        from .process import ProcessPoolExecutor as pe
        ProcessPoolExecutor = pe
        return pe

    if name == 'ThreadPoolExecutor':
        from .thread import ThreadPoolExecutor as te
        ThreadPoolExecutor = te
        return te

    raise AttributeError(f"module {__name__} has no attribute {name}")

这样还可以让import asyncio时可以快 15%。