python面向对象编程(OOP)

Published On October 15, 2017

category python | tags oop


本教程并不是泛泛地讲面向对象编程的概念,而是深入python中使用面向对象编程的技术,所以需要读者预先对面向对象有了解。 不需要使用类,python就可以做很多事情了。然而对于类库开发者,掌握面向对象编程的技术很有必要。 我们什么时候会使用面向对象编程?

  1. 当需要将某些数据和操作包装起来的时候
  2. 为了代码重用,避免冗余,易于维护

python是一门面向对象的编程语言,不仅如此,python里一切都是对象。整数、字符串、列表、函数、类都是对象,甚至模块(module)、类型(types)也是对象,像普通对象一样,它们有类型、方法和属性,可以作为函数的参数或表达式的操作数。

与c++相比,python作为动态类型的脚本语言,面向对象的使用要简单得多。与java这种纯面向对象的语言相比,python并不是只有面向对象这一种编程范式,在一个python项目中使用很少的面向对象机制也是完全可行的。例如python的模块就是天然的对象,提供了一种封装的功能,很多python项目就是由一个一个模块组成,模块内部直接定义函数和变量。

面向对象的三大特征分别是:封装、继承和多态,本文首先从这三个特征入手介绍python类编写的基础,然后深入类的组成,介绍属性和方法的高级用法,以及它们的访问控制,最后分别介绍了继承的高级特性和元类的用法。

以下示例代码在python2.7上使用doctest测试通过。

类编写基础

类把数据和函数包装在一起并且支持扩展。

本文只讨论新式类(new-style class),即:

  • python2中继承自Object的类
  • python3中的所有类

创建一个Person类,它有姓名和工资两个属性。

#!/usr/bin/env
# coding=utf-8

"""
>>> sue = Person('sue', 10000)
>>> print(sue)
[Person: sue, 10000]
>>> sue.giveRaise(.1)
>>> print(sue)
[Person: sue, 11000]
>>> Person.giveRaise(sue, .1)
>>> print(sue)
[Person: sue, 12100]
>>>
>>> tom = Manager('tom', 20000)
>>> tom.giveRaise(.1)
>>> print(tom)
[Person: tom, 24000]
"""
class Person(object):
    # 类属性在class语句中通过赋值语句添加
    base = 1000

    # 构造函数
    def __init__(self, name, pay=0):
        # 实例属性在方法中通过对self进行赋值添加
        self.name = name
        if pay < self.base:
            pay = self.base
        self.pay = pay

    # 方法,第一个参数自动填充为调用该函数的对象
    def giveRaise(self, percent):
        self.pay = int(self.pay*(1 + percent))

    # 运算符重载方法
    def __str__(self):
        return '[Person: %s, %s]' % (self.name, self.pay)


# Manager继承Person
# Manager是子类,Person是超类
class Manager(Person):
    def giveRaise(self, percent, bonus=0.1):
        # self.giveRaise会被解析为Manager.giveRaise,造成死循环
        Person.giveRaise(self, percent + bonus)

# 一个python文件是一个模块,模块也是对象
# 当作为脚本运行的时候模块的__name__属性的值是__main__
# 而当它作为一个模块被导入的时候__name__是模块名,也就是文件名(不包含后缀)
if __name__ == '__main__':
    sue = Person('sue', 10000)
    print(sue)
    sue.giveRaise(.1)
    print(sue)
    # 通过类调用实例方法
    Person.giveRaise(sue, .1)
    print(sue)

    tom = Manager('tom', 20000)
    tom.giveRaise(.1)
    print(tom)
将以上代码保存为Person.py,然后执行python Person.py或通过python -m doctest -v Person.py运行doctest测试。

__init__是构造函数,它是每次创建一个实例的时候python会自动运行的代码,通常用来初始化实例属性,除此之外,构造函数和其他函数没有任何区别。self是新创建的对象,由python自动填充。self.name=name,将本地的name变量赋值给了self对象的name属性

类和实例的关系

类和实例都是对象,class语句创建类对象,调用类对象返回实例对象,类充当了创建多个实例的工厂

对于具有C++或java编程背景的程序员,可能很难理解这一点,但是一旦接受了它,理解python的OOP机制就容易多了。

封装

与模块类似,每个类定义了一个命令空间,每个对象也有自己独立的命令空间。这些命令空间使用一个dict来保存一组属性(包括方法),这就是封装。

方法(位于类中的函数)

将操作对象属性的代码写成方法,每种操作只编码一次,却可以在程序的不同地方,不同对象上调用,修改方法内部的代码不会影响使用它的代码。

