好玩的 Python
一份 Python 游玩指南
友情提醒 0:为了避免冗长的解释,这篇指南需要读者有一定的 Python 编程基础,如果没有的话,建议先看看官方的 Python 教程。
友情提醒 1:这篇指南没有任何“实用”价值。
友情提醒 2:如果这样你还要读的话,不妨一边读,一边在你的电脑上,把文中提到的各种命令都执行看看,不然我保证你会睡着的。
友情提醒 3:以下所有内容均使用 CPython 解释器,基于 Python 3.13 版本,版本差太多的话,一些命令的结果可能会不一样。
一个彩蛋
我觉得 Python 本身就够好玩了,没必要非要用它做点啥。不信你试试在 Python 中执行 import this
,你将在输出中看到一首诗:
The Zen of Python, by Tim Peters
Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren’t special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one— and preferably only one —obvious way to do it.
Although that way may not be obvious at first unless you’re Dutch.
Now is better than never.
Although never is often better than right now.
If the implementation is hard to explain, it’s a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea — let’s do more of those!
这首诗叫“Python 之禅”,主要是说 Python 这个语言的设计更看重什么、而非什么,也就是描述 Python 语言的设计哲学。import
是 Python 中用来导入模块的语句,模块本质上就是一个 Python 脚本文件,Python 的解释器会搜索一些特定的路径来找到对应名字的文件,导入并执行这些文件来创建一个模块对象,最后把它的引用赋值给一个同名的变量(你也可以通过添加 as
来指定这个变量名,比如 import this as that
)。而 this
就是一个内置模块,纯粹用来输出上面那首诗。加入这个彩蛋的提案在 2004 年被一名重要的 Python 贡献者 Tim Peters 创建,那时 Python 3 尚未诞生,但这个语言的精髓已然奠定,并被写进这首诗中。如果一个编程语言能“吟诗”还不够吸引你,不妨再跟着我探索一段,毕竟这仅仅是个开头,而且关于这个彩蛋本身的故事仍未讲完……
在执行完 import this
后,我们再执行 print(this)
就能看到 this 这个模块的文件路径,循着路径找到的源代码的内容是这样的:
s = """Gur Mra bs Clguba, ol Gvz Crgref
Ornhgvshy vf orggre guna htyl.
Rkcyvpvg vf orggre guna vzcyvpvg.
Fvzcyr vf orggre guna pbzcyrk.
Pbzcyrk vf orggre guna pbzcyvpngrq.
Syng vf orggre guna arfgrq.
Fcnefr vf orggre guna qrafr.
Ernqnovyvgl pbhagf.
Fcrpvny pnfrf nera'g fcrpvny rabhtu gb oernx gur ehyrf.
Nygubhtu cenpgvpnyvgl orngf chevgl.
Reebef fubhyq arire cnff fvyragyl.
Hayrff rkcyvpvgyl fvyraprq.
Va gur snpr bs nzovthvgl, ershfr gur grzcgngvba gb thrff.
Gurer fubhyq or bar-- naq cersrenoyl bayl bar --boivbhf jnl gb qb vg.
Nygubhtu gung jnl znl abg or boivbhf ng svefg hayrff lbh'er Qhgpu.
Abj vf orggre guna arire.
Nygubhtu arire vf bsgra orggre guna *evtug* abj.
Vs gur vzcyrzragngvba vf uneq gb rkcynva, vg'f n onq vqrn.
Vs gur vzcyrzragngvba vf rnfl gb rkcynva, vg znl or n tbbq vqrn.
Anzrfcnprf ner bar ubaxvat terng vqrn -- yrg'f qb zber bs gubfr!"""
d = {}
for c in (65, 97):
for i in range(26):
d[chr(i+c)] = chr((i+13) % 26 + c)
print("".join([d.get(c, c) for c in s]))
可以看到,源代码中的诗是被凯撒加密的,在一串眼花缭乱的操作解密后,通过 print 输出。不用多想也知道,这完全是多此一举。但仔细读这段代码就能发现,它是刻意和它输出的“Python 之禅”反着来,比如诗中提到“简单优于复杂”,那这个源代码就要加点复杂度。可以说这一切是为了让 import this
这个彩蛋的源文件成为关于 import this
的一个彩蛋,可谓是“元彩蛋(meta easter egg)”。之所以要用“元(meta)”这个字,并不是我想故弄玄虚,而是我认为,这是 Python 最核心的概念,是皇冠上的明珠。
meta 是个相当难翻译和解释的概念,我觉得维基百科的说明写的很清楚,不妨读一读。在这里我取它的狭义,即“关于什么的什么”,元彩蛋就是关于彩蛋的彩蛋,这其中有自我参照(或者说自指、自我引用)、递归的含义。而“自指”也是著名的人工智能“圣经”《哥德尔、埃舍尔、巴赫:集异璧之大成》这本书中所核心讨论的主题。不可谓不巧的是,Python 也是当今人工智能领域的第一语言,仿佛是一种宿命。当然关于“元”的这些宏大的话题不是本文的核心内容,只是不得不提,感兴趣的读者还请去读侯世达等学者的书。
这里说回到 import this
这个彩蛋和它的元彩蛋,它们的关系在我看来,是对 Python 和它的解释器 CPython 的关系的类比。先给不熟悉 Python 乃至代码执行原理的读者简单解释下,Python 是一门解释型的语言,也就是说 Python 代码的执行过程是:启动一个 Python 解释器程序,把代码输入进去,让解释器进行一些底层的操作,比如在内存中创建一个数组,来满足代码的需求。CPython 解释器主要是用 C 语言写的,这也是它名字的由来。如果暂时忽略那些不是用 C 语言写成的解释器,可以说 C 语言是 Python 的元语言。而为了让 Python 能践行“Python 之禅”,CPython 的源代码看上去可能没有那么美观,但这正如太极阴阳一样,是必不可少的互补。CPython 充分利用 C 语言的高效,使得 Python 代码可以写的非常自由、简洁且强大。但遗憾的是,我见过不少人,仅仅把 Python 当作一个实用的工具,从没好好地玩过它,不了解“Python 之禅”,错过了机会,没能欣赏到这个语言本身的魅力。更有甚者,下意识地套用了其他编程语言的经验,反而觉得 Python 的代码写起来有点奇怪和别扭,惭愧的说,一开始的我就是这样的。直到我读到了《流畅的 Python》这本书,进而能够不断摸索这门语言的冰山一角之下的东西,在这里分享给你。
关于这个彩蛋,还有最后一个写在它的提案 PEP 20 中的玩笑值得一提,PEP 20 是 Python 改进提案第 20 号的意思,而这个序号并不是连续的,作者 Tim Peters 选了这个序号并在其中写道,他把 Python 的设计指导原则编成 20 条格言,只有其中的 19 条被写下了。如果你去数一数“Python 之禅”会发现确实只有 19 条格言。而那最后的一条,有这么一种说法,是让读者自行定义的。我的这一条格言是:玩多半没用,但真知灼见多半是玩出来的。
两个问题
玩 Python 最重要的工具是文档,它就像一本攻略书,其中“数据模型”这一章(也是我认为最重要的一章)提到了“对象是 Python 中对数据的抽象,Python 程序中的所有数据都是由对象或对象间关系来表示的”,或者说一切皆是对象。不过这个描述本身有点抽象,我们不妨探索下具体什么是对象。
什么是对象?
攻略中紧接着的是这样一段定义:“每个对象都有相应的标识号、类型和值。”按理来说,整数也是对象,但我们通常只关心它们的值,比如用 ==
运算符来检查两个整数是否相等,而不会用 is
比较它们的标识号(标识号是唯一标识 Python 对象的 ID,可以用 id()
函数获取到),因为整数作为一种不可变对象(创建后值不能改变),是不用担心它的值被意外改变的:
# 表达式可以创建对象
x = 12345
# 同样的表达式又会创建出新的对象
y = 12345
# 虽然 y 和 x 的值一样,但它们是不同的对象
print(y is x) # False
z = x
# 直接把 x 赋值给 z,所以 z 和 x 引用了同一个对象
print(z is x) # True
z = 54321
# z 的重新赋值只是创建了一个新的对象,也并不会改变 x
print(x) # 12345
print(z) # 54321
print(z is x) # False
上面的例子如果换一个数字,可能会出现不同的结果:
x = 1
y = 1
print(x is y) # True
为什么 x 和 y 会是同一个对象呢?继续看攻略会发现,对于不可变对象,有时会出现重用,事实上这是 CPython 的一种性能策略,通过缓存常用的不可变对象,来减少开销。那既然标识号在不可变对象上没什么用,为什么还要让它们有呢?既然有了,又为什么要有重用这样的特例在呢?我认为这恰恰是对 Python 之禅中的“特例不应打破规则,但务实胜过纯粹(Special cases aren’t special enough to break the rules. Although practicality beats purity)”的诠释,如果删掉不可变对象的标识号,那 CPython 中的相关代码就都要实现一套额外的逻辑来处理这个特例,会大大增加复杂度,而如果允许整数可变的话,就会给用户增加额外的管理成本,来避免一个整数对象被意外修改,在确保这两点的情况下,”对象重用“这个一般不会被用户感知到的特例就显得很务实了,毕竟在内存里存放一堆相同值的整数对象,无论对谁都没有什么好处,只会产生浪费。
为了能重复使用同一个对象,我们需要变量来绑定对象,显然 Python 中的变量和对象不是一一对应的关系。查阅攻略的“名称和对象”这一部分就会发现,变量本质上是“名称”,每个模块(也就是 Python 脚本文件)会有一个命名空间(一个映射,大多用 Python 字典 dict
实现),你可以通过 globals()
内置函数拿到这个字典(这里也可以用 locals()
,因为是在模块作用域中),然后对变量进行操作:
x = 123
print(globals()['x']) # 123
globals()['x'] = 456
print(x) # 456
# 这里用 locals 是一样的
locals()['x'] = 789
print(x) # 789
从这个角度上来看,变量在 Python 中只是一种语法糖而已,也就是说它没有增加新的功能,只是更方便用户使用。攻略的下一部分“Python 作用域和命名空间”还提到内置名称的集合也是一个命名空间,这意味着,内置对象同样可以起别名:
p = print
print = 123
p(print) # 123
# 你可能会担心,要是我把 print 玩丢了该怎么办?比如这样:
p = 123
# 我该怎么打印输出呢?其实这样就好了:
del print
print(p) # 123
del print
后,为什么 print
变量就又和内置函数 print()
重新绑定了呢?攻略是这么说的:在给名称赋值时,除非使用 global
和 nonlocal
这样的关键字,默认是修改到最内层作用域的命名空间,而获取一个名称绑定的对象时,则会从最内层开始搜索,如果没有就往更外层去寻找。p = print
中的 print
是往外搜索到了最外层的内置名称的命名空间中的 print
,而 print = 123
则是把当前命名空间中的 print
给赋值了,这之后用 print
这个名称就会优先搜索到 123 这个对象,直到我们用 del
删去了这个绑定。从中我们也可以得知,del
删除的是名称和对象的绑定,而非对象本身。
print()
函数用了这么多次,是时候关注下它本身了,上面的例子也印证了“一切皆是对象”这点,函数自然不例外:
print(print) # <built-in function print>
但函数是对象这件事可太实用了,所以这里就先不展开了。我们要关注的问题是为什么 print
能输出自定义类的实例呢?
class MyClass:
pass
obj = MyClass()
print(obj) # <__main__.MyClass object at 0x104a1eba0>
众所周知,最终能输出到命令行的一定是字符串,所以这之中隐含着对象到字符串的转换过程,涉及到自定义对象的默认字符串表示是怎么来的这个问题。
对象的表示是怎么来的?
攻略是这么说的,print
会把输入的对象转为字符串,就像 str()
做的那样,而 str
的描述则说,它返回 type(object).__str__(object)
,如果 object
没有 __str__
方法,则回退为返回 repr(object)
。也就是说,它用 type(object)
获取 object
的类型(类型自然也是对象),从这个对象上获取 __str__
方法,并把 object
作为参数调用它。
你可能已经知道了,像 __str__
这样双下划线开头结尾的方法被叫做特殊方法,它们是一种协议,如果一个类实现了某个特殊方法,它的实例就可以被相对应的语法使用,显然 __str__
就是对应内置函数 str()
使用的。另一方面,备选的 repr()
函数也有对应的特殊方法 __repr__
,这二者的区别不大,只不过 __str__
一般用作提供“非正式”的更适合显示的字符串表示形式,换句话说,更适合给人看的。
我们通常使用 .
从对象上获取属性(包括数据属性和方法),而根据攻略所说,属性也是一种“名称”,每个对象的属性的集合是一个命名空间,要是当前对象没有给定的属性,那就会去对象的类型乃至类型的基类中寻找。这就说明,这个没有实现 __str__
的自定义类 MyClass
一定有个基类,包含了 __str__
的默认实现。那就来打印输出一些信息看看,这里就要用到特殊属性了:
print(MyClass.__bases__) # (<class 'object'>,)
# 我们还可以输出 __str__ 来验证一下
print(MyClass.__str__) # <slot wrapper '__str__' of 'object' objects>
object
是什么呢?根据攻略的说明,它是所有其他类的终极基类,它提供了所有 Python 类实例均具有的方法。所以我们可以直接调用 object 上的 __str__
这个函数来输出任意一个对象的默认表示,哪怕我们给 MyClass
加一个 __str__
的实现,也可以绕过它:
def my_str(self):
return "hello world"
# 请原谅我图省事直接修改这个类了,你当然也可以重新定义一下 MyClass
MyClass.__str__ = my_str
print(obj) # "hello world"
print(object.__str__(obj)) # <__main__.MyClass object at 0x104a1eba0>
这里留下的最后一个问题是,为什么直接调用方法的时候要传入 obj
作为参数呢?事实上我们也可以从 obj
上获取到这个方法并调用:
print(obj.__str__()) # "hello world"
打印输出一下就能发现,从实例对象上获取到的,并不是原本的 __str__
:
print(MyClass.__str__) # <function MyClass.__str__ at 0x104de7100>
print(obj.__str__) # <bound method MyClass.__str__ of <__main__.MyClass object at 0x104cab770>>
print(MyClass.__str__ is obj.__str__) # False
攻略的“方法对象”这一部分解释了这件事,当引用一个实例对象的方法时,会把这个实例对象和在类型中找到的实现方法的函数对象打包,形成一个新的方法对象,这个对象会增加实例对象为第一个参数,然后再调用原函数,就像是一个动态添加的装饰器(高阶函数),那么这种动态添加是怎么实现的呢?攻略的这一部分揭示了,Python 的属性、方法等均基于[描述器](https://docs.python.org/zh-cn/3.13/howto/descriptor.html)协议。简单来说就是,如果一个对象的类实现了 __get__
、__set__
、__delete__
这些描述器协议的特殊方法,那么这个对象在被作为属性访问时,就会调用相应的描述器特殊方法。而 Python 的函数类型就实现了这么一个特殊方法 __get__
,使得我们定义在类上的任意函数在被类的实例进行属性引用时,调用 __get__
获得了一个方法对象。如果你觉得这听上去有点绕的话,可以看看[描述器指南](https://docs.python.org/zh-cn/3.13/howto/descriptor.html)中的例子,或者我们不妨实现一个“反方法对象”来覆盖这种默认的行为:
class AntiMethod:
def __init__(self, func):
# 传入一个原函数
self.func = func
def __get__(self, instance, owner=None):
# 当这个对象被作为属性引用的时候,总是返回原函数
return self.func
anti_method = AntiMethod(my_str)
MyClass.__str__ = anti_method
# 这之后,只要是通过属性获取 anti_method 这个对象的时候,都只能得到 my_str
print(MyClass.__str__ is anti_method) # False
print(MyClass.__str__ is my_str) # True
# 哪怕是从实例对象上获取
print(obj.__str__ is anti_method) # False
print(obj.__str__ is my_str) # True
# 实例对象也不会默认作为第一个参数传入了,不带参数调用的话就会报错
obj.__str__() # TypeError: my_str() missing 1 required positional argument: 'self'
理解了这些之后,我们就能意识到 self
只是一个普通的参数而已,尽管我们约定俗称地把它设为 self
,但其实它叫什么都行:
class MyClass:
def __str__(first):
return "hi"
# 你可以注意到,报错信息里提示的也是 first 这个参数名
print(MyClass.__str__()) # TypeError: MyClass.__str__() missing 1 required positional argument: 'first'
obj = MyClass()
print(obj) # hi
特殊方法和它们所能实现的协议实为 Python 的精髓所在。常见的 __init__
可以在一个实例对象初始化的时候做点什么;__len__
、__getitem__
和别的一些特殊方法可以用来模拟容器类型,从而让自定义对象用上 for
、in
切片等语句,甚至是标准库或第三方库中的任意接受可迭代对象作为参数的 API;__lt__
等一系列的特殊方法则可以用来重载运算符。这样的例子不胜枚举,攻略的“数据模型”这一章或者《流畅的 Python》这本我很喜欢的关于 Python 的书中都有充足的说明和案例,我就不再赘述了。
三生万物
到目前为止,我们已经多少能感受到,Python 的对象能有各种各样的表示、乃至不同的行为,都取决于它的类型实现了哪些特定的特殊方法或者说协议,而这些协议都对应着 Python 语言的各个功能,内置对象也不例外。一切就好像是用乐高积木搭起来的一样,只要组合特定的协议,就能构造各种各样的类型,从而生产各种各样的实例对象,这一般被叫做“鸭子类型”,对编程来说很实用,因为可以避免“继承”的诸多问题。当然我们关注点就一直不是实用性,而是一头房间里的大象:既然一切对象都有类型,类型也是对象,那么类型的类型是什么?
print(type(int)) # <class 'type'>
print(type(str)) # <class 'type'>
print(type(MyClass)) # <class 'type'>
print(type(object)) # <class 'type'>
print(type) # <class 'type'>
乍一看可能会觉得搞错了什么,type
不是个函数吗?但仔细想想这件事并不冲突,Python 中的函数、类都是对象,它们之所以是函数或者类,只是因为它们实现了某些特殊方法而已,所以一个对象可以既是函数也是类,只要把相关的特殊方法都实现了就可以。让一个对象能像函数一样被调用的特殊方法是 __call__
:
class MyClass:
def __call__(self):
print("called")
obj = MyClass()
obj() # called
上面这些类的类型都是 type
,也就是说这个类的实例是类型,这是一个创建类的类,结合前面我们关于“元”的定义就能猜到,这种类在 Python 中被叫做“元类”。Python 中的类默认都是用 type
这个元类创建的,当然我们也可以定义新的元类,用来定制一批类的共同特征,又或者是动态地生成类型。显然 type
也能用来动态地生成类,只要填入适当的参数,实例化一个它的对象即可:
MyClass = type("MyClass", (), {}) # 这里的三个参数分别是类名、基类的元组、属性的字典
print(MyClass) # <class '__main__.MyClass'>
obj = MyClass()
print(obj) # <__main__.MyClass object at 0x10336cc20>
这件事也告诉了我们,class
关键字开头的类型定义也可以被视作一种语法糖,当然我们在绝大多数情况下还是会用它来创建类,而不是 type
,因为这样比较方便。不过我们仍要刨根问底,元类 type
的依然是个对象,那它的类型又是谁呢,难道有一个“元元类”吗?这当然是不可能的,因为那之后就需要“元元元类”以及无穷无尽的更“元”的“元类”,而且它们也没有存在的价值。所以 Python 是这样诠释的:
# 如果一切被视作“对象”
print(object.__bases__) # ()
# 所有“对象”都有定义其行为的类型
print(type(object)) # <class 'type'>
# “类型”这个类型也不例外的是对象
print(type.__bases__) # (<class 'object'>,)
# 而“类型”的类型是它自己
print(type(type)) # <class 'type'>
object
和 type
是循环互指的,而 type
又是循环自指的。还记得攻略中对象的定义吗,它是这样的:“对象是 Python 中对数据的抽象,Python 程序中的所有数据都是由对象或对象间关系来表示的”,对象、类型、对象间的关系,这就是“三生万物”。现在知道为什么 Python 的标志是两条互相衔着尾巴的蛇了吧?
我想到这里,游玩 Python 的核心思想和方法已经挺清楚的了,前路的无限可能性也已然展开,只待我们去收集、探索更多的玩法。而且别忘了,这个“游戏”还在持续更新呢!