Python中类的全面分析(下)

  • A+

Python中类的全面分析(上)

类的继承

Python 是面向对象语言,支持类的继承(包括单重和多重继承),继承的语法如下:

  1. class DerivedClass(BaseClass1, [BaseClass2...]):
  2.     <statement-1>
  3.     .
  4.     <statement-N>

子类可以覆盖父类的方法,此时有两种方法来调用父类中的函数:

  1. 调用父类的未绑定的构造方法。在调用一个实例的方法时,该方法的self参数会被自动绑定到实例上(称为绑定方法)。但如果直接调用类的方法(比如A.init),那么就没有实例会被绑定。这样就可以自由的提供需要的self参数,这种方法称为未绑定(unbound)方法。大多数情况下是可以正常工作的,但是多重继承的时候可能会重复调用父类。
  2. 通过 super(cls, inst).method() 调用 MRO中下一个类的函数,这里有一个非常不错的解释,看完后对 super 应该就熟悉了。

未绑定(unbound)方法调用如下:

  1. class Base(object):
  2.     def __init__(self):
  3.         print("Base.__init__")
  4. class Derived(Base):
  5.     def __init__(self):
  6.         Base.__init__(self)
  7.         print("Derived.__init__")

supper 调用如下:

  1. class Base(object):
  2.     def __init__(self):
  3.         print "Base.__init__"
  4. class Derived(Base):
  5.     def __init__(self):
  6.         super(Derived, self).__init__()
  7.         print "Derived.__init__"
  8. class Derived_2(Derived):
  9.     def __init__(self):
  10.         super(Derived_2, self).__init__()
  11.         print "Derived_2.__init__"

继承机制 MRO

MRO 主要用于在多继承时判断调用的属性来自于哪个类。Python2.2以前的类为经典类,它是一种没有继承的类,实例类型都是type类型,如果经典类被作为父类,子类调用父类的构造函数时会出错。这时MRO的方法为DFS(深度优先搜索),子节点顺序:从左到右。inspect.getmro(A)可以查看经典类的MRO顺序。

Python中类的全面分析(下)

两种继承模式在DFS下的优缺点:

第一种,两个互不相关的类的多继承,这种情况DFS顺序正常,不会引起任何问题;

第二种,棱形继承模式,存在公共父类(D)的多继承,这种情况下DFS必定经过公共父类(D)。如果这个公共父类(D)有一些初始化属性或者方法,但是子类(C)又重写了这些属性或者方法,那么按照DFS顺序必定是会先找到D的属性或方法,那么C的属性或者方法将永远访问不到,导致C只能继承无法重写(override)。这也就是新式类不使用DFS的原因,因为他们都有一个公共的祖先object。

为了使类和内置类型更加统一,Python2.2版本引入了新式类。新式类的每个类都继承于一个基类,可以是自定义类或者其它类,默认承于object,子类可以调用父类的构造函数。可以用 A.__mro__ 可以查看新式类的顺序。

在 2.2 中,有两种MRO的方法:

  1. 如果是经典类MRO为DFS;
  2. 如果是新式类MRO为BFS(广度优先搜索),子节点顺序:从左到右。

Python中类的全面分析(下)

新式类两种继承模式在BFS下的优缺点:

第一种,正常继承模式。比如B明明继承了D的某个属性(假设为foo),C中也实现了这个属性foo,那么BFS明明先访问B然后再去访问C,但是A的foo属性是c,这个问题称为单调性问题。

第二种,棱形继承模式,BFS的查找顺序解决了DFS顺序中的只能继承无法重写的问题。

因为DFS 和 BFS 都存在较大的问题,所以从Python2.3开始新式类的 MRO采用了C3算法,解决了单调性问题,和只能继承无法重写的问题。MRO的C3算法顺序如下图:

Python中类的全面分析(下)

C3 采用图的拓扑排序算法,具体实现可以参考官网文档。

多态

多态即多种形态,在运行时确定其状态,在编译阶段无法确定其类型,这就是多态。Python中的多态和Java以及C++中的多态有点不同,Python中的变量是动态类型的,在定义时不用指明其类型,它会根据需要在运行时确定变量的类型。

Python本身是一种解释性语言,不进行预编译,因此它就只在运行时确定其状态,故也有人说Python是一种多态语言。在Python中很多地方都可以体现多态的特性,比如内置函数len(object),len函数不仅可以计算字符串的长度,还可以计算列表、元组等对象中的数据个数,这里在运行时通过参数类型确定其具体的计算过程,正是多态的一种体现。

特殊的类方法

类中经常有一些方法用两个下划线包围来命名,下图给出一些例子。合理地使用它们可以对类添加一些“魔法”的行为。

Python中类的全面分析(下)

构造与析构