方法的第一个参数总是self,python会自动填充self为当前调用的对象。 方法可以通过实例调用,也可以通过类调用,后者需要手动传递实例。比如sue.giveRaise(.2)等价于Person.giveRaise(sue, .2)

运算符重载方法

这种方法在开头和结尾各有两个下划线,通常由python自动调用。

使用运算符重载就是钩子一样允许一个对象拦截并处理某些内置操作,比如创建对象的时候自动运行__init__(注意:__init__也是运算符重载),销毁一个对象的时候自动运行__del__,使用print打印一个对象的时候会先调用对象的__str__方法,然后打印__str__返回的内容。另一方面,这种特殊的方法让实例的用法看起来就像内置类型,从而与内置类型的代码兼容,比如要支持+操作就需要重载__add__方法。

如果上面的代码没有重新定义__str__,直接print一个对象,默认会显示对象的类名以及它在内存中的地址

python2.7

<__main__.Person instance at 0x1005acd88>
python3.5
<__main__.Person object at 0x7f11eb915550>
__str__很接近的另一个运算符重载方法是__repr__,后面将介绍它们的区别。

继承

子类继承父类,实例继承类。python的OOP模型很简单,其实就是在继承树中搜索属性。

命名空间搜索

前面说了,每个类或对象都有自己的命名空间,与模块不同,它们的命名空间可以通过继承形成层次结构,即命名空间树或者叫类树,叶子是实例。实例从它的类继承属性,而类从它的父类继承属性。当访问实例的属性的时候,比如obj.attr,会自下而上、由左至右在命令空间树中搜索该属性首次出现的位置。通过这种机制实现继承或覆盖(定制)超类中已有的代码,或扩展超类的行为。

继承方法和类属性

任何方法都会继承,包括构造函数__init__。

类属性存在于类对象中,通常是多个类实例可共享的数据。

上面还定义一个Person类的子类Manager,它几乎像Person类一样,只不过有特别的工资待遇。 子类Manager中的giveRaise 定制 了超类Person类的同名方法,除此之外继承了__init____str__方法。可以使用相同的技术来定制构造函数,这是一种很常见的编码模式。

多态

是指调用某个方法时会根据实际对象的类型决定调用哪个版本的方法。

多态是python灵活性的核心。

下面的代码会调用Person类和Manager不同的giveRaise版本

sue = Person('sue', 10000)
tom = Manager('tom', 20000)
for p in [sue, tom]:
    p.giveRaise(.1)
    print(p)

python与C++的多态

python的多态与c++等强类型语言的多态很不一样,c++的多态是指允许将子类型的指针赋值给父类型的指针,通过父类型的指针调用某个虚函数时,直到运行时才会决定执行父类型的版本还是子类行的版本(如果子类行覆盖了该函数),也就是由该指针指向的对象决定执行的版本。而python的多态则自由的多,是通过一种叫做鸭子类型的方式实现。

duck typing(鸭子类型)是python等动态类型语言的一种风格,源自James Whitecomb Riley提出的鸭子测试:

"If it looks like a duck and quacks like a duck, it's a duck"

也就是说我们并不关心对象的类型到底是不是鸭子,只关心行为是不是鸭子。

在静态类型语言里,我们可以编写一个函数,接受一个鸭子类型的对象,调用它的quack方法,而在Python这种使用鸭子类型的语言里,这样的函数可以接收任意类型的对象,只要这个对象具有quack方法就可以正常运行。 这么做的好处就是这个函数不依赖于某种类型,也不需要在函数中检查参数的具体类型。 这也是多态的一种形式,但这种多态的实现完全由程序员自己来约束,并没有语言上的约束。

示例:

#!/usr/bin/env

"""
>>> in_the_forest(Duck())
Quaaaaaack!
>>> in_the_forest(Person())
The person imitates a duck.
"""

class Duck:
    def quack(self):
        print("Quaaaaaack!")

class Person:
    def quack(self):
        print("The person imitates a duck.")

def in_the_forest(mallard):
    mallard.quack()

类的组成

属性

类以及实例都可以动态增加属性。

根据属性名操作属性

python包含以下三个内置函数:

  • setattr(object, name, value)
  • getattr(object, name[, default])
  • hasattr(object, name)

示例:

class Spam(object):
    """[summary]

    >>> X = Spam()
    >>> hasattr(X, 'greet')
    True
    >>> hasattr(X, 'foo')
    False
    >>> getattr(X, 'foo')
    Traceback (most recent call last):
    AttributeError: 'Spam' object has no attribute 'foo'
    >>> setattr(X, 'foo', 123)
    >>> hasattr(X, 'foo')
    True
    >>> getattr(X, 'foo')
    123
    """
    def greet(self):
        print('hello world')

属性管理的四种工具

属性管理就是在属性被访问的时候被拦截下来进行某些操作,比如设置属性值的时候进行合法校验,获取属性值的时候返回一个经过经过实时计算出来的值。被管理的属性的值通常保存在类中以下划线开头的属性中,告诉开发者不应该直接访问这些属性。

实现这种目的有四种工具,特性(@property)和描述符(descriptor)针对特定的属性,而__getattr____getattribute__不对应具体的某个属性,下面一一进行介绍。

1.特性(@property)

将属性的get/get/delete指向我们提供的函数,还可以为属性提供文档。做法是通过property内置函数创建特性并分配给类对象:

attribute = property(fget, fset, fdel, doc)
fget是必填的,fest和fdel默认为None表示不支持相应的操作,不提供doc的话,默认为fget的文档字符串。 下面的例子使用特性的方式来控制对name属性的访问,实际存储数据的是实例属性_name:
#!/usr/bin/env 

class Person(object):
    """
    >>> bob = Person('Bob Smith')
    >>> print(bob.name)
    fetch...
    Bob Smith
    >>> bob.name = 'Robert Smith'
    change...
    >>> print(bob.name)
    fetch...
    Robert Smith
    >>> del bob.name
    remove...
    """
    def __init__(self, name):
        self._name = name
    def getName(self):
        print("fetch...")
        return self._name
    def setName(self, value):
        print("change...")
        self._name = value
    def delName(self):
        print("remove...")
        del self._name
    name = property(getName, setName, delName, "name property docs")

使用@property装饰器

class Person:
    @property
    def name(self): pass
相当于
class Person:
    def name(self): pass
    name = property(name)

同时property对象有getter、setter和deleter方法,指定相应的特性访问器,并返回特性自身。 所以上面的例子可以使用装饰器重写:

#!/usr/bin/env

class Person(object):
    """
    >>> bob = Person('Bob Smith')
    >>> print(bob.name)
    fetch...
    Bob Smith
    >>> bob.name = 'Robert Smith'
    change...
    >>> print(bob.name)
    fetch...
    Robert Smith
    >>> del bob.name
    remove...
    """
    def __init__(self, name):
        self._name = name

    @property
    def name(self):             # name = property(name)
        "name property docs"
        print('fetch...')
        return self._name
    @name.setter
    def name(self, value):      # name = name.setter(name)
        print('change...')
        self._name = value
    @name.deleter
    def name(self):             # name = name.deleter(name)
        print('remove...')
        del self._name

2.描述符

描述符是一个具有特殊访问器方法的类:

class Descriptor:
    "docstring goes here"
    def __get__(self, instance, owner): ... # Return attr value
    def __set__(self, instance, value): ... # Return nothing (None)
    def __delete__(self, instance): ... # Return nothing (None)

通过instance.attr方式访问,instance是要访问的属性所属的实例,通过class.attr方式访问,instance为None。 __get__接受一个额外的owner参数,表示描述符要附加到的类。 如果没有定义某个方法表示不支持相应的操作。

使用描述符管理属性就是把这个描述符的实例作为类属性,当属性被访问的时候,会自动调用描述符中的方法。

如下实例定义了一个描述符,用来管理Person类中的name属性,实现的功能和前面特性里的例子完全一样。

#!/usr/bin/env

class Name(object):
    "name descriptor docs"
    def __get__(self, instance, owner):
        print('fetch...')
        return instance._name
    def __set__(self, instance, value):
        print('change...')
        instance._name = value
    def __delete__(self, instance):
        print('remove...')
        del instance._name

class Person(object):
    """
    >>> bob = Person('Bob Smith')
    >>> print(bob.name)
    fetch...
    Bob Smith
    >>> bob.name = 'Robert Smith'
    change...
    >>> print(bob.name)
    fetch...
    Robert Smith
    >>> del bob.name
    remove...
    """
    def __init__(self, name):
        self._name = name
    name = Name()
Name类可以作为Person类的子类,从而不会污染模块的全局命名空间。

当描述符的__get__方法运行的时候,它接受了3个对象作为参数:

  • self是Name类实例
  • instance是Person类实例
  • owner是Person类实例

只读描述符,如果忽略__set__方法,并不会使该属性成为只读方法,因为直接对该属性赋值会在实例中存储该属性并且隐藏描述符。正确的做法是定义__set__方法,并抛出一个异常。

#!/usr/bin/env

class D(object):
    def __get__(*args): print('get')
    def __set__(*args): raise AttributeError('cannot set')

class C(object):
    """
    >>> X = C()
    >>> X.a
    get
    >>> X.a = 100
    Traceback (most recent call last):
        ...
    AttributeError: cannot set
    """
    a = D()

与特性相比

与特性一样,管理一个单个、特定的属性,都是作为类属性。 特性只是一种特殊的描述符,作为创建描述符的快捷方式。描述符更加灵活、通用,有自己的状态,可以将与属性相关的数据保存到描述符中。

3.__getattr____getattribute__
  • __getattr__(self, name):访问未定义(包括继承)的属性时触发该方法
  • __getattribute__(self, name):访问任何属性都会触发该方法

类似的还有__setattr__(self, name, value)和__delattr__(self, name),分别拦截所有属性的赋值和删除操作。

它们都是操作符重载协议的一部分,是类的特殊命名方法。适合用来实现委托设计模式。

还是上面的Person类的示例,与特性和描述符一样,只不过没有为属性指定文档:

#!/usr/bin/env

class Person(object):
    """
    >>> bob = Person('Bob Smith')
    >>> print(bob.name)
    fetch...
    Bob Smith
    >>> bob.name = 'Robert Smith'
    change...
    >>> print(bob.name)
    fetch...
    Robert Smith
    >>> del bob.name
    remove...
    """
    def __init__(self, name):
        self._name = name

    def __getattr__(self, attr):
        if attr == 'name':
            print('fetch...')
            return self._name
        else:
            raise AttributeError(attr)
    def __setattr__(self, attr, value):
        if attr == 'name':
            print('change...')
            attr = '_name'
        self.__dict__[attr] = value
    def __delattr__(self, attr):
        if attr == 'name':
            print('remove...')
            attr = '_name'
        del self.__dict__[attr]

__getattr____getattribute__对比
#!/usr/bin/env

class GetAttr(object):
    """
    >>> X = GetAttr()
    >>> print(X.attr1)
    1
    >>> print(X.attr2)
    2
    >>> print(X.attr3)
    get: attr3
    3
    >>> print(X.attr4)
    get: attr4
    3
    """
    attr1 = 1
    def __init__(self):
        self.attr2 = 2
    def __getattr__(self, attr):      # on undefined attrs only
        print('get: ' + attr)
        return 3

class GetAttribute(object):
    """
    >>> X = GetAttribute()
    >>> print(X.attr1)
    get: attr1
    1
    >>> print(X.attr2)
    get: attr2
    2
    >>> print(X.attr3)
    get: attr3
    3
    >>> print(X.attr4)
    Traceback (most recent call last):
        ...
    AttributeError: 'GetAttribute' object has no attribute 'attr4'
    """
    attr1 = 1
    def __init__(self):
        self.attr2 = 2
    def __getattribute__(self, attr): # on all attr fetches
        print('get: ' + attr)
        if attr == 'attr3':
            return 3
        else:
            return object.__getattribute__(self, attr) # use superclass to avoid looping here
属性拦截方法中的循环

在类内置的方法中操作属性时也会触发以上方法,包括这些方法本身以及__init__方法,所以就可能出现自身递归调用的死循环。比如下面这段代码,看起来一切正常,然而却有大问题。

class BadGetAttribute(object):
    def __init__(self):
        self.a = 'I am a'
    def __getattribute__(self, name):
        print('get: ' + name)
        return self.other

解决办法是在__getattribute__内使用一个超类来获取(通常用object)

class GoodGetAttribute(object):
    """
    >>> X = GoodGetAttribute()
    >>> X.a
    get: a
    'I am a'
    """
    def __init__(self):
        self.a = 'I am a'
    def __getattribute__(self, name):
        print('get: ' + name)
        return object.__getattribute__(self, name)

对于__setattr____delattr__也可以用这种方法,但更常用的方式是使用实例的__dict__命名空间(这种方式对setattribute无效)。

class GoodSetAttribute(object):
    """
    >>> X = GoodSetAttribute()
    set: a = I am a
    >>> X.a = 'A'
    set: a = A
    >>> X.a
    'A'
    """
    def __init__(self):
        self.a = 'I am a'
    def __setattr__(self, name, value):
        print('set: %s = %s' % (name, value))
        self.__dict__[name] = value

__getattr____getattribute__适合实现委托,特性和描述符更适合管理特定属性。

方法

方法就是定义在类中的函数,作为类的属性。

运算符重载方法

这类特殊的方法前后各有两个下划线,它的作用是拦截内置的操作,当类的对象出现在内置操作中时,python会自动调用调用它们。其实这些方法就像钩子,可与内置操作绑定。这些方法也可以从超类继承。 最常用的运算符重载方法就是构造函数__init__,与之相对的是析构函数__del__,本节介绍一些其他常用的运算符重载方法的例子。

字符串表示

有两个运算符重载方法用于返回对象的字符串表示,用以替代默认的显示格式。但它们的作用不同:

  • __str__:使用内置函数str时触发该方法,如果没有__str__方法,则使用__repr__。比如print一个对象的时候,打印操作会调用str内置函数,str会调用对象的__str__方法
  • __repr__:使用内置函数repr时触发该方法,除print以外的其他所有场景,比如在交互提示模式下回显结果。它通常是一个编码字符串,可以用来重新创建对象;如果想让所有环境都有统一的显示,应该定义该方法。
class Spam(object):
    """[summary]

    >>> X = Spam(13)
    >>> X
    __repr__: 13
    >>> print(X)
    __str__: 13
    >>> [X, X]
    [__repr__: 13, __repr__: 13]
    """
    def __init__(self, data):
        self.data = data

    def __str__(self):
        return '__str__: %s' % self.data

    def __repr__(self):
        return '__repr__: %s' % self.data

这两个函数都必须返回字符串。

用户定义的迭代器

有两种技术来构造一个可迭代的对象,__getitem____iter__

1.sequence protocol: __getitem__

可以用来拦截索引和分片运算,for语句是对一个序列重复进行索引,如果定义了__getitem__方法就支持for循环,同时支持了所有迭代环境:

  • 列表解析
  • 成员关系测试
  • 内置函数map、filter等
  • 列表/元组赋值
  • 类型构造
class Spam(object):
    """[summary]

    >>> X = Spam()
    >>> X.data = 'Spam'
    >>> for i in X:
    ...     print(i)
    ...
    S
    p
    a
    m

    >>> [c for c in X]
    ['S', 'p', 'a', 'm']

    >>> 'p' in X
    True

    >>> list(map(str.upper, X))
    ['S', 'P', 'A', 'M']

    >>> a,b,c,d=X
    >>> a,c,d
    ('S', 'a', 'm')

    >>> tuple(X)
    ('S', 'p', 'a', 'm')
    """
    def __getitem__(self, i):
        return self.data[i]
2.iteration protocol: __iter__

在迭代环境,python会优先遵守迭代协议,然后才是重复对对象进行索引运算 迭代协议规定,对一个对象调用iter内置函数返回迭代对象,对迭代对象重复调用next方法,进行迭代,直到发生StopIteration异常

class Spam(object):
    """[summary]

    >>> for i in Spam(1, 5):
    ...     print(i)
    ...
    1
    4
    9
    16

    >>> # iterate manully
    >>> X = Spam(1, 5)
    >>> I = iter(X)
    >>> next(I)
    1
    >>> next(I)
    4
    >>> next(I)
    9
    >>> next(I)
    16
    >>> next(I)
    Traceback (most recent call last):
    StopIteration
    """
    def __init__(self, start, stop):
        self.value = start
        self.stop = stop

    def __iter__(self):
        return self

    # use `def __next__(self)` in python3
    def next(self):
        if self.value == self.stop:
            raise StopIteration
        rv = self.value ** 2
        self.value += 1
        return rv
可调用的对象

__call__方法可以使对象的用法类似于函数,同时还能保持状态信息以供使用:

class Spam(object):
    """[summary]

    >>> X = Spam()
    >>> X('a', 'b', c='c')
    ('Called: ', ('a', 'b'), {'c': 'c'})
    """
    def __call__(self, *args, **kargs):
        print('Called: ', args, kargs)

特殊方法

除了普通方法,类中还有两类不需要实例调用的特殊方法:静态方法和类方法(第一个参数是类实例),它们旨在处理类的数据而不是实例数据。使用内置函数staticmethod和classmethod将类内定义的函数声明为特殊方法,这两个特殊的内置函数常常写成装饰器。

三类方法的对比:

#!/usr/bin/env

class Spam(object):
    """
    >>> obj = Spam()
    >>> obj.imeth(1)
    1
    >>> Spam.imeth(obj, 2)
    2
    >>>
    >>> Spam.smeth(3)
    3
    >>> obj.smeth(4)
    4
    >>>
    >>> Spam.cmeth(5)
    (<class 'methods.Spam'>, 5)
    >>> obj.cmeth(6)
    (<class 'methods.Spam'>, 6)
    """
    def imeth(self, x):
        print(x)

    @staticmethod
    def smeth(x):
        print(x)

    @classmethod
    def cmeth(cls, x):
        print(cls, x)

