10. Python 类的句法设计

本系列文章译自Python之父 Guido van Rossum 的系列博客“The History of Python”。这个博客系列对我们理解Python及其演变很有帮助,经Guido同意,在这里翻译推荐给大家,希望大家喜欢,也请大家多多指教!


1. 问题

设计好用户自定义类的运行机制后(见上一篇),我需要确定类的句法,特别是方法定义句法。主要是我想让类的方法定义和一般函数定义保持一致,否则,就必须大动干戈,重构 Python 的基本语法和字节码生成器了。

不过,即使我能让方法定义句法保持一致,依然要解决实例变量的问题。
一开始,我想模仿 C++ 中的隐式引用。比如说,在 C++ 中,你可以这样定义一个类:

''class A {
''     public:
''      int x;
''          void spam(int y) {
''      printf("%d %d\n", x, y);
''  }
'' };

这个类的实例有一个变量 x,在其方法中,可以隐式地调用这个变量。比如,在 spam() 方法中,x 既不是方法的参数,也不是局部变量,但由于这个类声明了变量 x,所以可以直接指向实例变量。

不过我很快意识到,Python 是做不到这一点的。因为在一门不需要声明变量的语言中,找不到优雅的办法,来区分实例变量与局部变量。


2. 隐式调用实例变量的困难

理论上说,要获取实例变量的值是很容易的。Python 已经有一个变量搜索顺序:局部变量、全局变量、内置变量——它们各有一个字典,只需要按顺序查找过去就可以了。例如,我们运行一个函数,涉及局部变量 p 和全局变量 q,那么语句“print p, q”,就会先查找第一个字典,即局部变量,并在其中找到 p,由于没找到 q,便开始查找第二个字典,即全局变量。

把实例字典加到查找序列前面是很容易的。那么,如果我们运行一个涉及实例变量 x 和局部变量 y 的函数,语句“print x, y”就会先在实例变量中找到 x,然后在局部变量中找到 y。

不过,在给实例变量赋值时,这个思路就不行了。在 Python 中,给变量赋值并不会按顺序查找变量名,而是在查找顺序的第一个字典中直接添加变量或替换变量的值,一般来说,即局部变量。也就是说,变量是默认创建在局部作用域的(当然,可以通过全局声明来改变默认行为)。

如果不调整这种简单的赋值策略,只是将实例字典置于变量搜索顺序之前,就会导致无法给局部变量赋值。比如说,如下这个方法:

''def spam(y):
''      x = 1
''      y = 2

给 x 、 y 赋值的语句只会给实例变量 x 重新赋值,并增加一个实例变量 y,赋值为 2。

交换实例变量和局部变量的搜索顺序也是一样的,只是让我们无法赋值的对象改为实例变量而已。

另外,改变赋值语法:如果实例存在该变量,就赋值给实例,如果不存在,就赋值给局部变量——也是没用的,因为这会产生另一个问题:怎么增加实例变量呢?

一个可能的方案是,和全局变量一样,通过显式声明创建实例变量。不过,考虑到 Python 一直没有变量声明,我实在不想因为这个问题增加这个特性。而且,全局变量声明一般比较少用,而实例变量却几乎无处不在。

另一个可能的方案是,在词法上对实例变量进行区别,比如说,让实例变量前面都增加一个 @ (即Ruby采用的方法)或其它前缀等。

这两种方案我都毫无兴趣(至今依然如此)。


3. 采用显式调用

我打算放弃隐式引用的思路。在 C++ 之类的语言中,我们可以通过 this -> foo 来显式引用实例变量 foo(以免局部变量中有一个重名的 foo 的情况)。因此,我决定,实例变量必须显式引用。另外,我认为与其让当前对象(this)成为一个特殊关键词,不如直接让“this”(或者其它等义词)作为实例方法的第一个参数,这样,实例变量就可以作为这个参数的属性被引用。