当我们调用 x = SomeClass() 的时候,第一个被调用的函数是 __new__ ,这个方法创建实例。接下来可以用 __init__ 来指明一个对象的初始化行为。当这个对象的生命周期结束的时候, __del__ 会被调用。

  • __new__(cls,[...]) 是对象实例化时第一个调用的方法,它只取下 cls 参数,并把其他参数传给init。
  • __init__(self,[...]) 为类的初始化方法。它获取任何传给构造器的参数(比如我们调用 x = SomeClass(10, ‘foo’) ,init 函数就会接到参数 10 和 ‘foo’) 。
  • __del__(self):new和init是对象的构造器, del则是对象的销毁器。它并非实现了语句 del x (因此该语句不等同于x.__del__()),而是定义当对象被回收时的行为。

当我们创建一个类的实例时,首先会调用new创建实例,接着才会调用init来进行初始化。不过注意在旧式类中,实例的创建并没有调用new方法,如下例子:

  1. class A:
  2.     def __new__(cls):
  3.         print "A.__new__ is called"  # -> this is never called
  4. A()

对于新式类来说,我们可以覆盖new方法,注意该方法的第一个参数cls(其实就是当前类类型)用来指明要创建的类型,后续参数用来传递给init进行初始化。如果new返回了cls类型的对象,那么接下来调用init,否则的话不会调用init(调用该方法必须传递一个实例对象)。

  1. class A(object):  # -> don't forget the object specified as base
  2.     def __new__(cls):
  3.         print "A.__new__ called"
  4.         return super(A, cls).__new__(cls)
  5.     def __init__(self):
  6.         print "A.__init__ called"
  7. A()
  8. # A.__new__ called
  9. # A.__init__ called

这里我们调用super()来获取 MRO 中A的下一个类(在这里其实就是基类 object)的new方法来创建一个cls的实例对象,接着用这个对象来调用了init。下面的例子中,并没有返回一个合适的对象,所以并没有调用init:

  1. class Sample(object):
  2.     def __str__(self):
  3.         return "SAMPLE"
  4. class A(object):
  5.     def __new__(cls):
  6.         print "A.__new__ called"
  7.         return super(A, cls).__new__(Sample)
  8.         # return Sample()
  9.     def __init__(self):
  10.         print "A.__init__ called"  # -> is actually never called
  11. a = A()
  12. # A.__new__ called

关于 super,这里是一个非常不错的解释,简单来说super做了下面的事情:

  1. def super(cls, inst):
  2.     mro = inst.__class__.mro()
  3.     return mro[mro.index(cls) + 1]

关于 MRO,这篇文章非常棒:你真的理解Python中MRO算法吗?,简单来说,在新式类MRO的 C3 算法中,保证:基类永远出现在派生类后面,如果有多个基类,基类的相对顺序保持不变。

操作符

利用特殊方法可以构建一个拥有Python内置类型行为的对象,这意味着可以避免使用非标准的、丑陋的方式来表达简单的操作。在一些语言中,这样做很常见:

  1. if instance.equals(other_instance):
  2.     # do something

Python中当然也可以这么做,但是这样做让代码变得冗长而混乱。不同的类库可能对同一种比较操作采用不同的方法名称,这让使用者需要做很多没有必要的工作。因此我们可以定义方法__eq__,然后就可以像下面这样使用:

  1. if instance == other_instance:
  2.     # do something

Python 有许多特殊的函数对应到常用的操作符上,比如:

  • __cmp__(self, other):定义了所有比较操作符的行为。应该在 self < other 时返回一个负整数,在 self == other 时返回0,在 self > other 时返回正整数。
  • __eq__(self, other):定义等于操作符(==)的行为。
  • __ne__(self, other):定义不等于操作符(!=)的行为(定义了 eq 的情况下也必须再定义 ne!)
  • __le__(self, other):定义小于等于操作符(<)的行为。
  • __ge__(self, other):定义大于等于操作符(>)的行为。

数值操作符

就像可以使用比较操作符来比较类的实例,也可以定义数值操作符的行为。可以分成五类:一元操作符,常见算数操作符,反射算数操作符,增强赋值操作符,和类型转换操作符,下面为一些例子:

  • __pos__(self) 实现取正操作,例如 +some_object
  • __invert__(self) 实现取反操作符 ~
  • __add__(self, other) 实现加法操作
  • __sub__(self, other) 实现减法操作
  • __radd__(self, other) 实现反射加法操作
  • __rsub__(self, other) 实现反射减法操作
  • __floordiv__(self, other) 实现使用 // 操作符的整数除法
  • __iadd__(self, other) 实现加法赋值操作。
  • __isub__(self, other) 实现减法赋值操作。
  • __int__(self) 实现到int的类型转换。
  • __long__(self) 实现到long的类型转换。

反射运算符方法和它们的常见版本做的工作相同,只不过是处理交换两个操作数之后的情况。类型转换操作符,主要用于实现类似 float() 这样的内建类型转换函数的操作。

