Python 简要指南

前言

Python 是一门易学难精通的语言。原因一方面是,掌握常用部分就能满足大部分工作场景,虽然写起来会稍显啰嗦;另一方面是,语言花样繁多,各种偏好语法糖看的人眼花缭乱。恰逢有新同学进组,自己又感觉有些心得体会,故挑选一些风格经验作为例子,分享一下我眼中的 Python。

鸭子类型 Duck Typing

说到 Python,就不可避免的讲到鸭子类型,关于鸭子类型的详细介绍可以参考维基百科。简单来说,Python 是一门面向对象语言,类由方法与属性所构成,其定义了类上各种合法操作,一个类是“鸭子”,意味着这个类可以做“鸭子”所有合法操作,即具有相同(名字)的方法与属性。

举个例子,这里有一道完形填空,”___ 在水中游泳“,提问:这里应该填上什么,才能使上下文成立呢?按面向对象的方式来思考,这里能满足上下文的对象,应该具有“游泳”这种方法。“鸭子”可以在水中游泳,而不可能是“汽车在水中游泳”。面向对象编程就像这里的完形填空一样,上下文隐式约束填入的对象,就像方法函数隐式约束调用的形参。这些所有能够在水中游泳的对象,可以看作是同一范畴,暂且将其称作“游泳类”,如果要用面向对象的方式给定一个名称,即为类型的类 typeclass

实际编程中,有许多这样的上下文,大大小小的各种函数方法,每个上下文都隐性要求某种范畴意义上的 typeclass,或许是“游泳类”又或是其他更复杂的。假如是在静态语言中,则必须要在上下文中标注上具体的类型,例如需要在函数调用前手动标注形参类型,而想要达到 typeclass 的效果,还需要用上相应静态语言多态的功能,因为其不仅限定于某种具体的类型,如具体函数调用会依据实际类型而变化。在 Python 这种动态语言中,情况变得简单,一些尽在不言之中,约束是隐式的,实现无需额外声明,解释器只是根据名字查找调用,只要对象满足约束即可正常运行。Python 将其称为结构子类型,以区别于传统静态面向对象语言中的名义子类型。

动态类型加上结构子类型的直接结果是,Python 是一门重语义、协议的语言。要从程序意义上兼容原有对象,需要替换对象实现原有对象所有操作即可,实现属性访问结构子类型上的兼容。要从使用意义上兼容原有对象,需要有一致的使用模型,及对应模型操作兼容的语义。在 Python 中,类似“游泳类”这样的 typeclass 可以使用协议去描述,协议中声明了各种操作所需的基本语义。在单个对象上的使用上,隐式满足不同协议,就像光具有波粒二象性,能自由的用在需要波动与粒子的上下文中。在不同上下文环境的使用中,使用一致的协议,不同的实现,等价的结构,以满足对高层隔离具体细节的目的,就像帧同步框架中,需要先建立一套确定性、平台无关的操作原语,才能保证游戏状态一致性。

在 Python 标准库的 collections.abc 中定义了各种容器的抽象基类,在实际数据操作温和抽象数据结构使用方式的时候不妨多多使用,在简单定义几个基础的抽象方法后,即可获得像内置数据结构一样的支持体验,其他更多内建方法也会基于这些抽象方法语义能够使用,达到基于操作语义编程的目的。OrderedDict、defaultdict、CaseInsensitiveDict 等都是值得参考的例子。Python 魔术方法作为会被解释器隐式调用的特殊方法,也是基于操作语义编程的一种表现,其理念贯彻于 Python 数据模型之中,相应介绍可以参见《流畅的 Python》第一章:Python 数据模型。

继承与混入类 Super & Mixin

传统面向对象语言中,继承一般是实现动态子类型的一种手段。Python 由于鸭子类型的存在,并不需要继承这种名义上的强约束。在 Python 中引入继承,一个原因是确实要表达 is-a 关系,减少重复代码编写量(谁还不想偷点懒呢),另一方面是用来组合,作为同一对象不同性质的复合,常见相关的惯用法有 Mixin。

Python 支持多重继承,如果对 C++ 中的多重继承有所了解,可能会认为这是一项麻烦的特性,立体复杂的菱形继承结构走向未知的浑沌。虽然两者都叫多重继承,但是 Python 中的某些设定避免了成为 C++ 中那样怪物般的特性。首先,Python 中的菱形继承顶部基类只会有一份,不会有冗余、二义性的问题。其次,Python 使用 C3 线性化算法来解析 MRO,将多重继承中复杂结构拍扁成一个 list 的线性结构,同时保留继承链中所有父子类的偏序关系,皆由这种偏序关系与继承顺序推导出一个全序关系。

Super 则是与 MRO 相互搭配的设计,目的为沿着对象实例实际类型的 MRO 顺序依次调用,保证不遗漏调用 MRO 任何一个类的方法。Super 的语义是指向 MRO 链中当前方法调用类所在位置的下一个类,让我们据此仔细考察一下 Super 的使用情形。第一,我们不能对方法调用顺序作任何直接先后上的假定,举个例子,假如有继承链 A <- B 表示 B 继承于 A,我们可以认为 B 实例 Super 方法调用会直接到 A 方法调用吗?答案是不行,考虑有继承链 A <- C 与 (B, C) <- D,其中后者表示 D 多继承于 C 跟 B,此时 D 的 MRO 会变成 [D, B, C, A],B 使用 Super 调用将直接到 C,所以在 Super 调用链中我们只能得到父子类偏序关系调用顺序的保证,这种只有顺序依赖上的保证实际上比想象的要弱。第二,在单继承的情况下,其实没有必要使用 Super,调用是静态唯一确定的,直接用类方法调用替代即可,由此还可获得第一点讨论的直接调用顺序上的保证。

综上所述,Super 实际上是服务于利用多重继承进行组合的,为了不同行为组合成统一对象,或者是插件式单一对象行为功能的扩充。前者,结合鸭子类型的想法,我们可以想象用类去表示不同的能力,对象继承于这个类说明对象能够干这些事,组合不同的这样的能力类来表达对象的能力范畴,Super 在这样的情况下扮演的是个简单的调和者,对象的方法一旦在继承的多个能力中定义,组合的方式就要求解决如何统一同一操作不同能力定义冲突。后者,在 Python 中常惯用于 Mixin,详细的使用例子可以参照 StackOverflow 上的这个回答

总之,Super 适用于插件式的加载调用,每个调用按照依赖顺序先后调用有且仅有一次。一旦涉及到复杂情况需要调和冲突,屏蔽其中某些调用,表现出不同的行为,甚至是父类内在语义的变更,那最好直接用类调用替代 Super 调用,本质上抽象类的分层已经被打破了,只能控制在我们能够掌控的规模,而不能通过这种简单的性质组合起来。关于 Super 详细的一些介绍使用,更多可以参考 Python’s super() considered super! 一文。

修改与混乱 Hack & Chaos

鸭子类型是一把双刃剑,一切尽在不言中,混乱也是。Python 中最常见的事情之一,就是伪装,property 可以伪装成属性,装饰器返回值可以伪装成被装饰的对象,unittest 里面有 mock 伪装做单元测试等。各种各样的伪装,魔术方法的隐式调用,语法糖的包装,真真假假真真,对象所处的世界就像是黑客帝国中设定那般虚幻。只要能凑齐必要的信息,Python 可以通过各种 hack 方式达到想要的目的,再经过语法糖的精心包装,看上去就像是 web 前端一样优雅。而后面不同的各种 hack 方式、状态存取、潜在的 hack 方式冲突、各种 hack 相互叠加导致的问题,都是一堆定时炸弹,不合预期的操作可能导致 C++ 未定义行为一样的惊喜。

代码总是在演进的,鸭子类型的一个核心问题是,很难定义一个良好的语义,也很难在一开始就设计一个良好的模型,与渐进式的代码开发实际上是有些冲突的。分模块不同人维护的情况下,很难达成共有的语义一致。一开始以为是一致的某些行为,最后发现在某些情况下还是有区别的,需要做某些 isinstance 的判别。连理解标准库及内置的一大堆协议来正确使用它们,大部分人都不怎么清楚,这种隐式规则的具象化体现,就是一篇篇的规范手册与缘由简介。

在一门灵活动态的语言中,有无数条路通往终点,又该怎么找到简洁有效那一条呢?There should be one– and preferably only one –obvious way to do it.显式优于隐式,可以用语法元素显式的将语义标记出来,再用hack的手段包装下方便使用。一般 hack 的方式有元类、魔术方法、装饰器、属性描述符,详细的使用可以参照《流畅的 Python》中相关章节。

Others

typehint

Python 是一门动态语言,不代表它没有类型或是类型不重要。提供类型标识重要的一点在于,可以把 Python 中可以静态确定的那部分信息,显式表达出来。目的是可以提供更好的可读性,指导类型检查器及时发现很多问题,配合IDE、文档生成工具提高开发效率。不得不吐槽的是,这方面还很不完善,typing 中标准集合的注释,到了 Python 3.9 因为 class_getitem 加入就被标记废除掉了,参见 PEP 585StackOverflow 上的讨论。关于这些静态类型提示信息,一些库(pydantic)用来做数据的约束检查,而类型标注实际上是有成本的,相关的解决前向引用的字符串标注也带来了很多问题,相关讨论可以在《流程的Python》15 章:类型提示进阶中找到。个人认为,将不完善的类型标注引入实际上有点不负责,虽然在实践中可以更好的改进,但是不断引入类型信息黑魔法及破坏性更改,无疑是对社区信任的伤害。Typehint 的使用,可以参照这篇文章

pattern match

新引入的 match case 语法以及 args 与 kwargs 正确使用感觉很有必要,详细可以找找网上的资料。

tool chain

Python 是一门动态语言,很多时候由于静态类型信息的缺失,可读性及代码健壮性都会受到影响。工具是一个很好的手段帮助提升整体代码质量,很多时候我们需要自己去做工具,以帮助我们去发现自己模型语义上的错误。对于大型工程开发来说,Python 实际上是把一部分开发质量交给了需要自己去做工具约束。胶水语言的特性,也使得做工具变得比较简单,相关的开发工具有 black, mypy, pre-commit,掌握 poetry 以方便将你的代码写成库,便于发布包管理依赖也很重要。

作者

Sinksky

发布于

2023-07-11

更新于

2023-12-01

许可协议