采用显式引用后,就不用为类的方法定义设计特殊句法,也不用担心变量查找的复杂化。我们只需要定义一个方法,并把第一个参数设为实例自身,即“self”就可以了。比如说:

''def spam(self, y):
''      print self.x, y

我在 Modula-3 中见过类似思路——Python 的 import 和异常处理语法也借鉴自 Modula-3。Modula-3 没有类的概念,但可以创建包含指向已定义函数的指针的记录类型(record types),并提供语法糖,使我们可以在调用函数的时候引用记录的变量。比如 x 是记录的变量,m 是这个记录包含的函数指针,指向函数 f,那么,x.m(args) 就等价于 f(x, args)。

这样,我就完成了类与方法的实现,并可以通过方法的第一个参数的属性来调用实例变量。

剩下的就是一些细节设计了。

遵循一贯的简洁原则,我把类语句当成一系列的方法定义,句法上与函数一致,但一般会有“self”作为第一个参数。同时,为避免给特殊方法设计新句法(比如初始化方法或析构函数(destructors)),我决定让用户实现一些特殊命名的方法,比如 initdel 等。这种命名惯例来自 C语言,在 C语言中,带两个下划线前缀的变量名由编译器保留,通常有一些特殊意义(比如 FILE)。

这样,Python 中的类看起来就是如下代码:

''class A:
''      def __init__(self, x):
''          self.x = x
''      def spam(self, y):
''          print self.x, y

4. 类作为一个命名空间

在这里,我依然希望尽可能重用我之前的代码。

一般来说,定义一个函数就是创建一个可执行语句,并在当前命名空间创建一个指向函数对象的变量(变量名称即函数名称)。因此我想,与其设计一个全新的方法来处理类,不如直接把类看做一系列在新命名空间执行的语句。这个新命名空间的字典,就被用于初始化类字典并创建一个类对象。

从底层看,即把类变成一个匿名函数,所有语句都在其中执行,并将其局部变量字典作为结果返回。之后,这个字典被传递给一个创建类对象的辅助函数,辅助函数会把类对象存储于一个作用域中,类的名称就是这个作用域的名称。

因为类可以支持任意序列的有效语句,大家往往会觉得很神奇。其实 Python 的这个特性只是简化句法,不进行人为限制的直接结果而已。

最后一个细节就是实例化的句法。很多语言,比如 C++ 和 Java,都通过特殊操作符“new”来创建实例。在 C++ 中,因为类名在解析器中有一个特殊状态,这是可行的。但 Python 解析器不关心用户调用的什么类型的对象,因此,最好、最直接、不产生任何新句法的方案,就是让类对象本身可调用。

关于这一点,当时的我可能超越时代了——通过工厂函数创建实例现在已经很流行,而我当时即把类当做它自己的工厂。


5. 关于特殊方法

最后,简单提一下,我的一个主要目标,就是尽可能以简单的方式来实现类。在大多数面向对象语言中,都有一些只针对类的特殊操作符或特殊方法。比如在 C++ 中,有定义构造函数和析构函数的特殊句法,与定义常规函数或方法的句法不同。

而我真的不想为对象的特殊操作引入新句法,因此,我采用了一系列预定义的“特殊方法”,比如 initdel。用户可以通过实现这些方法来定义构造和析构过程。

同时,我也用这种方法来支持用户对 Python 操作符的行为进行自定义。如之前所说,Python 是用 C 语言实现的,并通过函数指针来实现内置对象行为(如“get attribute”、“add”、“call”等)。为使用户自定义类也可以有这些行为,我为这些函数指针也指定了一些特殊方法名称,比如 getattraddcall 等。


公众号:ReadingPython

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,560评论 4 361
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,104评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,297评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,869评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,275评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,563评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,833评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,543评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,245评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,512评论 2 244
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,011评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,359评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,006评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,062评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,825评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,590评论 2 273
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,501评论 2 268

推荐阅读更多精彩内容