类的表示

使用字符串来表示类是一个相当有用的特性。在Python中有一些内建方法可以返回类的表示,相对应的,也有一系列特殊方法可以用来自定义在使用这些内建函数时类的行为。

  • __str__(self) 定义对类的实例调用 str() 时的行为。
  • __repr__(self) 定义对类的实例调用 repr() 时的行为。 str() 和 repr() 最主要的差别在于“目标用户”,repr() 的作用是产生机器可读的输出(大部分情况下,其输出可以作为有效的Python代码),而 str() 则产生人类可读的输出。
  • __dir__(self) 定义对类的实例调用 dir() 时的行为,这个方法应该向调用者返回一个属性列表。如果重定义了__getattr__ 或者使用动态生成的属性,以实现类的交互式使用,那么这个方法是必不可少的。

属性控制

在Python中,重载__getattr__、__setattr__、__delattr__和__getattribute__方法可以用来管理一个自定义类中的属性访问。其中:

  • getattr方法将拦截所有未定义的属性获取(当要访问的属性已经定义时,该方法不会被调用,至于定义不定义,是由Python能否查找到该属性来决定的);
  • getattribute方法将拦截所有属性的获取(不管该属性是否已经定义,只要获取它的值,该方法都会调用),由于此情况,所以,当一个类中同时重载了getattr和getattribute方法,那么getattr永远不会被调用,另外getattribute方法仅仅存在于Python2.6的新式类和Python3的所有类中;
  • setattr方法将拦截所有的属性赋值;
  • delattr方法将拦截所有的属性删除。

在Python中,一个类或类实例中的属性是动态的(因为Python是动态的),也就是说,可以往一个类或类实例中添加或删除一个属性。

由于getattribute、setattr、delattr方法对所有的属性进行拦截,所以,在重载它们时,不能再像往常的编码,要注意避免递归调用(如果出现递归,则会引起死循环);然而对getattr方法,则没有这么多的限制。

在重载setattr方法时,不能使用“self.name = value”格式,否则,它将会导致递归调用而陷入死循环。正确的应该是:

  1. def  __setattr__(self, name, value):
  2.     # do-something
  3.     object.__setattr__(self, name, value)
  4.     # do-something

其中的object.__setattr__(self, name, value)一句可以换成self.__dict__[name] = value;但前提是,必须保证getattribute方法重载正确(如果重载了getattribute方法的话),否则,将在赋值时导致错误,因为self.dict将要触发对self所有属性中的dict属性的获取,这样从而就会引发getattribute方法的调用,如果getattribute方法重载错误,setattr方法自然而然也就会失败。

自定义序列

有许多办法可以让 Python 类表现得像是内建序列类型(字典,元组,列表,字符串等)。

在Python中实现自定义容器类型需要用到一些协议。首先,不可变容器类型有如下协议:想实现一个不可变容器,你需要定义__len__ 和 __getitem__。

可变容器的协议除了上面提到的两个方法之外,还需要定义 __setitem__ 和 __delitem__ 。如果你想让你的对象可以迭代,你需要定义 __iter__ ,这个方法返回一个迭代器。迭代器必须遵守迭代器协议,需要定义 __iter__ (返回它自己)和 next 方法。

上下文管理

上下文管理协议(Context Management Protocol)包含方法 __enter__() 和 __exit__(),支持 该协议的对象要实现这两个方法。

  • enter: 进入上下文管理器的运行时上下文。如果指定了 as 子句的话,返回值赋值给 as 子句中的 target。
  • exit: 退出与上下文管理器相关的运行时上下文。返回一个布尔值表示是否对发生的异常进行处理。

在执行with语句包裹起来的代码块之前会调用上下文管理器的 enter 方法,执行完语句体之后会执行 exit 方法。

with 语句的语法格式如下:

  1. with context_expression [as target(s)]:
  2.     with-body

Python 对一些内建对象进行改进,加入了对上下文管理器的支持,可以用于 with 语句中,比如可以自动关闭文件、线程锁的自动获取和释放等。如下面例子:

  1. >>> with open("etc/CS.json"as d:
  2. ...:     print d
  3. <open file 'etc/CS.json', mode 'r' at 0x109344540>
  4. >>> print d
  5. <closed file 'etc/CS.json', mode 'r' at 0x109344540>
  6. >>> print dir(d)
  7. ['__class__''__delattr__''__doc__''__enter__''__exit__', ...]

通过使用 with 语句,不管在处理文件过程中是否发生异常,都能保证 with 语句执行完毕后已经关闭了打开的文件句柄。

小额消费信贷用户数据
2016年度中国软件开发者白皮书下载(PDF)
深入浅出数据分析(中文版)
MySQL必知必会

发表评论

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen: