《python基础》~ 2*

python的下划线,继承规则

打星号原因之一是因为这篇文章内容太多,没有写完。二是因为这篇文章的难度深度很大,需要仔细揣摩才可以理解。由于大家对源码不熟悉,所以只好用通俗而晦涩的语言解释出来,抽空再做些补充(已经补充完毕)。

不要骗我,你是不是无数次的被python中坑爹的下划线弄的晕头转向的。不瞒你说我也曾经被它弄的痛不欲生,那么现在我就帮你分析总结一下python中的下划线到底代表什么意思,看过这篇总结以后,任何下划线都阻拦不了你reading的脚步。

变量:

python中的权限采用“你懂的”的规则,不会显式声明给你,规则如下:

  • 前面带“_”的变量,例如:_var,标明是一个私有类型的变量,但是只起到标明的作用,外部类还是可以访问到这个变量。但是带有这样的参数的变量或者包不能用from module import * 导入

  • 前后带两个“_”的变量,例如:__var,标明是内置私有变量,外部类无法访问,甚至是子类对象也无法访问。

  • 大写加下划线的变量:标明是不会发生改变的全局变量,例如:USER_CONSTANT,这好比c++中的constant。

函数:

  • 前面带_的变量:标明是私有函数,同理只是用于标明而已。

  • 前后两个_的函数,标明是特殊函数(一般是module内建函数,比如init函数,我们后面会讲到)

    注意:双下划线对解释器有特殊的意义,我们在命名的时候一定要尽量避 免这种命名方式

接下来你可以看看这样的一段代码,可能会涉及到python的继承方式,我们一并也都讲了,这段内容摘自CSDN某位博主,我就借花献佛了:

class A(object):
    def __init__(self):
        self.__private()
        self.public()
    def __private(self):
        print 'A.__private()'
    def public(self):
        print 'A.public()'
class B(A):
    def __private(self):
        print 'B.__private()'
    def public(self):
        print 'B.public()'
b = B()

那么这段代码的输出是什么呢?
答案是:A.__private() B.public()

你肯定会很奇怪,这样奇葩的输出到底是怎么回事儿,产生疑问的原因也很简单,因为你对python的机制还不了解,下面就进行分析(本来这一块是放在python高级编程里讲的,这里遇到了一并就都倒出来吧):

一切都从为什么会输出:A.__private()开始。我们还是来看一下python的命名规则

根据python tutorial的说法,变量名(标识符)是python的一种原子元素(什么是原子元素,你可以参照数据库操作的原子性来理解,也就是不可再分的),当变量命名被绑定到一个对象的时候,变量名就代指这个对象,当它出现在代码块中,它就是本地变量;当它出现在模块中它就是全局变量。

根据该理论,我们可以把上述代码块分为三个代码块:类A的定义、类B的定义和变量b的定义。类A定义了三个成员变量,类B定义了两个成员变量。当我们用dir(A)查看类A里面的东西的时候,我们发现了一些有趣的现象。

_A__private
__class__
__delattr__
__dict__
__doc__
__format__
__getattribute__
__hash__
__init__
__module__
__new__
__reduce__
__reduce_ex__
__repr__
__setattr__
__sizeof__
__str__
__subclasshook__
__weakref__
public

实际上,除去init以外,我们发现A中我们额外定义的几个类属性变成了这样:

_A__private
public

很明显,问题出在了__private()上。这里我们要深究一下python解释器对私有类型成员的处理细节。
当在类中定义了私有变量的时候,在代码正式生成以前,python会自动的将这个私有变量转换为长格式(变为共有)转换的机制是这样的:在变量前端插入类名,再在前端插入一个下划线字符。这就是所谓的私有变量轧压(跟我读:ya ya)(private name mangling)。这也就是类A中_A__private出现的原因。

(其实可以从另一个角度去理解,表面上它声明为私有变量而禁止别人访问但是私底下还是要给自己类内部的人访问的,所以要转换一下)

注意1:因为轧压会使得变量名字变长,所以一旦超过了255个字符以后,python会进行切断,要特别注意因此导致的命名冲突。
注意2:当类名全部以下划线命名的时候,轧压规则失效。
      尝试把上述类A换成“_____”