它们都允许通过类和实例调用,通过类调用普通方法需要显示的传入实例作为第一个参数,通过实例调用类方法会自动将类作为第一个参数。

静态方法和类方法在使用上是有区别的,因为类方法多一个类实例参数,该参数总是继承层次中最底的类,而静态方法获取类的数据只能通过硬编码类名称。

下面是用静态方法统计Spam类及其所有子类的实例的计数器

#!/usr/bin/env
# coding=utf-8

"""
>>> from static_meth import *
>>> x = Spam()
>>> y1, y2 = Sub(), Sub()
>>> z1, z2, z3 = Other(), Other(), Other()
>>> x.numInstances, y1.numInstances, z1.numInstances
(6, 6, 6)
>>> Spam.numInstances, Sub.numInstances, Other.numInstances
(6, 6, 6)
"""

class Spam:
    numInstances = 0
    def __init__(self):
        Spam.numInstances = Spam.numInstances + 1

    @staticmethod
    def printNumInstances():
        print("Number of instances: ", Spam.numInstances)

class Sub(Spam):
    @staticmethod
    def printNumInstances():
        print('Extra stuff')
        Spam.printNumInstances()

class Other(Spam): pass

下面是用类方法实现每个类的实例计数器

#!/usr/bin/env
# coding=utf-8

"""
>>> x = Spam()
>>> y1, y2 = Sub(), Sub()
>>> z1, z2, z3 = Other(), Other(), Other()
>>> x.numInstances, y1.numInstances, z1.numInstances
(1, 2, 3)
>>> Spam.numInstances, Sub.numInstances, Other.numInstances
(1, 2, 3)
"""

class Spam:
    numInstances = 0
    @classmethod
    def count(cls):
        cls.numInstances += 1
    def __init__(self):
        self.count()

class Sub(Spam):
    numInstances = 0
    def __init__(self):
        Spam.__init__(self)

class Other(Spam):
    numInstances = 0

访问控制

事实上,python无法像java或c++那样严格控制客户代码对类属性的访问,比如private、public等。python中的惯例是通常使用单下划线开头的名称表示私有属性。

惯例

python程序员通常使用单个下划线来表示类内部的名称,类的使用者不应该访问这些属性,但是这仅仅是一种非正式惯例,对于python解释器而言,这样的名称没有任何特别之处,用户仍然可以访问。

class A(object):
    """
    >>> a = A('foo')
    >>> # you can still access them directly but don't do this
    >>> a._spam()
    I am foo
    >>> print(a._name)
    foo
    """

    def __init__(self, name):
        self._name = name

    def _spam(self):
        print('I am %s' % self._name)

伪私有属性

另一种常见的方式是开头有两个下划线,结尾没有两个下划线,它的作用是扩张变量名,比如Spam类的__x属性会被扩张成_Spam__x,它的目的是把类创建的属性局部化,避免实例内的命名空间的冲突,而不是限制属性的访问,因此叫做伪私有更恰当。

还是通过例子来解释:

class C1(object):
    def meth1(self): self.__X = 88      # X is mine, becomes _C1__X
    def meth2(self): print(self.__X)

class C2(object):
    def metha(self): self.__X = 99      # X is mine, becomes _C2__X
    def methb(self): print(self.__X)

class C(C1, C2):
    """[summary]

    >>> c = C()
    >>> c.meth1()
    >>> c.metha()
    >>> c.meth2()
    88
    >>> c.methb()
    99
    >>> c.__dict__
    {'_C2__X': 99, '_C1__X': 88}

    Extends:
        C1
        C2

    Variables:
        pass {[type]} -- [description]
    """
    pass
通过查看类C的实例的命名空间字典可以看到,使用双下划线开头的属性被扩张了,这个技巧就可以避免实例中潜在的变量名冲突。

继承的高级特性

super

在子类方法中调用父类方法除了直接显式使用父类调用外,还可以使用内置函数super(type[, object-or-type])

# only works for new-style classes
class A(object):
    def spam(self):
        print('A.spam')

class B(A):
    """[summary]

    >>> b = B()
    >>> b.spam()
    B.spam
    A.spam

    Extends:
        A
    """

    def spam(self):
        print('B.spam')
        super(B, self).spam()  # Call parent spam(), can be replaced by super() in python3
super的作用是在type类的mro列表(即基类列表)里搜索包含.后面的属性的类,并返回第一个类的一个代理对象。事实上,super的使用并非像看上去那么简单,社区对于它的使用一直存在争议。什么时候会使用super呢?

  • 在单继承中使用super引用父类,而不需要显示的调用父类,这一点与java类似
  • 在多继承中(cooperative multiple inheritance),多个基类实现了同一个方法(通常具有相同的方法签名),在该方法内都使用super()调用下一个类的该方法,那么整个mro列表上的类的该方法都会被调用一次

为了说明第二种使用情况,下面是一个比较复杂的例子:

# http://python3-cookbook.readthedocs.io/zh_CN/latest/c08/p07_calling_method_on_parent_class.html

class Base(object):
    def __init__(self):
        print('Base.__init__')

class A(Base):
    def __init__(self):
        super(A, self).__init__()
        print('A.__init__')

class B(Base):
    def __init__(self):
        super(B, self).__init__()
        print('B.__init__')

class C(A, B):
    """[summary]
    >>> C.__mro__               
    (<class 'super_example_complex.C'>, <class 'super_example_complex.A'>, <class 'super_example_complex.B'>, <class 'super_example_complex.Base'>, <type 'object'>)
    >>> c = C()
    Base.__init__
    B.__init__
    A.__init__
    C.__init__

    Extends:
        A
        B
    """
    def __init__(self):
        super(C, self).__init__()  # Only one call to super() here
        print('C.__init__')

C的mro列表是[C, A, B, Base],执行C()的调用顺序如下:

  1. C.__init__中调用super().__init__相当于调用A.__init__
  2. A.__init__中调用super().__init__相当于调用了B.__init__,注意,B和A并不存在继承关系。
  3. B.__init__中调用super().__init__相当于调用了Base.__init__,最终Base.__init__只会被调用一次

如果不使用super,而是直接使用A.__init__B.__init__调用的话,显然Base.__init__会被调用两次。

抽象类

在其他面向对象的编程语言中都有抽象类的概念,它是一种特殊的基类,包含抽象方法,这种方法只提供了子类必须实现的方法的签名,而没有提供实现。通常,使用抽象类来定义期待子类实现的方法。

手动实现

class Super(object):
    """[summary]

    >>> X = Super()
    >>> X.delegate()
    Traceback (most recent call last):
    NotImplementedError: action must be defined!

    """
    def delegate(self):
        self.action()

    def action(self):
        raise NotImplementedError('action must be defined!')

class Sub1(Super):
    """[summary]

    >>> s1 = Sub1()
    >>> s1.delegate()
    Traceback (most recent call last):
    NotImplementedError: action must be defined!

    """
    pass

class Sub2(Super):
    """[summary]

    >>> s2 = Sub2()
    >>> s2.delegate()
    spam

    Extends:
        Super
    """
    def action(self):
        print('spam')

如果子类没有提供action方法的具体实现,父类的action将被调用,然后引发NotImplementedError异常。

使用abstractmethod装饰器

上面示例中的抽象方法action可以由特殊的语法来实现:

from abc import ABCMeta, abstractmethod
class Super:
    """[summary]

    >>> X = Super()
    Traceback (most recent call last):
    TypeError: Can't instantiate abstract class Super with abstract methods action

    """
    __metaclass__ = ABCMeta

    def delegate(self):
        self.action()

    @abstractmethod
    def action(self):
        pass


class Sub1(Super):
    """[summary]

    >>> X = Sub1()
    Traceback (most recent call last):
    TypeError: Can't instantiate abstract class Sub1 with abstract methods action

    Extends:
        Super
    """

class Sub2(Super):
    """[summary]

    >>> X = Sub2()
    >>> X.delegate()
    spam

    Extends:
        Super
    """
    def action(self):
        print('spam')
这种方式的优点是,在示例化类的实例时,如果没有提供抽象方法的实现会产生错误。

多继承

MRO 查找顺序

下面的代码展示了两个父类中有同名的方法时的情况:

#!/usr/bin/env 

"""
>>> c1 = C1()
>>> c1.method()
method of a
>>> c2 = C2()
>>> c2.method()
method of b
"""
class A:
    def method(self):
        print('method of a')

class B:
    def method(self):
        print('method of b')

class C1(A,B):
    pass

class C2(B,A):
    pass
看起来像是在父类列表中从左到右查询某个方法,但子类继承的方法在父类列表中查找的顺序(MRO)并不总是按深度优先从左到右的顺序,而是使用了C3算法。具体就不展开,这篇博客讲得很好。

