“新式类” 与 “经典类”
我们所接触到的类,都是 “新式类”( new-style classes )。同时,在 Python 2.X 相关的版本中,还存在另外一种称之为 “经典类”( classic classes )的类。接下来,我们就来看看 “新式类” 和 “经典类” 在概念上的区别,以及由 “经典类” 到 “新式类” 的所引入的变化。
如何成为 “新式类”?
在 Python 2.X 中,我们将继承自 object 或其他内置类型的类称为 “新式类”,将除此之外的其他类称为 “经典类” 或 “旧式类”。在Python 3.X中,所有的类均隐式自动的继承自object,即所有的类都是 “新式类”。这一点总结起来,可以认为 “新式类” 在Python 2.X 中是可选的(是否继承自 object )功能,在 Python 3.X 是原生固有的功能。
更进一步,在 Python 2.X 中,若考虑到继承的存在,我们上面对于 “如何成为新式类” 的定义应该被进一步拓宽:在派生类的基类继承链中,只要在任一位置处出现了 object 或者内置类型,则该派生类便会成为 “新式类”。
# Python 2.7
>>> class A:
... pass
...
>>> class B(object):
... pass
...
>>> class C(A, B):
... pass
...
>>> a = A()
>>> b = B()
>>> c = C()
>>> a
<__main__.A instance at 0x7f19000c0dd0>
>>> b
<__main__.B object at 0x7f190010cd90>
>>> c
<__main__.C object at 0x7f19000cafd0>
何为 “新式类”?
“新式类” 所涉及的相关特性是从 Python 2.2 开始逐步引入的,我们之所以将其称为 “新式类”,是因为它们在语言特性层面上和之前的类(“经典类”)有着非常显著的变化,这些变化体现在OOP的各个方面。笼统地说,“新式类” 不仅在之前 “经典类” 的基础上从多个方面进行了特性上的修改,也在多个方面进行了特性上的扩展。我们下面就从细节上看看 “新式类” 的改变与新增之处。
“新式类” 的改变与新增
“新式类” 的改变
类和类型的合并
在 “新式类” 中,一个实例对象的类型便是实例化它的类,即 “新式类” 中类和类型的概念得到了统一。关于 “类” 和 “类型” 的统一,在前面 “一切皆对象” 的小节中简单的提到过,“在新式类中,类和类型的概念合并,类即是类型”,但我们并没有涉及到这个结论的相关细节性内容,接下来我们便更进一步,详细地看看这部分内容。在阅读和理解代码示例前,我们需要提前预习和回顾两个知识点,分别是元类、type() 和 obj.__class__:
简要的说,元类是一种可以拦截类创建的机制,它可以在 class 语句中插入创建类对象时自动执行的相关逻辑,主要用于管理和扩展类。type 类是实例化关系中最顶层的对象(它的另外一个角色是作为内置函数查看对象的类型),默认情况下类由 type 类创建,而元类则通常是 type 类的定制化子类;
type() 返回传入对象的类型,obj.__class__ 则返回类实例所属的类。在 “新式类” 中,type(obj) 和 obj.__class__两者的返回结果相同;而在 “经典类” 中,前者返回始终为 instance,而后者则会返回用户自定义的类对象。
我们依次看看下面三个示例。我们分别使用了 Python 2.X 中的 “经典类” 和 “新式类” 以及Python 3.X中的 “新式类”(Python 3.X 中的所有类均为 “新式类”)来进行测试:
# Python 2.X classic classes
>>> class Foo:
... pass
...
>>> foo = Foo()
>>> type(foo) # ①
<type 'instance'>
>>> foo.__class__
<class __main__.Foo at 0x7fb4662cee88>
>>> type(Foo) # ②
<type 'classobj'>
>>> Foo.__class__ # ③
AttributeError: class Foo has no attribute '__class__'
>>> a = 1
>>> type(a) # ④
<type 'int'>
>>> a.__class__
<type 'int'>
>>> type(int) # ⑤
<type 'type'>
>>> int.__class__
<type 'type'>
# Python 2.X new-style classes
>>> class Foo(object):
... pass
...
>>> foo = Foo()
>>> type(foo) # ①
<class '__main__.Foo'>
>>> foo.__class__
<class '__main__.Foo'>
>>> type(Foo) # ②
<type 'type'>
>>> Foo.__class__ # ③
<type 'type'>
>>> a = 1
>>> type(a) # ④
<type 'int'>
>>> a.__class__
<type 'int'>
>>> type(int) # ⑤
<type 'type'>
>>> int.__class__
<type 'type'>
对比上面 “经典类” 和 “新式类” 两个示例,我们分别以下面几点来进行说明(下面的标号和上方代码中的标号一一对应):
①“经典类” 的实例对象的类型名称是通用的名称 instance(对于其他自定义类的实例对象来说,它们的类型名称也是 instance)。而在 “新式类” 中,可以看到实例对象的类型便是它们的类,不同的类的实例对象的类型都是各异的;
②“经典类” 的类对象本身的类型名称是通用的名称 classobj,而 “新式类” 的类对象的类型名称是 type,这对应了我们在上面说的 “默认情况下类由 type 类创建”;
③在 “新式类” 中,由于元类的引入,类对象也具有了 __class__ 属性(这里返回的是创建它的 type 类),而在 “经典类” 中,类对象是不具有该属性的;
④⑤在 “经典类” 中,相比于用户自定义的类的实例对象的类型来说,内置类型的实例对象的类型并不是通用的名称,而是具体的内置类型,比如这里的 int,而对于 int 类的类型,也是 type 类,这一点和 “新式类” 中的表现是一致的。
# Python 3.X
>>> class Foo:
... pass
...
>>> foo = Foo()
>>> type(foo)
<class '__main__.Foo'>
>>> foo.__class__
<class '__main__.Foo'>
>>> type(Foo)
<class 'type'>
>>> Foo.__class__
<class 'type'>
>>> a = 1
>>> type(a)
<class 'int'>
>>> a.__class__
<class 'int'>
>>> type(int)
<class 'type'>
>>> int.__class__
<class 'type'>
对比上方 Python 3.X 中的示例,尽管在 Python 3.X 中,所有的类均已是 “新式类”,我们仍然可以看到一些细微的变化,用户自定义的类的实例对象和内置类型的实例对象在类型上的逻辑更加具有一致性了。“新式类” 中 “类” 与 “类型” 的统一,使得 Python 的类模型在一致性上更加完善。类是 type 类的实例,实例对象则是类的实例,type 和类(包括自定义的、内置的)均可以创建实例,并成为实例对象的类型。
基类 object
在前面的小节中,我们曾提到过,type 是实例化关系最顶层的对象,而 object 则是继承关系最顶层的对象,object 是所有新式类的基类。按照 Python 版本进行区分,Python 2.X 中 “新式类” 显式继承自 object,而在 Python 3.X 中 “新式类” 默认隐式的继承自 object。下面的两个示例对比了 “新式类” 和 “经典类” 在继承方面的不同,在 “新式类” 中,Foo 的基类元组 bases 包含 object 类对象,而在 “经典类” 中则没有,并且内置类型及其实例对象也直接或间接地继承自 object。
# Python 2.X classic classes
>>> class Foo:
... pass
...
>>> Foo.__bases__
()
# Python 2.X new-style classes
>>> class Foo(object):
... pass
...
>>> foo = Foo()
>>> Foo.__bases__
(<type 'object'>,)
>>> isinstance(foo, object), isinstance(Foo, object)
(True, True)
>>> isinstance(int, object), isinstance(dict, object)
(True, True)
另外,在 object 基类中,默认定义了一些方法,所有 “新式类” 都会自动的继承这些方法。
>>> dir(object)
['__class__', '__delattr__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']
内置运算方法的搜索与属性访问拦截方法
在 Python 中,通用的拦截实例属性访问的方法包括 __getattr__( object.__getattr__(self, name) )和 __getattribute__( object.__getattribute__(self, name) )两种。我们在这里仅简要地说明它们的基本作用,而对于这两种方法的详细内容会在后面属性管理技术相关的小节中再进行涉及。
对于 __getattr__ 来说, 当在实例中访问某个属性失败时(属性访问引发 AttributeError )该方法会被调用,若该属性通过正常的机制被搜索到,便不会调用该方法,即该方法仅在属性未定义时才会被调用。对于 __getattribute__ 方法来说,当在实例中访问任意属性时该方法都会被调用,即该方法针对于所有属性的访问均会运行,当 __getattr__ 和 __getattribute__ 方法同时存在的时候,通常 __getattr__ 方法只有在 __getattribute__ 方法引发 AttributeError 时才会被调用。注意,和前面提到的 __setattr__ 相同,__getattribute__ 也是一个相当 “危险” 的方法,稍有不慎便会造成无限递归进而耗尽内存,这部分内容在后面的小节再来讨论。接下来,我们通过两个简单的示例看看它们的作用:
# __getattr__
>>> class Foo:
... def __init__(self, data):
... self.data = data
...
... def __getattr__(self, name):
... return 'default'
...
>>> foo = Foo(10)
>>> foo.data
10
>>> foo.method
'default'
# __getattribute__
>>> class Foo:
... def __init__(self, data):
... self.data = data
...
... def __getattribute__(self, name):
... return 'default'
...
>>> foo = Foo(10)
>>> foo.data
'default'
>>> foo.method
'default'
那么在了解了这两种方法的基本作用后,我们便来谈谈它们在新式类中的变化。在 “新式类” 中,对实例对象使用内置运算表达式(比如索引运算 x[0]、加法算术运算 x + 1 等)时,拦截实例属性访问的 __getattr__ 方法和 __getattribute__ 方法将不再拦截对应的运算符重载方法 __X__(比如索引运算对应的 __getitem__ 方法、加法算术运算对应的 __add__ 方法等)。
我们参考下面的代码及其执行结果,可以对 “经典类” 到 “新式类” 的这个变化有一个更加直观的体会(由于 __getattribute__ 方法是在新式类中新增的特性,所以为了对比 “经典类” 和 “新式类” 起见,我们这里选择以__getattr__为例进行说明。注意,最好将下面的代码在 IDE 或者脚本文件中运行(而不是在交互式终端中运行),这样可以避免终端中的默认功能在 __getattr__ 方法下对最终输出结果有可能的影响):
# Python 2.X classic classes
class Foo:
data = 10
def __getattr__(self, item):
print(item)
return getattr(self.data, item)
foo = Foo()
print(foo)
try:
print foo + 1
except Exception as e:
print e
__str__
10
__coerce__
11
可以看到,在 “经典类” 中,当未定义内置运算(这里指的是打印和加法算数运算)对应的特殊方法时(这里可能是 __str__、 __add__ 等方法,但并不仅限于这两种方法,在运算符重载的小节中会提到更详细的内容),作用在实例对象上的打印和加法算数运算,会被 __getattr__ 拦截。这里简要解释一下上述 __coerce__ 的含义,这是一个已经废弃的方法:对于带有两个操作数 x 和 y 的任何数值运算,都会调用 x.__coerce__(y),它返回一个将 x 和 y 转换为可接受类型的元组,如果转换失败,则返回 None。接下来,我们再看看同样的示例在 “新式类” 上的表现:
# Python 2.X new-style classes
class Foo(object):
data = 10
def __getattr__(self, item):
print(item)
return getattr(self.data, item)
foo = Foo()
print(foo)
try:
print foo + 1
except Exception as e:
print e
<__main__.Foo object at 0x7f7afb74b7d0>
unsupported operand type(s) for +: 'Foo' and 'int'
在 “新式类” 中,打印和加法算数运算这两种内置运算的表现与 “经典类” 是明显不同的。首先,打印操作并未通过 __getattr__ 被路由到 self.data 上,而是调用了对象默认的 “官方” 字符串表示方法( 即 __repr__ );其次,加法算术运算 foo + 1 并未被 __getattr__ 拦截,而是引发了 TypeError 异常。可以看到,“新式类” 中,属性访问拦截方法 __getattr__(及 __getattribute__ )不会拦截内置运算对应的运算符重载方法。
更准确的说,以下方示例中的 foo[0] 为例,在 “经典类” 中,foo[0] 相当于 foo.__getitem__(0),而在 “新式类” 变更后,foo[0] 相当于 foo.__class__.__getitem__(foo, 0),即对 __getitem__ 方法的搜索在 “新式类” 中略过了实例而从类直接开始,所以 getattr 在这种情况下不会进行拦截。
# Python 2.X new-style classes
class Foo(object):
data = 'test'
def __getattr__(self, item):
print(item)
return getattr(self.data, item)
print(foo[0]) # 输出:TypeError: 'Foo' object does not support indexing
print(foo.__class__.__getitem__(foo, 0)) # 输出:AttributeError: type object 'Foo' has no attribute '__getitem__'
再次注意, __getattr__ 仅作用于未定义的方法,如果我们在类中定义了内置运算对应的特殊方法,则无论是在 “新式类” 还是 “经典类” 中,__getattr__ 都不会被调用:
# new-style classes
>>> class Foo(object):
... data = 10
...
... def __getattr__(self, item):
... print(item)
... return getattr(self.data, item)
...
... def __add__(self, other):
... return 0
...
>>> foo = Foo()
>>> foo + 1
0
不同于上面说明的内置运算表达式,__getattr__ (及 __getattribute__ )对于其他常规属性的获取以及内置运算对应的运算符重载方法的显式调用仍然会起到预期的作用,参考下面的示例:
class Foo(object):
data = 'test'
def __getattr__(self, item):
if item.startswith('__'):
return getattr(self.data, item)
else:
return item
foo = Foo()
print(foo.__getitem__(0)) # 输出 t
print(foo.undefined) # 输出 undefined
总的来说,造成这个改动的原因可以总结为两个方面,一方面,内置运算对应的方法的搜索略过了实例而从类直接开始,减少了搜索方法的开销以及内置运算方法被调用的次数,在一定程度上优化了程序的执行;另一方面的原因则和元类有关,这些内容目前对于我们来说有一些晦涩难懂,所以我们把它放在后面的小节中再来涉及。
那么,面对这个 “经典类” 到 “新式类” 的变动,对我们在 “新式类” 下编写OOP代码会有什么影响呢?在需要实例对象实现内置运算的场景下,我们需要在 “新式类” 中手动的实现(或借助相关OOP的第三方库工具等)相关内置运算对应的运算符重载方法,比如上面提到的 __getitem__ 方法等。
钻石继承( diamond pattern )中的搜索顺序
在前面我们简单的描述了多继承的概念,而这里提到的钻石继承是多继承中的一个特例,鉴于多继承(包括钻石继承和方法解析顺序MRO)会在后面的小节中被详细描述,所以这里我们暂时只需要了解钻石继承中的搜索顺序在 “经典类” 到 “新式类” 的过程中发生了变化即可。
文章持续更新,可以微信搜索「极客夜读 」第一时间阅读,更多Python学习文章,大家的关注就是更新的动力!