伯乐在线注:本文作者 Armin Ronacher 是知名 Python 框架 Flask 的开发者。文章写于 2014 年 8 月。
我不是Python 3的粉,也不喜欢这门语言的发展方向,这都不是什么秘密了。这也导致了最近几个月,铺天盖地邮件询问我,我到底想要Python怎样。所以我觉得我应该公开分享一下我的想法,给将来的程序语言设计者提供一些灵感。:)
显然Python作为一门编程程序,并不完美。然而,让我感到沮丧的是,这门语言大多数问题都与解释器的细节有关,很少是语言本身。然而这些解释器的细节,正在变成语言的一部分,这也是为什么这一点是很重要的。
我希望能带你们一起遨游,从解释器中古怪的槽(slots)开始,到python语言设计中最大的错误结束。如果反响比较好的话,以后还会有更多类似的文章。
大体上讲,这些文章将会探索解释器中的设计决策以及这些决策是如何影响解释器和语言的。我相信从程序语言设计的角度来看本文,会比将其作为对python将来如何发展的建议会更有趣。
语言vs实现
完成本文的初版后,我又添加了这段文字,因为我觉的我们在很大程度上忽视了,Python作为一种语言,CPython作为一种解释器,他们之间并没有开发者认为的那样分得开。
虽说有语言规范,但是大多数情况下,它只是规定了解释器要做什么,甚至连这些都没有规定。
在这种特定的情况下,解释器的这种晦涩的实现细节,改变或是影响了语言的设计,并且迫使Python的其他实现方式要去适应它。比如我猜想PyPy并不知道什么是槽(slots),但是仍然要操作它,就好像槽(slots)是解释器的一部分一样。
槽(Slots)
目前为止,我对专门语言最大的意见就是愚蠢的槽(slots)系统。我指的不是__slot__
,而是特殊方法的内部类型槽。这些槽(slots)是语言的“特性”,这点在很大程度上是错误的,因为你很少需要去关心它。也就是说,我认为,槽(slots)存在的这一事实,是这门语言最大的问题。
那么槽(slots)是什么东西呢?slot是解释器内部实现时产生的副作用。每个Python程序员都知道所谓的“魔术方法”,比如__add__
。这些方法以两个下划线开头,后面是这种特殊方法的名称,然后又是两个下划线。正如每位开发者所知,a+b就相当于a.__add__(b)
不幸的是,这是一个谎言
Python实际上并不是这样工作的。现如今的Python内部完全不是这样工作的。这里我们大致讲解解释器是如何工作的:
- 当一个类型被创建后,解释器会寻找类里面所有的描述符,同时还会寻找类似
__add__
这样的特殊方法。 - 对于每一个特殊方法,解释器找到它,把描述符的引用放在类型对象的一个预定义的槽(slots)中。
例如,特殊方法add对应两个内部槽(slots),tp_as_number->nb_add 和 tp_as_sequence->sq_concat.
- 当解释器想求a+b的值时,它会进行类似于TYPE_OF(a)->tp_as_number->nb_add(a, b)的调用(实际上比这复杂的多,因为
__add__
实际上有多个槽)
因此,表面上看a+b
类似于type(a).__add__(a, b)
,但是即使这样还是不准确的,这点你可以从槽(slots)的处理中看出来。你自己就可以轻易的证明这件事,通过在元类里实现
__getattribute__
并尝试连接一个自定义的__add__
。你会发现,它永远不会被调用。
我认为槽(slots)系统简直荒唐透顶。对于一些特定的类型(比如整型),它是一种优化,但是对于其他的类型则是毫无意义的。
为了证明这一点,请看这种完全无意义的类型(x.py
):
1 2 3 |
class A(object): def __add__(self, other): return 42 |
因为我们有一个__add__
方法,解释器会在槽(slots)中建立它。速度有多快呢?我们做a+b的时候,会用到槽(slots),下面是执行所用时间:
1 2 |
$ python3 -mtimeit -s 'from x import A; a = A(); b = A()' 'a + b' 1000000 loops, best of 3: 0.256 usec per loop |
然而如果我们使用a.__add__(b)
,则会绕过槽(slots)系统。解释器会去查看实例字典(在那里它什么也找不到),然后取查看类型字典,它将找到这个方法。下面是执行所需的时间
1 2 |
$ python3 -mtimeit -s 'from x import A; a = A(); b = A()' 'a.__add__(b)' 10000000 loops, best of 3: 0.158 usec per loop |
你敢信吗:不使用槽(slots)的版本实际上更快。这是什么魔力?这一现象的原因我不是很明白,但是这种情况已经持续了很久很久了。实际上,对运算符来讲旧式的类(旧式类不含有槽(slots))比新式的类速度要快很多,并且有更多的特性。
更多的特性?没错,因为旧式的类可以这样做(Python 2.7):
1 2 3 4 5 6 7 8 9 10 11 12 |
>>> original = 42 >>> class FooProxy: ... def __getattr__(self, x): ... return getattr(original, x) ... >>> proxy = FooProxy() >>> proxy 42 >>> 1 + proxy 43 >>> proxy + 1 43 |
是的。对于一个复杂的类型系统而言,如今我们拥有的特性比Python2还少。因为上面的那段代码,是不能新式类中使用的。
1 2 3 4 5 6 7 8 9 10 11 |
>>> import sys >>> class OldStyleClass: ... pass ... >>> class NewStyleClass(object): ... pass ... >>> sys.getsizeof(OldStyleClass) 104 >>> sys.getsizeof(NewStyleClass) 904 |
如果考虑到旧式的类是如此的轻量级,情况实际上更坏。
槽(slots)是哪来的?
这就提出了一个问题:为什么会存在槽(slots)。据我所知,槽(slots)的存在仅仅是一个历史遗留问题。当Python的解释器最初发明之时,像字符串等内建类型,被实现为全局变量,并且静态分配了结构体,用来存放一个类型所需的全部的特殊方法。
这都是在__add__
出现之前。如果你看一下1990年的Python,你可以看到当时的对象是如何创建的。
比如说,整数看上去是这样的:
1 2 3 4 5 6 7 8 9 10 |
static number_methods int_as_number = { intadd, /*tp_add*/ intsub, /*tp_subtract*/ intmul, /*tp_multiply*/ intdiv, /*tp_divide*/ intrem, /*tp_remainder*/ a href="http://hao.jobbole.com/flask/" target="_blank">Flask 的开发者。文章写于 2014 年 8 月。
我不是Python 3的粉,也不喜欢这门语言的发展方向,这都不是什么秘密了。这也导致了最近几个月,铺天盖地邮件询问我,我到底想要Python怎样。所以我觉得我应该公开分享一下我的想法,给将来的程序语言设计者提供一些灵感。:) 显然Python作为一门编程程序,并不完美。然而,让我感到沮丧的是,这门语言大多数问题都与解释器的细节有关,很少是语言本身。然而这些解释器的细节,正在变成语言的一部分,这也是为什么这一点是很重要的。 我希望能带你们一起遨游,从解释器中古怪的槽(slots)开始,到python语言设计中最大的错误结束。如果反响比较好的话,以后还会有更多类似的文章。 大体上讲,这些文章将会探索解释器中的设计决策以及这些决策是如何影响解释器和语言的。我相信从程序语言设计的角度来看本文,会比将其作为对python将来如何发展的建议会更有趣。
语言vs实现完成本文的初版后,我又添加了这段文字,因为我觉的我们在很大程度上忽视了,Python作为一种语言,CPython作为一种解释器,他们之间并没有开发者认为的那样分得开。 虽说有语言规范,但是大多数情况下,它只是规定了解释器要做什么,甚至连这些都没有规定。 在这种特定的情况下,解释器的这种晦涩的实现细节,改变或是影响了语言的设计,并且迫使Python的其他实现方式要去适应它。比如我猜想PyPy并不知道什么是槽(slots),但是仍然要操作它,就好像槽(slots)是解释器的一部分一样。
槽(Slots)目前为止,我对专门语言最大的意见就是愚蠢的槽(slots)系统。我指的不是 那么槽(slots)是什么东西呢?slot是解释器内部实现时产生的副作用。每个Python程序员都知道所谓的“魔术方法”,比如 不幸的是,这是一个谎言 Python实际上并不是这样工作的。现如今的Python内部完全不是这样工作的。这里我们大致讲解解释器是如何工作的:
例如,特殊方法add对应两个内部槽(slots),tp_as_number->nb_add 和 tp_as_sequence->sq_concat.
因此,表面上看 我认为槽(slots)系统简直荒唐透顶。对于一些特定的类型(比如整型),它是一种优化,但是对于其他的类型则是毫无意义的。 为了证明这一点,请看这种完全无意义的类型(
因为我们有一个
然而如果我们使用
你敢信吗:不使用槽(slots)的版本实际上更快。这是什么魔力?这一现象的原因我不是很明白,但是这种情况已经持续了很久很久了。实际上,对运算符来讲旧式的类(旧式类不含有槽(slots))比新式的类速度要快很多,并且有更多的特性。 更多的特性?没错,因为旧式的类可以这样做(Python 2.7):
是的。对于一个复杂的类型系统而言,如今我们拥有的特性比Python2还少。因为上面的那段代码,是不能新式类中使用的。
如果考虑到旧式的类是如此的轻量级,情况实际上更坏。
槽(slots)是哪来的?这就提出了一个问题:为什么会存在槽(slots)。据我所知,槽(slots)的存在仅仅是一个历史遗留问题。当Python的解释器最初发明之时,像字符串等内建类型,被实现为全局变量,并且静态分配了结构体,用来存放一个类型所需的全部的特殊方法。 这都是在 比如说,整数看上去是这样的:
|