那么当类A经过轧压之后,它的代码变成了:

class A(object):
   def __init__(self):
      self._A__private()          # 这行变了
      self.public()
   def _A__private(self):           # 这行也变了
      print 'A.__private()'
   def public(self):
      print 'A.public()'

因为类B定义的时候没有覆盖init方法,所以调用的仍然是A.init,即执行了self._A__private().(这一点就涉及到下面要讲的python的继承了)

最后在讲继承之前还有一点特别的重要,刚刚dir()出来的一堆东西,除了我们自己定义的,还有系统为每个类自定义的属性和方法,那么它们有什么用呢?我们这里详细讲解一下:

自定义属性:

  • class.__doc__ 类型帮助信息
  • class.__name__ 类型名称
  • class.__dict__ 类型字典,存储所有该类实例成员的信息,如果这么说你不明白的话,请自行print 类名.dict观察。
  • class.__class__ 类类型。可能你会觉得奇怪,那是因为Python里面所有的东西都是对象,甚至比java都要面向对象。在《python源码分析》的时候我会讲到。
  • class.__module__ 类型所在的模块

自定义方法(也称保留方法):

目的                  代码                     实际调用
初始化              x=class()               x.__init__()
字符串官方表现       repr(x)                 x.__repr__()
字符串非正式         str(x)                  x.__str__()
字节数组非正式值      bytes(x)                x.__bytes__()
格式化字符串的值      
format(x,format_spec)    x.__format__(format_spec)

注意:
str()一般是将数值转成字符串。
repr()是将一个对象转成字符串显示,注意只是显示用,有些对象转成字符串没有直接的意思。如list,dict使用str()是无效的,但使用repr可以,这是为了看它们都有哪些值,为了显示之用。 类似于java的toString方法。

在最后,我们再给大家总结一下python中常见的下划线函数的作用以及其调用情况,这一部分要求多看多想。

part1:迭代器类似类

序号  目的  所编写代码   Python 实际调用
①   遍历某个序列  iter(seq)   seq.__iter__()
②   从迭代器中获取下一个值 next(seq)   seq.__next__()
③   按逆序创建一个迭代器  reversed(seq)   seq.__reversed__()

无论何时创建迭代器都将调用 __iter__() 方法。这是用初始值对迭代器进行初始化的绝佳之处。

无论何时从迭代器中获取下一个值都将调用 __next__() 方法。

__reversed__() 方法并不常用。它以一个现有序列为参数,并将该序列中所有元素从尾到头以逆序排列生成一个新的迭代器。  

part2 :计算属性 (这一部分非常难理解,有时间我再下面更新讲解)

序号  目的  所编写代码   Python 实际调用
①   获取一个计算属性(无条件的)  x.my_property   x.__getattribute__('my_property')
②   获取一个计算属性(后备) x.my_property
x.__getattr__('my_property')
③   设置某属性   x.my_property = value                   x.__setattr__('my_property',value)
④   删除某属性   del x.my_property               x.__delattr__('my_property')
⑤   列出所有属性和方法   dir(x)  x.__dir__()

如果某个类定义了 __getattribute__() 方法,在 每次引用属性或方法名称时 Python 都调用它(特殊方法名称除外,因为那样将会导致讨厌的无限循环)。

如果某个类定义了 __getattr__() 方法,Python 将只在正常的位置查询属性时才会调用它。如果实例 x 定义了属性 color, x.color 将 不会 调用x.__getattr__('color');而只会返回 x.color 已定义好的值。

无论何时给属性赋值,都会调用 __setattr__() 方法。

无论何时删除一个属性,都将调用 __delattr__() 方法。

如果定义了 __getattr__() 或 __getattribute__() 方法, __dir__() 方法将非常有用。通常,调用 dir(x) 将只显示正常的属性和方法。如果 __getattr()__方法动态处理 color 属性, dir(x) 将不会将 color 列为可用属性。可通过覆盖 __dir__() 方法允许将 color 列为可用属性,对于想使用你的类但却不想深入其内部的人来说,该方法非常有益。  

part3:可比较的类

序号  目的  所编写代码   Python 实际调用

相等  x == y  x.__eq__(y)

不相等 x != y  x.__ne__(y)

小于  x < y   x.__lt__(y)

小于或等于   x <= y  x.__le__(y)

大于  x > y   x.__gt__(y)

大于或等于   x >= y  x.__ge__(y)

布尔上上下文环境中的真值    if x:   x.__bool__()

如果定义了 __lt__() 方法但没有定义 __gt__() 方法,Python 将通过经交换的算子调用 __lt__() 方法。然而,Python 并不会组合方法。例如,如果定义了 __lt__() 方法和 __eq()__ 方法,并试图测试是否 x <= y,Python 不会按顺序调用 __lt__() 和 __eq()__ 。它将只调用__le__() 方法。  

part4:可序列化的类

Python 支持任意对象的序列化和反序列化。(多数 Python 参考资料称该过程为 “pickling” 和 “unpickling”)。该技术对与将状态保存为文件并在稍后恢复它非常有意义。所有的 内置数据类型 均已支持 pickling 。如果创建了自定义类,且希望它能够 pickle,阅读 pickle 协议 了解下列特殊方法何时以及如何被调用。

序号  目的  所编写代码   Python 实际调用

自定义对象的复制    copy.copy(x)    x.__copy__()

自定义对象的深度复制  copy.deepcopy(x)    x.__deepcopy__()

在 pickling 之前获取对象的状态    pickle.dump(x, file)    x.__getstate__()

序列化某对象  pickle.dump(x, file)    x.__reduce__()

序列化某对象(新 pickling 协议)   pickle.dump(x, file, protocol_version)  x.__reduce_ex__(protocol_version)

*   控制 unpickling 过程中对象的创建方式    x = pickle.load(file)   x.__getnewargs__()

*   在 unpickling 之后还原对象的状态  x = pickle.load(file)       x.__setstate__()

* 要重建序列化对象,Python 需要创建一个和被序列化的对象看起来一样的新对象,然后设置新对象的所有属性。__getnewargs__() 方法控制新对象的创建过程,而 __setstate__() 方法控制属性值的还原方式。

part5:something amazing

如果知道自己在干什么,你几乎可以完全控制类是如何比较的、属性如何定义,以及类的子类是何种类型。

序号  目的  所编写代码   Python 实际调用

类构造器    x = MyClass()   x.__new__()
*类析构器   del x   x.__del__()

只定义特定集合的某些属性    
x.__slots__()

自定义散列值  hash(x) x.__hash__()

获取某个属性的值    x.color type(x).__dict__['color'].__get__(x, type(x))

设置某个属性的值    x.color = 'PapayaWhip'  type(x).__dict__['color'].__set__(x, 'PapayaWhip')

删除某个属性  del x.color type(x).__dict__['color'].__del__(x)

控制某个对象是否是该对象的实例 your class  isinstance(x, MyClass)  MyClass.__instancecheck__(x)

控制某个类是否是该类的子类   issubclass(C, MyClass)  MyClass.__subclasscheck__(C)

控制某个类是否是该抽象基类的子类    issubclass(C, MyABC)    MyABC.__subclasshook__(C)

上面就简单科普python下划线的知识,接着我们谈谈python的重点问题:继承

继承之功能不赘述,继承之特点也是各有千秋,C++的多继承,java的单继承,父类方法的调用机制等等,基础不扎实也够喝一壶了。python继承机制与平时所理解的有所不同。

先来说说super,我们平时把父类也叫做超类,那么super也是调用父类属性(attribute)的时候时候所用到的关键字,对没错,关键字,但是super在python里面是一个内建类型,尽管它的使用方法和函数有点儿类似,但是它实际上还是一个内建类型。(什么是内建类型?顾名思义就是内部构建的类,诸如None,数值类型,字符串类型)

>>>super
<type 'super'>  

这就证实了我所言非虚,如果你已经习惯了直接调用父类并将self作为第一个参数,来访问类型的特性,super的用法可能让你有点儿混乱。(关于self的用法,我也会在后面的文章中作为补充,作为最基本的你就记着定义类方法一定要用self作为第一个参数)我们可以看看下面的代码:

>>> class Father(object):
...     def say(self):
...         print('Hello my child')
...
>>> class Child(Father):
...     def say(self):
...         Father.say(self)
...         print('clean your bedroom')
...
>>>Tom = Child()
>>>Tom.say()

Hello my child
clean your bedroom

上述这段代码是没有问题,但是你是不是有疑问在里面。粗心的人可能觉得这没问题啊,先显示调用父类,再调子类。没错,Father.say( )这一行确实调用了超类的say( )方法,将self作为第一个参数传入。但是它传递self,是Child的self。抛开这个疑问不管,我们再来看看如果要用super实现相同效果的话该怎么用:

>>>class Child(Father):
...    def say(self):
...        super(Father, self).say()
,,,        print 'clean your bedroom'  

如上所示,简单的二重继承,你可能觉得问题不大,但面对多重继承的时候,super无论是在使用上还是阅读上都是非常费力的,比较而言第一种不用super的方式还是比较符合继承逻辑的。

那么在如何避免使用super以及解释我们刚刚的问题之前,我们先要来看看python中的方法解析顺序(MRO)。这个较之于轧压,又有所不同。

在python2.3以前的版本(当然了,我们现在最常用的是2.7,但是官方力推的是3.0以上的版本,比如html解析器在3.0左右的版本就已经支持的不是很好了,我们在阅读一门语言规范的时候当然要比较一下各个版本的做出了哪一块儿的改进),类继承是按照旧的C3规则进行的。在旧的C3规则中,如果一个类有两个祖先,MRO计算很简单:

            class Base1           class Base2
                        \         /
                          \     /
                        class MyClass
>>>class Base1:
...    pass
...
>>>class Base2:
...    def method(self):
...        print 'Base2'
...
>>>class MyClass(Base1, Base2):
...    pass
...
>>>here.MyClass()
>>>here.method()
Base2  

以上的解析顺序是:当here.method被调用的时候,解释程序将查找MyClass中的方法,然后在Base1中查找,最后在Base2中查找。

好的,上面的内容可能没什么难事,因为它还在我们正常的认知范围内~那么现在我们在两个父类上面加一个公共类,然后你再猜一下代码的输出是什么。

                       class BaseBase
                            /  \
                           /    \
                 class Base1      class Base2
                           \    /
                            \  /
                        class MyClass
>>>class BaseBase:
...    def method(self):
...        print 'BaseBase'
...
>>>class Base1(BaseBase):
...    pass
...
>>>class Base2(BaseBase):
...    def method(self):
...        print 'Base2'
...
>>>class MyClass(Base1, Base2):
...    pass
...
>>>here = MyClass()
>>>here.method()

好了,你可以猜一下现在的输出是什么了,如果你猜不到,那就请继续耐心的看完这篇教程。

答案是:BaseBase

我们没有意图去解释旧的python规则中的这种继承现象,而且无论是源代码中还是实际应用中,这种继承方式也是极为罕见的。但正是由于这种旧的MRO规则会产生这种古怪的输出,2.3以后的较新版本中的输出变为了:Base2

如此古怪的输出结果,导致我们想问一个问题,Python的继承输出结果还是不是可以预测的?

我们来简单解释一下MRO的规则:MRO说白了其实就是一颗继承树

我们来检测一下MyClass中的继承顺序:

>>>def L(klass):
...        print [k.__name__ for k in klass.__mro__]
...
>>>L(MyClass)
['MyClass', 'Base1', 'Base2', 'BaseBase', 'object']

tips:  
你也可以直接用print MyClass.__mro__来查看,或者用MyClass.mro(),这就是上面我们讲到的下划线函数的实际调用问题。  

那么现在我们就能够明白,子父类同名方法的调用是要遵循MRO树的顺序的。然后你还需要记住的是python的调用都是显示声明的

希望没有把你绕晕,让我们回到super这里来

