返回介绍

8.9 延伸阅读

发布于 2024-02-05 21:59:47 字数 5254 浏览 0 评论 0 收藏 0

Python 语言参考手册中“Data Model”一章的开头清楚解释了对象的标识和值。

“Python 核心系列”图书的作者 Wesley Chun 在 OSCON 2013 做了一场精彩的演讲,涵盖了本章讨论的很多话题。在“Python 103: Memory Model & Best Practices”演讲页面可以下载幻灯片。Wesley 在 EuroPython 2011 还做过一次更长的演讲(YouTube 视频),不仅涵盖了本章的主题,还讨论了特殊方法的使用。

Doug Hellmann 写了一长串精彩的博客文章,题为“Python Module of the Week”,8 后来集结成书,即《Python 标准库》。他写的“copy - Duplicate Objects9 和“weakref - Garbage-Collectable References to Objects10 两篇文章涵盖了本章讨论的部分话题。

8原来是基于 Python 2 的,现在已经改为基于 Python 3。——编者注

9新的版本基于 Python 3。——编者注

10新的版本基于 Python 3,并改名为“weakref - Impermanent References to Objects”。——编者注

关于 CPython 分代垃圾回收程序的更多信息,请参阅 gc 模块的文档。文档开头的第一句话是:“这个模块为可选的垃圾回收程序提供接口。”“可选的”这个修饰词可能让人惊讶,不过“Data Model”一章也说:

垃圾回收可以延缓实现,或者完全不实现——如何实现垃圾回收是实现的质量问题,只要不把还能获得的对象给回收了就行。

Fredrik Lundh(很多核心库的创建者,如 ElementTree、Tkinter 和图像库 PIL)写了一篇短文,谈论了 Python 的垃圾回收程序,题为“How Does Python Manage Memory?”。他强调垃圾回收程序是一种实现的特性,其行为在不同的 Python 解释器中有所不同。例如,Jython 用的是 Java 垃圾回收程序。

CPython 3.4 改进了处理有 __del__ 方法的对象的方式,参见“PEP 442—Safe object finalization”。

维基百科中有一篇文章讲解了字符串驻留,那篇文章提到了几种语言对这个技术的利用,包括 Python。

杂谈

平等对待所有对象

发现 Python 之前,我学过Java。我一直觉得 Java 的 == 运算符用着不舒服。程序员关注的基本上是相等性,而不是标识,但是 Java 的 == 运算符比较的是对象(不是基本类型)的引用,而不是对象的值。就算是比较字符串这样的基本操作,Java 也强制你使用 .equals 方法。尽管如此,.equals 方法还有另一个问题:如果编写 a.equals(b),而 a 是 null,会得到一个空指针异常。Java 设计者觉得有必要重载字符串的 + 运算符,那为什么不把 == 也重载了?

Python 采取了正确的方式。== 运算符比较对象的值,而 is 比较引用。此外,Python 支持重载运算符,== 能正确处理标准库中的所有对象,包括 None——这是一个正常的对象,与 Java 的 null 不同。

当然,你可以在自己的类中定义 __eq__ 方法,决定 == 如何比较实例。如果不覆盖 __eq__ 方法,那么从 object 继承的方法比较对象的 ID,因此这种后备机制认为用户定义的类的各个实例是不同的。

1998 年 9 月的一个下午,读完 Python 教程后,考虑到这些行为,我立即就从 Java 转到 Python 了。

可变性

如果所有 Python 对象都是不可变的,那么本章就没有存在的必要了。处理不可变的对象时,变量保存的是真正的对象还是共享对象的引用无关紧要。如果 a == b 成立,而且两个对象都不会变,那么它们就可能是相同的对象。这就是为什么字符串可以安全使用驻留。仅当对象可变时,对象标识才重要。

在“纯”函数式编程中,所有数据都是不可变的,如果为集合追加元素,那么其实会创建新的集合。然而,Python 不是函数式语言,更别提纯不纯了。在 Python 中,用户定义的类,其实例默认可变(多数面向对象语言都是如此)。自己创建对象时,如果需要不可变的对象,一定要格外小心。此时,对象的每个属性都必须是不可变的,否则会出现类似元组那种行为:元组本身不可变,但是如果里面保存着可变对象,那么元组的值可能会变。