总之,不同的父类中有相同的方法时就有可能产生冲突,有如下继承关系:

C1继承(A,B)会引发Cannot create a consistent method resolution order (MRO) for bases A, B的异常。 C2继承(B,A)却是可以的。

Mixin

Mixin类是具有以下特征的类:

  1. 只包含方法,不包含数据
  2. 不能单独实例化
  3. 只继承object或其它的Mixin类,不允许定义同名方法(避免mro查找问题)

将Mixin类混入某个类就可以为该类添加特定的功能。但Mixin类和被混入的类之间并不是一种is-a的关系,继承强调I am,而Mixin强调I can。 混入Mixin类是通过多继承实现组合模式,类似java中的接口,最大的不同是java的接口只能规定一个类具有哪些方法,而并不包含实现。

Mixin技术是一种受限制的多重继承: 主线使用单一继承,其余父类都是Mixin类,这样的话,类在层次上具有单一继承一样的树结构,简单清晰,自由的混入Mixin类就可以灵活的添加需要的功能。

元类

元类允许我们通过在创建类的时候执行自定义的代码,它是定制类的一种钩子。简单的说,元类是创建类的类型。

比如希望在创建类的时候动态地添加属性和方法,就可以编写一个元类来实现。

python的类型模型

类也是对象,它的类型是type,type是一种独特的类型。即顶层的type类型创建了具体的类型,具体的类型创建了实例。如何验证呢?type内置函数可以返回对象的类型,对象的__class__属性链接到创建它的类,可以体会一下:

>>> type([])
<type 'list'>
>>> type(list)
<type 'type'>
>>> type(type)
<type 'type'>
>>>
>>> [].__class__
<type 'list'>
>>> [].__class__.__name__
'list'
>>> list.__class__
<type 'type'>
>>> type.__class
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: type object 'type' has no attribute '__class'

类对象和type类型的关系具体地说就是: 在class语句的末尾,会调用type对象

class = type(classname, superclasses, attributedict)
type对象的__call__的运算符重载方法调用了另外的两个方法
type.__new__(typeclass, classname, superclasses, attributedict)
type.__init__(class, classname, superclasses, attributedict)
__new__方法创建并返回了新的class对象,并且随后__init__方法初始化了新创建的对 象。

比如,给定一个类

class Spam(Eggs): # Inherits from Eggs
    data = 1 # Class data attribute
    def meth(self, arg): # Class method attribute
        pass
在class语句的末尾会调用type对象产生Spam类
Spam = type('Spam', (Eggs,), {'data': 1, 'meth': meth, '__module__': '__main__'})

这种类型模型是通过元类定制类的基础:普通的类都是type类的实例,使用元类后,元类是type类的子类,用户定义的类是元类的实例。

使用元类

通过以上分析,使用元类分两步:

  1. 创建元类:继承type,并定制__new____init__方法,甚至__call__方法:
  2. 在创建类的时候申明自定义的元类,而不是默认的type类型

下面的例子展示了元类何时被调用

#!/usr/bin/env
# coding=utf-8
from __future__ import print_function

class MetaOne(type):
    def __new__(meta, classname, supers, classdict):
        print('In MetaOne.new:', classname, supers, classdict, sep='\n...')
        return type.__new__(meta, classname, supers, classdict)

    def __init__(Class, classname, supers, classdict):
        print('In MetaOne init:', classname, supers, classdict, sep='\n...')
        print('...init class object:', list(Class.__dict__.keys()))

class Eggs(object):
    pass

print('making class')
class Spam(Eggs): # Inherits from Eggs, instance of Meta
    __metaclass__ = MetaOne
    data = 1 # Class data attribute
    def meth(self, arg): # Class method attribute
        pass

print('making instance')
X = Spam()
print('data:', X.data)

下面的例子展示了如何动态地向一个类添加一个方法:

#!/usr/bin/env
# coding=utf-8

def required():
    return True

def extra(self, arg):
    print(arg)

# config class based on some runtime test
class Extras(type):
    def __init__(Class, classname, superclasses, attributedict):
        if required():
            Class.extra = extra

class Client1():
    __metaclass__ = Extras

X = Client1() # X is instance of Client1
X.extra('hello')

元类与继承的关系

元类可以被继承,但是元类的属性不会被继承,比如类A的元类是M,类B继承A,那么B的元类也是M,但是M中的属性m不会出现在类A或B的搜索范围内。

参考


qq email facebook github
© 2018 - Xurui Yan. All rights reserved
Built using pelican