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日凌晨于杭州家中

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

推荐阅读更多精彩内容

  • Python进阶-继承中的MRO与super @(Python)[python, python进阶] [TOC] ...
    理查德成阅读 1,542评论 2 10
  • 转至元数据结尾创建: 董潇伟,最新修改于: 十二月 23, 2016 转至元数据起始第一章:isa和Class一....
    40c0490e5268阅读 1,678评论 0 9
  • 两本不错的书: 《Python参考手册》:对Python各个标准模块,特性介绍的比较详细。 《Python核心编程...
    静熙老师哈哈哈阅读 3,354评论 0 80
  • Spring Cloud为开发人员提供了快速构建分布式系统中一些常见模式的工具(例如配置管理,服务发现,断路器,智...
    卡卡罗2017阅读 134,585评论 18 139
  • @(python)[笔记] 目录 前言 在python中,一切皆对象面向对象的程序设计的核心就是对象;面向对象的程...
    CaiGuangyin阅读 585评论 0 5