我们前面已经讲过了一种多重继承,当发生多重继承的时候,普通的调用可能也会陷入困境,那么super就更不必说,多重继承使用super是相当危险的,原因在于python类的初始化函数,更进一步在于python父类的初始化函数需要我们显示的调用,我们来看看这么一个历程,来自:http://fuhm.net/super-harmful

混用super和传统调用:
class A(object):
def __init__(self):
    print 'A'
    super(A,self).__init__()
    
class B(object):
    def __init__(self):
    print 'B'
    super(B,self).__init__()
    
class C(A,B):
    def __init__(self):
        print 'C'
    A.__init__(self)
    B.__init__(self)
    
print 'MRO: ', [x.__name__ for x in C.__mro__]
c = C()

大胆猜一下输出:
MRO: ['C', 'A', 'B', 'object']
C A B B

很诡异啊,为什么是这种输出,为什么多了一个B呢?MRO树里面的关系明明没有重复项,而且调用的顺序也是我们按照自己意愿声明的,为什么多了一个B?

原因是:C实例调用了A.__init_(self),这样一来,我们在A中的super(A,self).___init_____( )函数将调用B的构造程序,还不理解?那么我们看看上面的MyClass的调用顺序就一目了然了,如果它想调用根类的函数,它是按照优先兄弟的顺序来调用的。这么看来super在python的用法就特别的混乱。

我们的经验是如果你想将一个父类子类话,你应该先检查一下这个父类的mro特性,如果mro不存在,那么说明这个类是一个旧式的类,python还没有将它加入mro特性,那我们为了安全就应该避免使用super。

如果一个类_mro_特性,则快速的检查一下MRO所涉及的类的构造程序代码,如果到处都使用了super,那你也可以使用它,否则你就试着保持一致性而不要混用。

那么都使用super,拒绝混用会不会达到理想的效果呢?让我们来看下面一段代码:

>>>class BaseBase(object):
        def __init__(self):
            print 'basebase'
            super(BaseBase, self).__init__()
>>>class Base1(BaseBase):
        def __init__(self):
            print 'base1'
            super(Base1, self).__init__()
>>>class Base2(BaseBase):
        def __init__(self, arg):
            print 'base2'
            super(Base2, self)
>>>class MyClass(Base1, Base2):
        def __init__(self, arg):
        print 'my base'
        super(MyClass, self).__init__(arg)
        
>>>m = MyClass(10)

然后它不仅输出了my base,而且还输出了一堆错误,看了一下TypeError:  __init__() tales exactly 1 argument(2 given).

嗯哼,super大法也不是那哪儿都好用的,可能你说,不就是一个参数问题么,脑洞开一下,python是可以接受动态参数的,对啊,*args和**kw确实是可以解决这个问题呢。把上面的:

__init__(self, arg)、__init__(self)

全部改成:__init__(self, *args, **kw)

这样做确实解决了我们的bug,赞!得到正确的输出:
my base
base1
base2
basebase

但是这是一种糟糕的修复方法,因为它使所有的构造函数接受所有类型的参数,就好比屋顶到处都是漏洞,而且洞很大,什么灰尘垃圾虫子都能进来,同理,这会使我们的代码变得极度脆弱。

上面我们了解了super滥用导致的代码可读性,然后我们也看到了使用显示调用也并不能给我们带来多少优惠,其实我们应该抱着这样一个思想,本来🐶就不可能跑得比汽车快,你怎么鞭笞它,也是于事无补的。就像python本来对多继承支持的就不是很好,我们何必要强python所难呢。

所以当我们用到继承的时候,我们一定要提醒自己以下几点:

  • 一定要避免多重继承,如果非用不可,我们会有相应的设计模式替代。(后续再更新)
  • 如果不使用多继承,super的使用就必须一致,在类层次结构中,应该在所有的地方都使用super或者是彻底不使用它。
  • 不要混用老式类和新式类,如何辨别参见MRO。
  • 调用父类的时候必须检查类层次,为了避免出现任何问题,每次调用父类的时候我们不要偷懒,我们在终端可以快速的查看所涉及的MRO(使用方法上面已经说明了)
那么我们的继承,就先讲到这里。
    原文作者:牧师Harray
    原文地址: https://www.jianshu.com/p/13eda7932e5d
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