可变对象还是导致多线程编程难以处理的主要原因,因为某个线程改动对象后,如果不正确地同步,那就会损坏数据。但是过度同步又会导致死锁。

对象析构和垃圾回收

Python 没有直接销毁对象的机制,这一疏漏其实是一个好的特性:如果随时可以销毁对象,那么指向对象的强引用怎么办?

CPython 中的垃圾回收主要依靠引用计数,这容易实现,但是遇到引用循环容易泄露内存,因此 CPython 2.0(2000 年 10 月发布)实现了分代垃圾回收程序,它能把引用循环中不可获取的对象销毁。

但是引用计数仍然作为一种基准存在,一旦引用数量归零,就立即销毁对象。这意味着,在 CPython 中,这样写是安全的(至少目前如此):

open('test.txt', 'wt', encoding='utf-8').write('1, 2, 3')

这行代码是安全的,因为文件对象的引用数量会在 write 方法返回后归零,Python 在销毁内存中表示文件的对象之前,会立即关闭文件。然而,这行代码在 Jython 或 IronPython 中却不安全,因为它们使用的是宿主运行时(Java VM 和 .NET CLR)中的垃圾回收程序,那些回收程序更复杂,但是不依靠引用计数,而且销毁对象和关闭文件的时间可能更长。在任何情况下,包括 CPython,最好显式关闭文件;而关闭文件的最可靠方式是使用 with 语句,它能保证文件一定会被关闭,即使打开文件时抛出了异常也无妨。使用 with,上述代码片段变成了:

with open('test.txt', 'wt', encoding='utf-8') as fp:
  fp.write('1, 2, 3')

如果对垃圾回收程序感兴趣,可以阅读 Thomas Perl 的论文,“Python Garbage Collector Implementations: CPython, PyPy and GaS”。就是从那篇论文中,我得知在 CPython 中 open().write() 是安全的。

参数传递:共享传参

解释 Python 中参数传递的方式时,人们经常这样说:“参数按值传递,但是这里的值是引用。”这么说没错,但是会引起误解,因为在旧式语言中,最常用的参数传递模式有按值传递(函数得到参数的副本)和按引用传递(函数得到参数的指针)。在 Python 中,函数得到参数的副本,但是参数始终是引用。因此,如果参数引用的是可变对象,那么对象可能会被修改,但是对象的标识不变。此外,因为函数得到的是参数引用的副本,所以重新绑定对函数外部没有影响。读过《程序设计语言——实践之路(第 3 版)》11(Michael L. Scott 著)之后,尤其是 8.3.1 节“参数模式”,我决定采用共享传参(call by sharing)这个说法。

爱丽丝和白骑士关于那首歌的对话完整版

我喜欢这段对话,但是放在一章的开头太长了。下面是关于白骑士那首歌的完整对话,谈到了曲名和得名的缘由。

“你不开心,”白骑士用一种忧虑的声调说,“让我给你唱一首歌安慰你吧。”

“那首歌很长吗?”爱丽丝问道,因为这一天她已经听过许多诗了。

“是很长,”白骑士说,“不过它非常、非常美。不论谁听到我唱这首歌——或者是听得热泪盈眶,或者是——”

“或者是什么呀?”爱丽丝问道,因为白骑士忽然煞住不言语了。

“或者是没有热泪盈眶,你知道。这首歌的曲名叫作:《黑线鳕的眼睛》。”

“哦,那是一首歌的曲名,是吗?”爱丽丝问道,她试着使自己感到有兴趣。

“不,你不明白,”白骑士说,看来有些心烦的样子,“那是人家这么叫的曲名。

真正的曲名《老而又老的老头儿》。”

“那么我刚才应该说,‘那首是那么被人叫的’?”爱丽丝自己纠正说。

“不,你不应该这么说。这是另一码事!这首人家叫作《方法和手段》。不过这只不过是人家这样叫,你知道!”

“嗯,那么,那究竟是什么歌呢?”爱丽丝问道,她这一次完完全全给弄糊涂了。

“我正是准备说的呀,”白骑士说道,“这首歌真正《坐在大门上》;曲子是我自己发明的。”

——Lewis Carroll
《爱丽丝镜中奇遇记》,第 8 章“这是我自己的发明”

11该书英文版(书名:Programming Language Pragmatics)在 2015 年 12 月已出第 4 版。——编者注

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。
列表为空,暂无数据
    我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
    原文