流畅的 Python 第二版(GPT 重译)(十三)(3)https://developer.aliyun.com/article/1485191
示例 24-20。将evaldemo_meta.py作为程序运行
$ ./evaldemo_meta.py [... 20 lines omitted ...] @ deco(<class 'Klass' built by MetaKlass>) # ① @ Builder.__init__(<Klass instance>) # Klass.__init__(<Klass instance>) @ SuperA.__init_subclass__:inner_0(<Klass instance>) @ deco:inner_1(<Klass instance>) % MetaKlass.__new__:inner_2(<Klass instance>) # ② @ Descriptor.__set__(<Descriptor instance>, <Klass instance>, 999) # evaldemo_meta module end
①
前 21 行,包括这一行,与示例 24-19 中显示的相同。
②
由main
中的obj.method_c()
触发;method_c
是由MetaKlass.__new__
注入的。
现在让我们回到Checked
类的概念,其中Field
描述符实现了运行时类型验证,并看看如何使用元类来实现。
用于 Checked 的元类解决方案
我不想鼓励过早优化和过度设计,所以这里有一个虚构的场景来证明使用__slots__
重写checkedlib.py的合理性,这需要应用元类。随意跳过。
我们接下来将研究的metaclass/checkedlib.py模块是initsub/checkedlib.py的一个可替换项。它们中嵌入的 doctests 是相同的,以及用于 pytest 的checkedlib_test.py 文件。
checkedlib.py中的复杂性对用户进行了抽象。这里是使用该包的脚本的源代码:
from checkedlib import Checked class Movie(Checked): title: str year: int box_office: float if __name__ == '__main__': movie = Movie(title='The Godfather', year=1972, box_office=137) print(movie) print(movie.title)
这个简洁的Movie
类定义利用了三个Field
验证描述符的实例,一个__slots__
配置,从Checked
继承的五个方法,以及一个元类将它们全部整合在一起。checkedlib
的唯一可见部分是Checked
基类。
考虑图 24-4。 Mills & Gizmos Notation 通过使类和实例之间的关系更加可见来补充 UML 类图。
例如,使用新的checkedlib.py的Movie
类是CheckedMeta
的一个实例,并且是Checked
的一个子类。此外,Movie
的title
、year
和box_office
类属性是Field
的三个单独实例。每个Movie
实例都有自己的_title
、_year
和_box_office
属性,用于存储相应字段的值。
现在让我们从Field
类开始研究代码,如示例 24-21 所示。
Field
描述符类现在有点不同。在先前的示例中,每个Field
描述符实例将其值存储在具有相同名称的属性中。例如,在Movie
类中,title
描述符将字段值存储在托管实例中的title
属性中。这使得Field
不需要提供__get__
方法。
然而,当类像Movie
一样使用__slots__
时,不能同时拥有相同名称的类属性和实例属性。每个描述符实例都是一个类属性,现在我们需要单独的每个实例存储属性。代码使用带有单个_
前缀的描述符名称。因此,Field
实例有单独的name
和storage_name
属性,并且我们实现Field.__get__
。
图 24-4。带有 MGN 注释的 UML 类图:CheckedMeta
元工厂构建Movie
工厂。Field
工厂构建title
、year
和box_office
描述符,它们是Movie
的类属性。字段的每个实例数据存储在Movie
的_title
、_year
和_box_office
实例属性中。请注意checkedlib
的包边界。Movie
的开发者不需要理解checkedlib.py内部的所有机制。
示例 24-21 显示了带有storage_name
和__get__
的Field
描述符的源代码。
示例 24-21。元类/checkedlib.py:带有storage_name
和__get__
的Field
描述符
class Field: def __init__(self, name: str, constructor: Callable) -> None: if not callable(constructor) or constructor is type(None): raise TypeError(f'{name!r} type hint must be callable') self.name = name self.storage_name = '_' + name # ① self.constructor = constructor def __get__(self, instance, owner=None): if instance is None: # ② return self return getattr(instance, self.storage_name) # ③ def __set__(self, instance: Any, value: Any) -> None: if value is ...: value = self.constructor() else: try: value = self.constructor(value) except (TypeError, ValueError) as e: type_name = self.constructor.__name__ msg = f'{value!r} is not compatible with {self.name}:{type_name}' raise TypeError(msg) from e setattr(instance, self.storage_name, value) # ④
①
从name
参数计算storage_name
。
②
如果__get__
的instance
参数为None
,则描述符是从托管类本身而不是托管实例中读取的。因此我们返回描述符。
③
否则,返回存储在名为storage_name
的属性中的值。
④
__set__
现在使用setattr
来设置或更新托管属性。
示例 24-22 显示了驱动此示例的元类的代码。
示例 24-22。元类/checkedlib.py:CheckedMeta
元类
class CheckedMeta(type): def __new__(meta_cls, cls_name, bases, cls_dict): # ① if '__slots__' not in cls_dict: # ② slots = [] type_hints = cls_dict.get('__annotations__', {}) # ③ for name, constructor in type_hints.items(): # ④ field = Field(name, constructor) # ⑤ cls_dict[name] = field # ⑥ slots.append(field.storage_name) # ⑦ cls_dict['__slots__'] = slots # ⑧ return super().__new__( meta_cls, cls_name, bases, cls_dict) # ⑨
①
__new__
是CheckedMeta
中唯一实现的方法。
②
仅在cls_dict
不包含__slots__
时增强类。如果__slots__
已经存在,则假定它是Checked
基类,而不是用户定义的子类,并按原样构建类。
③
为了获取之前示例中的类型提示,我们使用typing.get_type_hints
,但这需要一个现有的类作为第一个参数。此时,我们正在配置的类尚不存在,因此我们需要直接从cls_dict
(Python 作为元类__new__
的最后一个参数传递的正在构建的类的命名空间)中检索__annotations__
。
④
迭代type_hints
以…
⑤
…为每个注释属性构建一个Field
…
⑥
…用Field
实例覆盖cls_dict
中的相应条目…
⑦
…并将字段的storage_name
追加到我们将用于的列表中…
⑧
…填充cls_dict
中的__slots__
条目——正在构建的类的命名空间。
⑨
最后,我们调用super().__new__
。
metaclass/checkedlib.py的最后部分是Checked
基类,这个库的用户将从中派生类来增强他们的类,如Movie
。
这个版本的Checked
的代码与initsub/checkedlib.py中的Checked
相同(在示例 24-5 和示例 24-6 中列出),有三个变化:
- 添加一个空的
__slots__
,以向CheckedMeta.__new__
表明这个类不需要特殊处理。 - 移除
__init_subclass__
。它的工作现在由CheckedMeta.__new__
完成。 - 移除
__setattr__
。它变得多余,因为向用户定义的类添加__slots__
可以防止设置未声明的属性。
示例 24-23 是Checked
的最终版本的完整列表。
示例 24-23。元类/checkedlib.py:Checked
基类
class Checked(metaclass=CheckedMeta): __slots__ = () # skip CheckedMeta.__new__ processing @classmethod def _fields(cls) -> dict[str, type]: return get_type_hints(cls) def __init__(self, **kwargs: Any) -> None: for name in self._fields(): value = kwargs.pop(name, ...) setattr(self, name, value) if kwargs: self.__flag_unknown_attrs(*kwargs) def __flag_unknown_attrs(self, *names: str) -> NoReturn: plural = 's' if len(names) > 1 else '' extra = ', '.join(f'{name!r}' for name in names) cls_name = repr(self.__class__.__name__) raise AttributeError(f'{cls_name} object has no attribute{plural} {extra}') def _asdict(self) -> dict[str, Any]: return { name: getattr(self, name) for name, attr in self.__class__.__dict__.items() if isinstance(attr, Field) } def __repr__(self) -> str: kwargs = ', '.join( f'{key}={value!r}' for key, value in self._asdict().items() ) return f'{self.__class__.__name__}({kwargs})'
这结束了一个带有验证描述符的类构建器的第三次渲染。
下一节涵盖了与元类相关的一些一般问题。
真实世界中的元类
元类很强大,但也很棘手。在决定实现元类之前,请考虑以下几点。
现代特性简化或替代元类
随着时间的推移,几种常见的元类用法被新的语言特性所取代:
类装饰器
比元类更容易理解,更不太可能与基类和元类发生冲突。
__set_name__
避免需要自定义元类逻辑来自动设置描述符的名称。¹⁵
__init_subclass__
提供了一种透明对终端用户进行自定义类创建的方式,甚至比装饰器更简单——但可能会在复杂的类层次结构中引入冲突。
内置dict
保留键插入顺序
消除了使用__prepare__
的#1 原因:提供一个OrderedDict
来存储正在构建的类的命名空间。Python 只在元类上调用__prepare__
,因此如果您需要按照源代码中出现的顺序处理类命名空间,则必须在 Python 3.6 之前使用元类。
截至 2021 年,CPython 的每个活跃维护版本都支持刚才列出的所有功能。
我一直在倡导这些特性,因为我看到我们行业中有太多不必要的复杂性,而元类是复杂性的入口。
元类是稳定的语言特性
元类是在 2002 年与所谓的“新式类”、描述符和属性一起在 Python 2.2 中引入的。
令人惊讶的是,Alex Martelli 于 2002 年 7 月首次发布的MetaBunch
示例在 Python 3.9 中仍然有效——唯一的变化是在 Python 3 中指定要使用的元类的方式,即使用语法class Bunch(metaclass=MetaBunch):
。
我提到的“现代特性简化或替代元类”中的任何添加都不会破坏使用元类的现有代码。但是,使用元类的遗留代码通常可以通过利用这些特性来简化,尤其是如果可以放弃对不再维护的 Python 版本(3.6 之前的版本)的支持。
一个类只能有一个元类
如果您的类声明涉及两个或更多个元类,您将看到这个令人困惑的错误消息:
TypeError: metaclass conflict: the metaclass of a derived class must be a (non-strict) subclass of the metaclasses of all its bases
这可能发生在没有多重继承的情况下。例如,像这样的声明可能触发TypeError
:
class Record(abc.ABC, metaclass=PersistentMeta): pass
我们看到abc.ABC
是abc.ABCMeta
元类的一个实例。如果Persistent
元类本身不是abc.ABCMeta
的子类,则会出现元类冲突。
处理该错误有两种方法:
- 找到其他方法来做你需要做的事情,同时避免涉及到的元类之一。
- 编写自己的
PersistentABCMeta
元类,作为abc.ABCMeta
和PersistentMeta
的子类,使用多重继承,并将其作为Record
的唯一元类。¹⁶
提示
我可以想象实现满足截止日期的两个基本元类的元类的解决方案。根据我的经验,元类编程总是比预期时间长,这使得在严格的截止日期之前采用这种方法是有风险的。如果您这样做并且达到了截止日期,代码可能会包含微妙的错误。即使没有已知的错误,您也应该将这种方法视为技术债务,因为它很难理解和维护。
元类应该是实现细节
除了type
,整个 Python 3.9 标准库中只有六个元类。较为知名的元类可能是abc.ABCMeta
、typing.NamedTupleMeta
和enum.EnumMeta
。它们中没有一个旨在明确出现在用户代码中。我们可能将它们视为实现细节。
尽管您可以使用元类进行一些非常古怪的元编程,但最好遵循最少惊讶原则,以便大多数用户确实将元类视为实现细节。¹⁷
近年来,Python 标准库中的一些元类已被其他机制替换,而不会破坏其包的公共 API。未来保护这类 API 的最简单方法是提供一个常规类,供用户子类化以访问元类提供的功能,就像我们在示例中所做的那样。
为了总结我们对类元编程的覆盖范围,我将与您分享我在研究本章时发现的最酷、最小的元类示例。
使用 prepare 的元类技巧
当我为第二版更新这一章节时,我需要找到简单但具有启发性的示例来替换自 Python 3.6 以来不再需要元类的bulkfood LineItem
代码。
最简单且最有趣的元类概念是由巴西 Python 社区中更为人熟知的 João S. O. Bueno(简称 JS)给我的。他的想法之一是创建一个自动生成数值常量的类:
>>> class Flavor(AutoConst): ... banana ... coconut ... vanilla ... >>> Flavor.vanilla 2 >>> Flavor.banana, Flavor.coconut (0, 1)
是的,代码如图所示是有效的!实际上,这是autoconst_demo.py中的一个 doctest。
这里是用户友好的AutoConst
基类和其背后的元类,实现在autoconst.py中:
class AutoConstMeta(type): def __prepare__(name, bases, **kwargs): return WilyDict() class AutoConst(metaclass=AutoConstMeta): pass
就是这样。
显然,技巧在于WilyDict
。
当 Python 处理用户类的命名空间并读取banana
时,它在__prepare__
提供的映射中查找该名称:一个WilyDict
的实例。WilyDict
实现了__missing__
,在“missing 方法”中有介绍。WilyDict
实例最初没有'banana'
键,因此触发了__missing__
方法。它会即时创建一个具有键'banana'
和值0
的项目,并返回该值。Python 对此很满意,然后尝试检索'coconut'
。WilyDict
立即添加该条目,值为1
,并返回它。同样的情况也发生在'vanilla'
,然后映射到2
。
我们之前已经看到了__prepare__
和__missing__
。真正的创新在于 JS 如何将它们结合在一起。
这里是WilyDict
的源代码,也来自autoconst.py:
class WilyDict(dict): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.__next_value = 0 def __missing__(self, key): if key.startswith('__') and key.endswith('__'): raise KeyError(key) self[key] = value = self.__next_value self.__next_value += 1 return value
在实验过程中,我发现 Python 在正在构建的类的命名空间中查找__name__
,导致WilyDict
添加了一个__name__
条目,并增加了__next_value
。因此,我在__missing__
中添加了那个if
语句,以便为看起来像 dunder 属性的键引发KeyError
。
autoconst.py包既需要又展示了对 Python 动态类构建机制的掌握。
我很高兴为AutoConstMeta
和AutoConst
添加更多功能,但是我不会分享我的实验,而是让您享受 JS 的巧妙技巧。
以下是一些想法:
- 如果你有值,可以检索常量名称。例如,
Flavor[2]
可以返回'vanilla'
。您可以通过在AutoConstMeta
中实现__getitem__
来实现这一点。自 Python 3.9 起,您可以在AutoConst
本身中实现__class_getitem__
。 - 支持对类进行迭代,通过在元类上实现
__iter__
。我会让__iter__
产生常量作为(name, value)
对。 - 实现一个新的
Enum
变体。这将是一项重大工作,因为enum
包中充满了技巧,包括具有数百行代码和非平凡__prepare__
方法的EnumMeta
元类。
尽情享受!
注意
__class_getitem__
特殊方法是在 Python 3.9 中添加的,以支持通用类型,作为PEP 585—标准集合中的类型提示通用的一部分。由于__class_getitem__
,Python 的核心开发人员不必为内置类型编写新的元类来实现__getitem__
,以便我们可以编写像list[int]
这样的通用类型提示。这是一个狭窄的功能,但代表了元类的更广泛用例:实现运算符和其他特殊方法以在类级别工作,例如使类本身可迭代,就像Enum
子类一样。
总结
元类、类装饰器以及__init_subclass__
对以下方面很有用:
在某些情况下,类元编程也可以帮助解决性能问题,通过在导入时执行通常在运行时重复执行的任务。
最后,让我们回顾一下亚历克斯·马特利在他的文章“水禽和 ABC”中的最终建议:
而且,不要在生产代码中定义自定义 ABCs(或元类)…如果你有这种冲动,我敢打赌这很可能是“所有问题看起来都像钉子”综合症的情况,对于刚刚得到闪亮新锤子的人来说,你(以及未来维护你代码的人)将更喜欢坚持简单直接的代码,避免深入这样的领域。
我相信马特利的建议不仅适用于 ABCs 和元类,还适用于类层次结构、运算符重载、函数装饰器、描述符、类装饰器以及使用__init_subclass__
的类构建器。
这些强大的工具主要用于支持库和框架开发。应用程序自然应该使用这些工具,如 Python 标准库或外部包提供的那样。但在应用程序代码中实现它们通常是过早的抽象。
好的框架是被提取出来的,而不是被发明的。¹⁸
大卫·海涅迈尔·汉森,Ruby on Rails 的创始人
章节总结
本章以类对象中发现的属性概述开始,比如__qualname__
和__subclasses__()
方法。接下来,我们看到了type
内置函数如何用于在运行时构建类。
引入了__init_subclass__
特殊方法,设计了第一个旨在用Field
实例替换用户定义子类中属性类型提示的Checked
基类,这些实例应用构造函数以在运行时强制执行这些属性的类型。
通过一个@checked
类装饰器实现了相同的想法,它为用户定义的类添加特性,类似于__init_subclass__
允许的。我们看到,无论是__init_subclass__
还是类装饰器都无法动态配置__slots__
,因为它们只在类创建后操作。
“导入时间”和“运行时”概念通过实验证明了涉及模块、描述符、类装饰器和__init_subclass__
时 Python 代码执行顺序的清晰。
我们对元类的覆盖始于对type
作为元类的整体解释,以及用户定义的元类如何实现__new__
以自定义它构建的类。然后我们看到了我们的第一个自定义元类,经典的MetaBunch
示例使用__slots__
。接下来,另一个评估时间实验展示了元类的__prepare__
和__new__
方法在__init_subclass__
和类装饰器之前被调用,为更深层次的类定制提供了机会。
第三次迭代的Checked
类构建器使用Field
描述符和自定义__slots__
配置,随后是关于实践中元类使用的一些一般考虑。
最后,我们看到了由乔昂·S·O·布恩诺发明的AutoConst
黑客,基于一个具有__prepare__
返回实现__missing__
的映射的元类的狡猾想法。在不到 20 行的代码中,autoconst.py展示了结合 Python 元编程技术的强大力量。
我还没有找到一种语言能像 Python 一样,既适合初学者,又适合专业人士,又能激发黑客的兴趣。感谢 Guido van Rossum 和所有使之如此的人。
进一步阅读
Caleb Hattingh——本书的技术审阅者——编写了autoslot包,提供了一个元类,通过检查__init__
的字节码并找到对self
属性的所有赋值来自动创建一个__slots__
属性在用户定义的类中。这非常有用,也是一个优秀的学习示例:autoslot.py中只有 74 行代码,包括 20 行注释解释最困难的部分。
本章在 Python 文档中的基本参考资料是Python 语言参考中“数据模型”章节中的“3.3.3. 自定义类创建”,涵盖了__init_subclass__
和元类。在“内置函数”页面的type
类文档,以及Python 标准库中“内置类型”章节的“4.13. 特殊属性”也是必读的。
在Python 标准库中,types
模块文档涵盖了 Python 3.3 中添加的两个简化类元编程的函数:types.new_class
和types.prepare_class
。
类装饰器在PEP 3129—类装饰器中得到正式规范,由 Collin Winter 编写,参考实现由 Jack Diederich 编写。PyCon 2009 年的演讲“类装饰器:彻底简化”(视频),也是由 Jack Diederich 主持,是该功能的一个快速介绍。除了@dataclass
之外,在 Python 标准库中一个有趣且简单得多的类装饰器示例是functools.total_ordering
,它为对象比较生成特殊方法。
对于元类,在 Python 文档中的主要参考资料是PEP 3115—Python 3000 中的元类,其中引入了__prepare__
特殊方法。
Python 速查手册,第 3 版,由 Alex Martelli、Anna Ravenscroft 和 Steve Holden 编写,权威性很高,但是在PEP 487—简化类创建发布之前编写。该书中的主要元类示例——MetaBunch
——仍然有效,因为它不能用更简单的机制编写。Brett Slatkin 的Effective Python,第 2 版(Addison-Wesley)包含了几个关于类构建技术的最新示例,包括元类。
要了解 Python 中类元编程的起源,我推荐 Guido van Rossum 在 2003 年的论文“统一 Python 2.2 中的类型和类”。该文本也适用于现代 Python,因为它涵盖了当时称为“新式”类语义的内容——Python 3 中的默认语义,包括描述符和元类。Guido 引用的参考文献之一是Ira R. Forman 和 Scott H. Danforth的Putting Metaclasses to Work: a New Dimension in Object-Oriented Programming(Addison-Wesley),这本书在Amazon.com上获得了五星评价,他在评论中补充说:
这本书为 Python 2.2 中的元类设计做出了贡献
真遗憾这本书已经绝版;我一直认为这是我所知道的关于协同多重继承这一困难主题的最佳教程,通过 Python 的
super()
函数支持。¹⁹
如果你对元编程感兴趣,你可能希望 Python 拥有终极的元编程特性:语法宏,就像 Lisp 系列语言以及最近的 Elixir 和 Rust 所提供的那样。语法宏比 C 语言中的原始代码替换宏更强大且更不容易出错。它们是特殊函数,可以在编译步骤之前使用自定义语法重写源代码为标准代码,使开发人员能够引入新的语言构造而不改变编译器。就像运算符重载一样,语法宏可能会被滥用。但只要社区理解并管理这些缺点,它们支持强大且用户友好的抽象,比如 DSL(领域特定语言)。2020 年 9 月,Python 核心开发者 Mark Shannon 发布了PEP 638—语法宏,提倡这一点。在最初发布一年后,PEP 638 仍处于草案阶段,没有关于它的讨论。显然,这不是 Python 核心开发者的首要任务。我希望看到 PEP 638 进一步讨论并最终获得批准。语法宏将允许 Python 社区在对核心语言进行永久更改之前尝试具有争议性的新功能,比如海象操作符(PEP 572)、模式匹配(PEP 634)以及评估类型提示的替代规则(PEP 563 和 649)。与此同时,你可以通过MacroPy包尝试语法宏的味道。
¹ 引自《编程风格的要素》第二版第二章“表达式”,第 10 页。
² 这并不意味着 PEP 487 打破了使用这些特性的代码。这只是意味着一些在 Python 3.6 之前使用类装饰器或元类的代码现在可以重构为使用普通类,从而简化并可能提高效率。
³ 感谢我的朋友 J. S. O. Bueno 对这个示例的贡献。
⁴ 我没有为参数添加类型提示,因为实际类型是Any
。我添加了返回类型提示,否则 Mypy 将不会检查方法内部。
⁵ 对于任何对象来说都是如此,除非它的类重写了从object
继承的__str__
或__repr__
方法并具有错误的实现。
⁶ 这个解决方案避免使用None
作为默认值。避免空值是一个好主意。一般情况下很难避免,但在某些情况下很容易。在 Python 和 SQL 中,我更喜欢用空字符串代替None
或NULL
来表示缺失的数据。学习 Go 强化了这个想法:在 Go 中,原始类型的变量和结构字段默认初始化为“零值”。如果你感兴趣,可以查看在线 Go 之旅中的“零值”。
⁷ 我认为callable
应该适用于类型提示。截至 2021 年 5 月 6 日,这是一个未解决的问题。
⁸ 如在“循环、哨兵和毒丸”中提到的,Ellipsis
对象是一个方便且安全的哨兵值。它已经存在很长时间了,但最近人们发现它有更多的用途,正如我们在类型提示和 NumPy 中看到的。
⁹ 重写描述符的微妙概念在“重写描述符”中有解释。
¹⁰ 这个理由出现在PEP 557–数据类的摘要中,解释了为什么它被实现为一个类装饰器。
¹¹ 与 Java 中的import
语句相比,后者只是一个声明,让编译器知道需要某些包。
¹² 我并不是说仅仅因为导入模块就打开数据库连接是一个好主意,只是指出这是可以做到的。
¹³ 发送给 comp.lang.python 的消息,主题:“c.l.p.中的尖刻”。这是 2002 年 12 月 23 日同一消息的另一部分,在前言中引用。那天 TimBot 受到启发。
¹⁴ 作者们很友好地允许我使用他们的例子。MetaBunch
首次出现在 Martelli 于 2002 年 7 月 7 日在 comp.lang.python 组发布的消息中,主题是“一个不错的元类示例(回复:Python 中的结构)”,在讨论 Python 中类似记录的数据结构之后。Martelli 的原始代码适用于 Python 2.2,只需进行一次更改即可在 Python 3 中使用元类,您必须在类声明中使用 metaclass 关键字参数,例如,Bunch(metaclass=MetaBunch)
,而不是旧的约定,即添加一个__metaclass__
类级属性。
¹⁵ 在《流畅的 Python》第一版中,更高级版本的LineItem
类使用元类仅仅是为了设置属性的存储名称。请查看第一版代码库中bulkfood 的元类代码。
¹⁶ 如果您考虑到使用元类的多重继承的影响而感到头晕,那很好。我也会远离这个解决方案。
¹⁷ 在决定研究 Django 的模型字段是如何实现之前,我靠写 Django 代码谋生几年。直到那时我才了解描述符和元类。
¹⁸ 这句话被广泛引用。我在 DHH 的博客中发现了一个早期的直接引用帖子,发布于 2005 年。
¹⁹ 我买了一本二手书,发现它是一本非常具有挑战性的阅读。
²⁰ 请参见第 xvii 页。完整文本可在Berkeley.edu上找到。
²¹ 《机器之美:优雅与技术之心》 作者 David Gelernter(Basic Books)开篇讨论了工程作品中的优雅和美学,从桥梁到软件。后面的章节不是很出色,但开篇值得一读。
后记
Python 是一个成年人的语言。
Alan Runyan,Plone 联合创始人
Alan 的简洁定义表达了 Python 最好的特质之一:它不会干扰你,而是让你做你必须做的事情。这也意味着它不会给你工具来限制别人对你的代码和构建的对象所能做的事情。
30 岁时,Python 仍在不断增长。但当然,它并不完美。对我来说,最令人恼火的问题之一是标准库中对CamelCase
、snake_case
和joinedwords
的不一致使用。但语言定义和标准库只是生态系统的一部分。用户和贡献者的社区是 Python 生态系统中最好的部分。
这是社区最好的一个例子:在第一版中写关于asyncio时,我感到沮丧,因为 API 有许多函数,其中几十个是协程,你必须用yield from
调用协程—现在用await
—但你不能对常规函数这样做。这在asyncio页面中有记录,但有时你必须读几段才能找出特定函数是否是协程。所以我给 python-tulip 发送了一封标题为“建议:在asyncio文档中突出显示协程”的消息。asyncio核心开发者 Victor Stinner;aiohttp的主要作者 Andrew Svetlov;Tornado 的首席开发人员 Ben Darnell;以及Twisted的发明者 Glyph Lefkowitz 加入了讨论。Darnell 提出了一个解决方案,Alexander Shorin 解释了如何在 Sphinx 中实现它,Stinner 添加了必要的配置和标记。在我提出问题不到 12 小时后,整个asyncio在线文档集都更新了今天你可以看到的coroutine标签。
那个故事并不发生在一个独家俱乐部。任何人都可以加入 python-tulip 列表,当我写这个提案时,我只发过几次帖子。这个故事说明了一个真正对新想法和新成员开放的社区。Guido van Rossum 过去常常出现在 python-tulip 中,并经常回答基本问题。
另一个开放性的例子:Python 软件基金会(PSF)一直致力于增加 Python 社区的多样性。一些令人鼓舞的结果已经出现。2013 年至 2014 年,PSF 董事会首次选举了女性董事:Jessica McKellar 和 Lynn Root。2015 年,Diana Clarke 在蒙特利尔主持了 PyCon 北美大会,大约三分之一的演讲者是女性。PyLadies 成为一个真正的全球运动,我为我们在巴西有这么多 PyLadies 分部感到自豪。
如果你是 Python 爱好者但还没有参与社区,我鼓励你这样做。寻找你所在地区的 PyLadies 或 Python 用户组(PUG)。如果没有,就创建一个。Python 无处不在,所以你不会孤单。如果可以的话,参加活动。也参加线上活动。在新冠疫情期间,我在线会议的“走廊轨道”中学到了很多东西。来参加 PythonBrasil 大会—多年来我们一直有国际演讲者。和其他 Python 爱好者一起交流不仅带来知识分享,还有真正的好处。比如真正的工作和真正的友谊。
我知道如果没有多年来在 Python 社区结识的许多朋友的帮助,我不可能写出这本书。
我的父亲,贾伊罗·拉马尔,曾经说过“Só erra quem trabalha”,葡萄牙语中的“只有工作的人会犯错”,这是一个避免被犯错的恐惧所束缚的好建议。在写这本书的过程中,我肯定犯了很多错误。审阅者、编辑和早期发布的读者发现了很多错误。在第一版早期发布的几个小时内,一位读者在书的勘误页面上报告了错别字。其他读者提供了更多报告,朋友们直接联系我提出建议和更正。O’Reilly 的编辑们在制作过程中会发现其他错误,一旦我停止写作就会开始。我对任何错误和次优的散文负责并致歉。
我很高兴完成这第二版,包括错误,我非常感谢在这个过程中帮助过我的每个人。
希望很快能在某个现场活动中见到你。如果看到我,请过来打个招呼!
进一步阅读
我将以关于“Pythonic”的参考资料结束本书——这本书试图解决的主要问题。
Brandon Rhodes 是一位出色的 Python 教师,他的演讲“Python 美学:美丽和我为什么选择 Python”非常出色,标题中使用了 Unicode U+00C6(LATIN CAPITAL LETTER AE
)。另一位出色的教师 Raymond Hettinger 在 PyCon US 2013 上谈到了 Python 中的美:“将代码转化为美丽、惯用的 Python”。
伊恩·李在 Python-ideas 上发起的“风格指南的演变”主题值得一读。李是pep8
包的维护者,用于检查 Python 源代码是否符合 PEP 8 规范。为了检查本书中的代码,我使用了flake8
,它包含了pep8
、pyflakes
,以及 Ned Batchelder 的McCabe 复杂度插件。
除了 PEP 8,其他有影响力的风格指南还有Google Python 风格指南和Pocoo 风格指南,这两个团队为我们带来了 Flake、Sphinx、Jinja 2 等伟大的 Python 库。
Python 之旅者指南!是关于编写 Pythonic 代码的集体作品。其中最多产的贡献者是 Kenneth Reitz,由于他出色的 Pythonic requests
包,他成为了社区英雄。David Goodger 在 PyCon US 2008 上做了一个名为“像 Pythonista 一样编码:Python 的惯用法”的教程。如果打印出来,教程笔记有 30 页长。Goodger 创建了 reStructuredText 和 docutils
——Sphinx 的基础,Python 出色的文档系统(顺便说一句,这也是 MongoDB 和许多其他项目的官方文档系统)。
马蒂恩·法森在“什么是 Pythonic?”中直面这个问题。在 python-list 中,有一个同名主题的讨论线程。马蒂恩的帖子是 2005 年的,而主题是 2003 年的,但 Pythonic 的理念并没有改变太多——语言本身也是如此。一个标题中带有“Pythonic”的很棒的主题是“Pythonic way to sum n-th list element?”,我在“Soapbox”中广泛引用了其中的内容。
PEP 3099 — Python 3000 中不会改变的事情解释了为什么许多事情仍然保持原样,即使 Python 3 进行了重大改革。很长一段时间,Python 3 被昵称为 Python 3000,但它提前了几个世纪到来——这让一些人感到沮丧。PEP 3099 是由 Georg Brandl 撰写的,汇编了许多由BDFL,Guido van Rossum 表达的观点。“Python Essays”页面列出了 Guido 本人撰写的几篇文章。