本系列文章译自 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)), 我决定让用户实现一些特殊命名的方法, 比如 init,del 等. 这种命名惯例来自 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++ 中, 有定义构造函数和析构函数的特殊句法, 与定义常规函数或方法的句法不同.
而我真的不想为对象的特殊操作引入新句法, 因此, 我采用了一系列预定义的 "特殊方法", 比如 init 和 del. 用户可以通过实现这些方法来定义构造和析构过程.
同时, 我也用这种方法来支持用户对 Python 操作符的行为进行自定义. 如之前所说, Python 是用 C 语言实现的, 并通过函数指针来实现内置对象行为(如 "get attribute","add","call" 等). 为使用户自定义类也可以有这些行为, 我为这些函数指针也指定了一些特殊方法名称, 比如 getattr,add,call 等.
公众号: ReadingPython
来源: http://www.jianshu.com/p/2f18261af95f