Python进阶(六)

在讨论完Callable相关的部分之后,我们继续来看一下Class相关的两个东西——Property和super。

9. Property

这部分其实比较简单,属于Python的基本内容,也是工作中经常用到,比如做属性封装,在属性访问的时候添加一些自己想要做的事情。直接看一个简单的代码例子。

class A(object):
    def __init__(self, a, b):
        self._a = a
        self._b = b
        
    def _get_a(self):
        return self._a
    def _set_a(self, a):
        self._a = a
    a = property(_get_a, _set_a)
    
    def _get_b(self):
        return self._b
    b = property(_get_b)
    
a = A(1, 2)
a1 = A.a
a2 = A.a
a3 = a.a
a4 = a.a

print id(a1), id(a2), id(a3), id(a4), id(a._a)
print a1, a2, a3, a4
print a1.fget, a1.fset

输出结果为

39712928 39712928 39745760 39745760 39745760
<property object at 0x025DF8A0> <property object at 0x025DF8A0> 1 1
<function _get_a at 0x02C677F0> <function _set_a at 0x02C677B0>

我们担心会有类似bound method或者unbound method的机制,所以尝试用两个变量来保存A.a和a.a,发现他们的id并不会变化,说明它们在Ptyhon中是一个稳定的对象,而非在需要是动态创建的。当然,A.a和a.a并不是同一个对象,把他们分别输出出来就可以看到,A.a是一个property对象,而a.a直接访问到了a对象身上的_a属性。通过id方法可以看到a3、a4和a._a其实是一同一个对象。
关键词property其实是一个class,当访问它的时候,代理对象会用对应的方法进行替换,支持的方法包括fget、fset和fdel。

10. super

对于super这个关键词的使用其实算是Python的基础知识了,我们从一个简单的例子来探究一下,Python为什么从2.2版本加入这个关键词,它的使用带来我们什么样的便利和困扰?
先来看最为简单的例子:

class A(object):
    def __init__(self):
        print 'in A'
        
class B(A):
    def __init__(self):
        print 'in B'
        A.__init__(self)
        
class C(A):
    def __init__(self):
        print 'in C'
        super(C, self).__init__()
        
b = B()
c = C()

print super(A, c)

输出结果:

in B
in A
in C
in A
<super: <class 'A'>, <C object>>

结果很简单,无需过多的解释,输出的信息显示了构造的过程,唯一需要指出的是super是一种buildin class。那么它是怎样工作的呢?也就是构造B的时候是如何确定它要调用到的父类方法的呢?
在C++中,有虚函数表的结构,用于实现继承中动态多态的特性。在Python中,有__mro__属性来描述继承关系。什么是mro呢?mro是method resolution order,直译是“方法解决顺序”,但是这个不能很好地表达英文原文的含义,简单来说Python用它来解析方法的调用顺序。我们通过具体的例子来看一下。

class A(object):
    def __init__(self):
        print 'in A'
        
    def foo(self):
        pass
        
class B(A):
    def __init__(self):
        super(B, self).__init__()
        
    def foo(self):
        pass
        
b = B()     # in A

print B.__mro__     # (<class '__main__.B'>, <class '__main__.A'>, <type 'object'>)

可以看到B.__mro__是一个元组,记录了对于B这个类来说的继承关系。在运行时,当调用b.foo方法的时候,会按照mro的顺序去依次查找含有此方法定义的对象或者类,直到找到为止,或者未找到会报错。注意这个过程是在运行时的,而非编译时,这也是python方法调用比较慢的原因之一。
super关键字也是通过这个数据结构来进行方法调用的,不同的是原理上,在存在的前提下,它会依次调用整个mro结构上的类上对应的方法。

思考__mro__存在在哪里?

看上去mro属性是一个数据,它似乎应该存储在B的身上,但是我们查看B的__dict__属性的时候发现其中并没有__mro__这个名称。

print dir(B)
# ['__class__', '__delattr__', '__dict__', '__doc__', '__format__', '__getattribute__', '__hash__', '__init__', '__module__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'foo']

似乎又有神奇的事情发生了,B.__mro__是如何访问到的呢?我们知道,在没有定义metaclass的情况下,所有的类对象(类对象,并非类实例对象)都是一个type对象,那我们来用dir看一下type:

print dir(type)
#['__abstractmethods__', '__base__', '__bases__', '__basicsize__', '__call__', '__class__', '__delattr__', '__dict__', '__dictoffset__', '__doc__', '__eq__', '__flags__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__instancecheck__', '__itemsize__', '__le__', '__lt__', '__module__', '__mro__', '__name__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasscheck__', '__subclasses__', '__subclasshook__', '__weakrefoffset__', 'mro']

Bingo!不但有__mro__,还有一个mro属性。
这里关于为什么放在type里,有一个stackoverflow的讨论可以参考下,本文不进行详述了:
Why does mro not show up in dir(MyClass)?

接着我们来回答第一个问题——Python为什么从2.2版本加入这个关键词?
在更早的版本中,当调用父类方法的时候,比如上面B继承自A的例子,需要手动调用A.__init__(self),这样在需要重构A的时候,比如修改名称等,需要把所有继承自A的类中的代码都进行修改,这其实是不合理的一种设定。因此引入了super关键词处理这个过程,同时也为了实现super的功能添加了mro的结构。
在单继承结构下,mro可以很好的工作,多继承的情况下会否有什么问题呢?依然看一个简单的例子,C继承自A和B:

class A(object):
    def __init__(self):
        print 'enter A'
        # super(A, self).__init__()
        print 'level A'

    def foo(self):
        pass
        
class B(object):
    def __init__(self):
        print 'enter B'
        # super(B, self).__init__()
        print 'level B'
        
    def foo(self):
        pass

class C(A, B):
    def __init__(self):
        print 'in C'
        super(C, self).__init__()
        
c = C()
m = c.foo
print C.__mro__
print m.im_func, A.foo.im_func, B.foo.im_func

输出结果:

in C
enter A
level A
(<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <type 'object'>)
<function foo at 0x029478B0> <function foo at 0x029478B0> <function foo at 0x02947830>

不太对哦,按照我们想要的结果,应当是通过super可以调用到父类A和父类B中的初始化方法才对!但是结果只调用到了A的,为什么?
细心的读者已经发现了,A和B中注释掉了两行super相关的代码,把它们的注释取消掉,输出结果就变成了:

in C
enter A
enter B
level B
level A
(<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <type 'object'>)
<function foo at 0x02A37930> <function foo at 0x02A37930> <function foo at 0x02A378B0>

这样就可以调用到了A和B的初始化方法,注意,这里的调用过程是嵌套的。具体这是为什么,我这里偷懒,请参考关于super的经典文章:Python’s super() considered super!,或者参考知乎的问题讨论:Python中既然可以直接通过父类名调用父类方法为什么还会存在super函数?

结论:super基于mro可以处理多重继承的调用关系,按照拓扑结构来进行,同层继承中遵守从左到右的继承顺序。但是,要注意所有的父类方法都正确使用super来进行调用,即使是直接继承自object的对象!否则可能产生方法调用不到的现象。

需要额外指出的是,对于参数数目或者类型不同的情况,通过super的调用可能会导致trace,比如下面的例子就会有错误。

class A(object):
    def __init__(self, p1):
        pass
        
class B(A):
    def __init__(self, p1, p2):
        super(B, self).__init__(p1)
        
b = B(1, 2)
print b

class C(A):
    def __init__(self):
        super(C, self).__init__(0)
        
class D(B, C):
    def __init__(self, p1, p2):
        super(D, self).__init__(p1, p2)
        
d = D(1, 2)

print D.__mro__

在原本A、B和C的继承结构可以正常工作的情况下,加入D,就会导致TypeError,解决的方法只能是按照最多的参数或者dict这种容器来设计函数的参数以兼容多种情况。

总结:在Python中,即使引入了super关键字和mro的机制来处理继承的情况,对于复杂的多重继承甚至菱形继承,还会出现很多问题,包括维护上和运行时的问题,因此本质上还是使用类似面向接口编程的方式来从根本上避免这种问题更好。

PS:关于Python进阶的问题已经写了六篇了,在整理记录的过程中,我发现了一些听课时没有考虑过的新问题,也通过阅读其他的文档掌握新的知识,在简书上也收到了不少喜欢和关注。感谢引导我学习给我们讲课的同事大雄,然后感谢每一个阅读的读者,你有收获或者思考,我们彼此花的时间就都值得了。
关于基本的语言特性部分基本就包括上面的1-10个小节的内容,后面的课程内容关于gc和C++绑定等部分的,看时间慢慢整理。

2016年7月11日凌晨于杭州家中

    原文作者:董夕
    原文地址: https://www.jianshu.com/p/941045b4ce2d
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