Python属性和方法

转载出处:http://abyssly.com/2013/08/19/python_attributes_and_methods/

关于此文

解释了Python新式对象的属性访问机制:

  • 函数是如何成为方法的?

  • descriptor和property是如何工作的?

  • 如何确定方法解析顺序(method resolution order)?

新式对象是从Python2.2版本开始引入的。在2.2及其以上的各版本之中存在一些差别,但这里涉及到的所有概念都是OK的。

开始之前

你需要注意的几点:

  • 本文讲述的是新式对象(在很久之前的Python2.2中就已引入),下面的示例对于Python2.5及其以上版本适用。

  • 本文不适合什么都不会的新手,如果要理解本文,至少需要知道一点Python,而且想知道更多。

  • 你应该对Python中的不同类别的对象较熟悉,当你期望类(class)而看到类型(type)时也不应该感到困惑。你可以先阅读这个系列的第一部分Python Types and Objects,了解一下背景知识。

第一章 新的属性访问(Attribute Access)

动态的dict

属性是什么?很简单,属性是通过一个对象得到另一个对象的方式。借助全能的点操作符——objectname.attributename——你就得到了另一个对象。你也能创造属性,通过赋值操作符:objectname.attributename = notherobject

但是访问一个属性会返回哪个对象?属性对应的对象在哪个地方?这些问题会在这章得到解答。

例1.1. 简单的属性访问

>>> class C(object):
...     classattr = "attr on class" #1
...
>>> cobj = C()
>>> cobj.instattr = "attr on instance" #2
>>>
>>> cobj.instattr #3
'attr on instance' 
>>> cobj.classattr #4
'attr on class'
>>> C.__dict__['classattr'] #5
'attr on class'
>>> cobj.__dict__['instattr'] #6
'attr on instance'
>>>
>>> cobj.__dict__ #7
{'instattr': 'attr on instance'}
>>> C.__dict__ #8
{'classattr': 'attr on class', '__module__': '__main__', '__doc__': None}
  • 1 属性可以在一个类上设置。

  • 2 或者甚至设在类的实例上。

  • 3,4 从一个实例可以同时访问类的属性和实例的属性。

  • 5,6 属性实际上包含在对象的一个类似字典的__dict__里面。

  • 7,8 __dict__只包含了用户提供的属性。

好,我承认'用户提供的属性'是我造出来的术语,但我觉得它有助于你更好的理解。注意__dict__本身是一个属性。我们并没有自己去设置这个属性,但是Python提供了它。我们的老朋友__class____bases__(尽管似乎没有哪个在__dict__里面)也类似。让我们称它们为Python提供的属性。一个属性是否是Python提供的取决于所讨论的对象(例如__bases__只是对于类来说是Python提供的)。

不过我们更感兴趣的是用户定义的属性。它们由用户提供的,而且通常(并非总是)会出现在所在对象__dict__中。

当一个属性被访问时(例如,print objectname.attributename),会依次在下面的对象上搜索它:

  1. 对象自身(objectname.__dict__或者objectname的任意一个Python提供的属性)。

  2. 对象的类型(objectname.__class__.__dict__)。注意只有__dict__被搜索,这意味着只有用户提供的类的属性。换句话说,objectname.__bases__可能不会返回任何东西,即使objectname.__class__.__bases__存在。

  3. 对象的类的基类,基类的基类,一直下去。(objectname.__class__.__bases__中的每一个类的__dict__)。多个基类不会使Python混淆,我们也暂时不用考虑。重点是所有的基类都会被搜索直到属性被找到。

如果所有的努力都没有成功找到名字相符的属性,Python会抛出异常AttributeError。访问某对象(例子中的objectname)的属性时,其类型的类型(objectname.__class__.__class__)永远不会被搜索。

内建函数dir()返回一个对象的全部属性的列表。也可以看看标准库中的inspect module,里面有更多的考察对象的函数。

上面的小节解释了对所有对象通用的机制。甚至对于类也适用(例如访问classname.attrname),只需要一点轻微的修改:类的基类先于类的类(即classname.__class__,对于大部分类型来说都是<type 'type'>)被搜索。

一些对象,比如内建类型及它们的实例(列表,元组等)没有__dict__。因此它们不能接受用户定义的属性。

还没完!这只是故事的浓缩版。当设置和获取属性时有更多的事情发生,下面的章节会介绍。

从函数到方法

继续我们的Python实验:

例1.1. 考察函数

>>> class C(object):
...     classattr = "attr on class"
...     def f(self):
...             return "function f"
...
>>> C.__dict__ #1
{'classattr': 'attr on class', '__module__': '__main__',
 '__doc__': None, 'f': <function f at 0x008F6B70>}
 >>> cobj = C()
 >>> cobj.classattr is C.__dict__['classattr'] #2
 True
 >>> cobj.f is C.__dict__['f'] #3
 False
 >>> cobj.f #4
 <bound method C.f of <__main__.C instance at 0x008F9850>>
 >>> C.__dict__['f'].__get__(cobj, C) #5
 <bound method C.f of <__main__.C instance at 0x008F9850>>
  • 1 两个看起来正常的类属性,字符串'classattr'和函数'f'

  • 2 访问'classattr'属性实际上是从类的__dict__中获得,这在意料之中。

  • 3 对于函数却不是这样的!为什么?

  • 4 嗯,它确实看起来像一个不同的对象。(绑定方法是一个可调用(callable)的对象,它在被调用时,会将一个实例(例子中的cobj)作为第一个参数添加到被提供的参数之前一起传给一个函数(例子中的C.f),这是实例中的方法运行的机制。)

  • 5 剧透一下,Python就是这样来创建绑定方法的。当查找一个实例的属性时,如果Python在类的__dict__中找到的对象拥有__get__()方法,Python不会直接返回这个对象,而是返回它调用__get__()方法后的结果。注意__get__()方法被调用时传的第一个参数是实例,第二个参数是类。

仅仅是由于__get__()方法的存在使得一个普通函数变为一个绑定方法。函数对象真没啥特别的,任何人都可以在类的__dict__中放置带__get__()方法的对象。这样的对象被叫做descriptor,有很多用处。

创建descriptor

任何对象,只要具有__get__()方法,以及可有可无的__set__()__delete__()方法,并接受特定的参数,就被认为遵循descriptor协议。这样的对象由此成为descriptor,可以被放到一个类的__dict__之中。当相应的属性被获取、设置或者删除时,descritor能够做一些特别的事。一个空的descriptor如下例所示。

例1.3. 一个简单的descriptor

class Desc(object):
    "A descriptor example that just demonstrates the protocol"

    def __get__(self, obj, cls=None): #1
            pass

    def __set__(self, obj, val): #2
            pass

    def __delete__(self, obj): #3
            pass
  • 1 读属性时被调用(例如print objectname.attrname)。这里的obj是要访问的属性所在的对象(如果是直接访问类的属性,则为None,例如print classname.attrname)。clsobj的类(当直接访问类的属性时,cls就是类本身,此时objNone)

  • 2 在实例上设置属性时被调用(例如objectname.attrname = 12)。这里的obj是要设置属性的对象,val是作为属性值的对象。

  • 3 删除实例的属性时被调用(例如del objectname.attrname)。这里的obj是要删除的属性所在的对象。

我们在上面定义了一个类,实例化这个类就可以创建一个descriptor。让我们看看怎样创建descriptor并将它附在一个类中以便让它起作用。

例1.4. 使用descriptor

class C(object):
    "A class with a single descriptor"
    d = Desc() #1

cobj = C()

x = cobj.d #2
cobj.d = "setting a value" #3
cobj.__dict__['d'] = "try to force a value" #4
x = cobj.d #5
del cobj.d #6

x = C.d #7
C.d = "setting a value on class" #8
  • 1 现在属性d就是一个descriptor。(使用了上一个例子中的Desc。)

  • 2 调用d.__get__(cobj, C),返回值被绑定到x。这里的d指的是#1中定义的Desc的实例,即C.__dict__['d']

  • 3 调用d.__set__(cobj, "setting a value")

  • 4 直接在实例的__dict__中强行给d赋值,没有问题,但是...

  • 5 没有用,仍然调用d.__get__(cobj, C)

  • 6 调用d.__delete__(cobj)

  • 7 调用d.__get__(None, C)

  • 8 不会调用什么,会将descriptor替换成一个新的字符串对象。在这之后,访问cobj.d或者C.d只会返回字符串"setting a value on class",descriptor被踢出了C__dict__

注意当从类本身访问时,只有__get__()方法会起作用,在类上直接设置或删除属性会替换或移除相应的descriptor。

只有附在类中,descriptors才会起作用。将descriptor放在一个不是类的对象中没有任何用处。

两种descriptor

在前面的例子中,我们使用了一个同时具有__get__()__set__()方法的descriptor,这样的descriptor叫做数据(data) descriptor。只带有__get__()方法的descriptor要稍微弱一些,称为非数据(non-data) descriptor

重复我们的实验,不过这次用非数据descriptor:

例1.5. 非数据descriptor

class GetonlyDesc(object):
    "Another useless descriptor"

    def __get__(self, obj, typ=None):
        pass

class C(object):
    "A class with a single descriptor"
    d = GetonlyDesc()

cobj = C()

x = cobj.d #1
cobj.d = "setting a value" #2
x = cobj.d #3
del cobj.d #4

x = C.d #5
C.d = "setting a value on class" #6
  • 1 调用d.__get__(cobj, C)(和以前一样)。

  • 2 将"setting a value"放置在实例自己中(准确的说是在cobj.__dict__中)。

  • 3 很奇怪!返回的是"setting a value",即cobj.__dict__中的值。而之前对于数据descriptor,实例的__dict__被绕过。

  • 4 删除实例的d属性(准确的说是从cobj.__dict__中删除)。

  • 5,6 和数据descriptor的表现一样。

有趣的是,没有__set__()不仅影响属性设置,还影响属性的获取。Python在想什么?如果设置属性的时候,descriptor失去作用并把数据放在某个地方,那么就只有descriptor知道如何取回数据。还有什么必要改变实例的__dict__

数据descriptor对于提供某个属性的全部控制非常有用。人们对于用来存储数据的属性,通常都会使用数据descriptor。举个例子,如果一个属性在设置时会被转化并存储在某个地方,那么当我们读取它时一般会还原并返回它。当你有一个数据descriptor时,它会接管实例上对相应属性的所有访问(读和写)。当然,你还是可以直接去中替换descriptor,但你不能从类的实例中做这件事。

相反的是,非数据descriptor,只会在实例自身没有值的时候提供一个值。因此在一个实例上设置相应的属性会隐藏descriptor。这对于函数(函数就是非数据descriptor)的情况特别有用,因为它允许通过在实例上附上一个函数来隐藏定义在类中的同名函数。

例1.6. 隐藏一个方法

 class C(object):
     def f(self):
         return "f defined in class"

 cobj = C()

 cobj.f() #1

 def another_f():
     return "another f"

 cobj.f = another_f

 cobj.f() #2
  • 1 调用被f.__get__(cobj, C)返回的绑定方法。实质上最终调用的是C.__dict__['f'](cobj)

  • 2 调用another_f()。定义在C中的函数f()被隐藏了。

属性搜索总结

这是属性访问故事的长版本,仅仅是为了完整性而包含在这里。

当从一个对象中获取某个属性(print objectname.attrname)时,Python会执行下列步骤:

  1. 如果attrname对于objectname来说一个特殊的(比如是Python提供的)属性,返回它。

  2. objectname.__class__.__dict__中查找attrname,如果存在且是一个数据descriptor,返回descriptor结果。在objectname.__class__的全部基类中做同样的搜索。

  3. objectname.__dict__中查找attrname,如果找到则返回。如果objectname是一个类,也搜索它的基类。如果它是一个类而且在它或它基类中存在descriptor,返回descriptor结果。

  4. objectname.__class__.__dict__中查找attrname,如果存在且是一个非数据descriptor,返回descriptor结果。如果存在且不是一个descriptor,返回它。如果存在且是一个数据descriptor,我们不应该遇到这种情况,因为这种情况在第2步中就返回了。在objectname.__class__的全部基类中做同样的搜索。

  5. 抛出AttributeError异常。

注意Python首先会在类(和类的基类)中查找数据descriptor,然后是在对象的__dict__中查找这个属性,然后是在(和类的基类)中查找非数据descriptor。这分别对应于上述第2、3、4步。

上面所说的descriptor结果指的是通过合适的参数调用descriptor的__get__()方法的结果。另外,在__dict__中查找attrname指的是检查__dict__["attrname"]是否存在。

现在,看看设置一个用户定义的属性时Python采取的步骤(objectname.attrname = something):

  1. objectname.__class__.__dict__中查找attrname。如果存在且是一个数据descriptor,使用descriptor去设置值。在objectname.__class__的全部基类中做同样的搜索。

  2. objectname.__dict__中对键attrname插入值something

  3. 感觉“哇,这简单得多了!”

当设置一个Python提供的属性时会发生什么取决于要设置的属性。Python可能甚至不会允许某些属性的设置。删除属性和上面所说的设置属性非常类似。

Python提供的descriptor

在你创建自己的descriptor之前,注意Python已经自带了一些非常有用的descriptor。

例1.7. 内建descriptor

class HidesA(object):

    def get_a(self):
        return self.b - 1

    def set_a(self, val):
        self.b = val + 1

    def del_a(self):
        del self.b

    a = property(get_a, set_a, del_a, "docstring") #1


    def cls_method(cls):
        return "You called class %s" % cls

    clsMethod = classmethod(cls_method) #2


    def stc_method():
        return "Unbindable!"

    stcMethod = staticmethod(stc_method) #3
  • property提供了一种简单的方式去调用函数,当实例上的属性被获取、设置或删除时。当属性从类中直接获取时,getter方法不会被调用,而是返回property对象本身。还可以提供一个docstring,通过HidesA.a.__doc__可以访问到。

  • classmethod类似于常规的方法,只是它会将类(不是实例)作为第一个参数传给相应函数。剩余的参数和通常一样进行传递。它也能够直接在类上调用,并且具有相同的行为。第一个参数被命名为cls而非传统的的self,是为了清楚地表达出它的含义。

  • staticmethod就像类外面的一个函数。它永远不会绑定,意味着无论你怎么访问它(通过类或者实例),它都会原原本本地接收你传的参数,不会有对象作为第一个参数插入。

我们之前看到,Python的函数也是descriptor,它们在Python的早期版本中并不是descriptor(因为那时根本就没有descriptor),但是现在它们变成了一般的机制。

property总是数据descriptor,但在定义property是并非全部参数都是必需的。

Example 1.8. More on properties

class VariousProperties(object):

例1.8. 关于property的更多

class VariousProperties(object):

    def get_p(self):
        pass

    def set_p(self, val):
        pass

    def del_p(self):
        pass

    allOk = property(get_p, set_p, del_p) #1

    unDeletable = property(get_p, set_p) #2

    readOnly = property(get_p) #3
  • 1 可以被设置、获取或删除。

  • 2 尝试从实例中删除这个属性会抛出AttributeError

  • 3 尝试设置或从实例中删除这个属性会抛出AttributeError

getter和setter函数不一定要在class自身里面定义,任何函数都可以被使用。任何情况下,这些函数调用时都会接受实例作为第一个参数。注意在上例中这些函数被传给property构造器的地方,它们还不是绑定函数。

另一个需要注意的有用的结论是,子类化这个类并重定义getter(或setter)函数并不会改变property。property始终指向刚开始被定义时提供的函数,它会说“嘿我会一直保存你给我的这个函数,我只会调用这个函数然后返回结果。”,而不是说“嗯,让在当前的类中去查找一个叫'get_a'的方法然后利用那个方法”。如果那是你想要的,就自己去定义一个新的descriptor吧。怎么弄?我们可以在它初始化时传一个字符串(例如要调用的方法名),当被激活时,它通过getattr()去当前类中找这个方法名,然后利用所找到的方法。简单吧!

classmetod和staticmethod是非数据descriptor,所以如果有一个同名的属性在实例上直接被设置时它们就被隐藏。如果你在定义自己的descriptor(而且没有使用property),你可以通过给它一个__set__()方法但是在方法中抛出AttributeError的方式使它变为只读,这就是property没有setter函数时的行为。

第二章 方法解析顺序(Method Resolution Order)

问题(方法解析的混乱)

为什么我们需要方法解析顺序?

  1. 我们很乐意进行面向对象编程并创建不同层次的类。

  2. 我们实现do_your_stuff()方法通常采用的技术是首先调用基类的do_your_stuff(),然后做我们自己的事情。

例2.1. 通常的基类调用技术

class A(object):
    def do_your_stuff(self):
        # do stuff with self for A
        return

class B(A):
    def do_your_stuff(self):
        A.do_your_stuff(self)
        # do stuff with self for B
        return

class C(A):
    def do_your_stuff(self):
        A.do_your_stuff(self)
        # do stuff with self for C
        return
  1. 我们从两个类中继承生成一个新的子类,于是就存在一个超类可以通过两条路径被访问到。

例2.2. 基类调用技术失败

class D(B,C):
    def do_your_stuff(self):
        B.do_your_stuff(self)
        C.do_your_stuff(self)
        # do stuff with self for D
        return

图2.1 钻石图示

图2.1 钻石图示

  1. 现在如果我们想实现do_your_stuff()就卡住了。使用我们通常用的技术,如果我们同时调用BC,我们就会调用A.do_your_stuff()两次。我们当然知道,如果A只是准备被调用一次的话,调用A两次就可能会有问题。另一个选择是我们只调用B或只调用C,但这也不是我们想要的结果。

对于这个问题,有很麻烦的解决办法,也有干净的。明显Python实现了一个干净的,在下节中会解释。

"谁是下一个"列表

比如说:

  1. 对每一个类,我们将全部的超类无重复地放到一个有序的列表里,然后将这个类本身插入到这个列表的头部。我们将这个列表放到一个叫做next_class_list的类属性中供后面用。

例2.3. 创建"谁是下一个"列表

B.next_class_list = [B,A]

C.next_class_list = [C,A]

D.next_class_list = [D,B,C,A]
  1. 我们使用一种不同的技术来为我们的类实现do_your_stuff()

例2.4. 调用下一个方法技术

class B(A):
    def do_your_stuff(self):
        next_class = self.find_out_whos_next()
        next_class.do_your_stuff(self)
        # do stuff with self for B

    def find_out_whos_next(self):
        l = self.next_class_list           # l depends on the actual instance
        mypos = l.index(B) #1             # Find this class in the list
        return l[mypos+1]                  # Return the next one

有意思的部分在于我们如何find_out_whos_next(),这依赖于我们正在操作哪个实例。注意:

  • 根据我们传递的是D还是B的实例,next_class会被解析成C或者A

  • 我们需要为每个类实现find_out_whos_next(),因为它需要将类名硬编码在里面(看上面代码中的1处)。我们不能在这里使用self.__class__,如果我们在D的一个实例上调用do_your_stuff(),这个调用会沿着层次上溯,那么这里的self.__class__会是D

使用这个技术,每个方法只会被调用一次。它显得很干净,但似乎需要做太多的工作。对我们幸运的是,我们既不需要为每个类实现find_out_whos_next(),也不需要去设置next_class_list,因为Python帮我们把这两件事都做了。

Super解决办法

Python为每个类提供了一个叫做__mro__的类属性,还提供了一个叫做super的类型。__mro__属性是一个元组,这个元组包含了类本身和它的全部超类,以一种可预测的没有重复的方式。super对象被用来代替find_out_whos_next()方法。

例2.5. super技术

class B(A): #1
    def do_your_stuff(self):
        super(B, self).do_your_stuff() #2
        # do stuff with self for B
  • super()调用创建一个super对象。它在self.__class__.__mro__之中查找B之后的下一个类。在super对象上访问的属性在下一个类上搜索并返回,descriptors也会得到解析。这意味着访问某个方法(像上面一样)返回的是绑定方法(注意do_your_stuff()调用并没有传递self)。当使用super()时,第一个参数应该始终和它所在的类相同(例中1处)。

如果我们使用的是一个类方法,我们就不会有一个叫self的实例传给super调用。幸运的是,即使第二个参数是一个类,super依然可以照常工作。观察上例,super使用self仅仅是为了得到self.__class__.__mro__。类可以直接被传给super,像下例中一样。

例2.6. 通过类方法使用super

class A(object):
    @classmethod #1
    def say_hello(cls):
        print 'A says hello'

class B(A):
    @classmethod
    def say_hello(cls):
        super(B, cls).say_hello() #2
        print 'B says hello'

class C(A):
    @classmethod
    def say_hello(cls):
        super(C, cls).say_hello()
        print 'C says hello'

class D(B, C):
    @classmethod
    def say_hello(cls):
        super(D, cls).say_hello()
        print 'D says hello'

B.say_hello() #3
D.say_hello() #4
  • 1 这个例子说的是类方法(不是实例方法)。

  • 2 注意我们传给super()的是cls(类而非实例)。

  • 3 这个会打印出:

    A says hello B says hello

  • 4 这个会打印出(注意每个方法只被调用了一次):

    A says hello C says hello B says hello D says hello

还有另一种使用super的方式:

例2.7. 另一个super技术

class B(A):

    def do_your_stuff(self):
        self.__super.do_your_stuff()
        # do stuff with self for B

B._B__super = super(B) #1

当只用类型来创建时,super实例表现就像descriptor。这意味着(如果dD的实例)super(B).__get__(d)super(B,d)返回的东西一样。在上面的1处,我们改写了一个属性名,类似于Python对于类里面的以双下划线开头的名字所做的事。因此它在类中能够以self.__super的形式被访问。如果我们不使用一个特定于类的属性名,通过实例self访问属性可能会返回一个定义在子类中的对象。

在使用super时,我们一般在一个方法中只会调用一次super,即使类有多个基类。而且,使用super代替直接调用基类上的方法是一个好的编程习惯。

如果do_your_stuff()CA分别接受的是不同的参数,一个可能的陷阱就会出现。因为,如果我们在B中使用super来调用下一个类的do_your_stuff(),我们不知道它是会在A还是C上被调用。如果这种场景不可避免,那就需要特定问题特别对待了。

计算方法解析顺序(MRO)

一个还未回答的问题是Python是如何决定一个类型的__mro__的?这节会解释其算法后面的基本思想。对于使用super或者阅读后面的章节这并不是必要的,所以你可以跳到下一节,如果你想的话。

Python通过两种由用户指定的限制来决定类型的优先级(或者说它们在任意_mro__中应该按何种顺序放置):

  1. 如果AB的超类,则B的优先级高于A。或者说,在所有的__mro__(同时包含了两者)中,B应该总是出现在A之前。为了简洁,我们将这个关系表示为B > A

  2. 如果在类语句(例如class Z(C,D):)的基类列表中,C出现在D之前,则C > D

另外,为了避免含混,Python坚持下面的原则:

  1. 如果在一个场景(或者一个__mro__)中E > F,那么在所有场景(或者所有__mro__)中E > F

我们能够满足上述限制,如果在我们为每一个我们引入的新的类C构建__mro__时,使得:

  1. C全部超类出现在C.__mro__(加上C本身,在最前面)之中,而且

  2. 对于C.__bases__里面的每个BC.__mro__里面的类型的优先级不会与B.__mro__中类型的优先级冲突。

这里将这个问题转换成一个游戏。考虑一个类层次如下所示:

图2.2 简单的层次

图2.2 简单的层次

因为只有单继承,所以找出这些类的__mro__很容易。比如我们定义了一个新的类class N(A,B,C),为了计算__mro__,考虑玩一个游戏,这个游戏使用串在一系列线上的像算盘一样的珠子。

图2.3 串在线上的珠子-不可解

图2.3 串在线上的珠子-不可解

珠子能够在线上自由移动,但是线不能被剪断或扭曲。从左到右的的线按顺序包含了每个基类的__mro__。最右边的线包含着对应每个基类的小珠,按照类定义中基类声明的顺序。

目标是将珠子按行排列整齐,使得每行只包含同一种标签的珠子(就像图中的O珠一样)。每根线都代表着一个顺序限制,如果我们能达到目标,我们就能得到一种满足全部限制的顺序。那样的话我们就只需要从底往上读每行的标签来得到N__mro__

不幸的是,我们不能解决这个问题。后两根线上的CB有不同的顺序。但是,如果我们改边类定义为class N(A,C,B),我们就有了希望。

图2.4 串在线上的珠子-可解

图2.4 串在线上的珠子-可解

我们可以发现N.__mro__(N,A,C,B,object)(注意我们将N插到了前面)。读者可以在真正的Python里面尝试这个试验(对于上述不可解的情形,Python会抛出异常)。注意我们甚至交换了两根线的位置,以保持线的顺序和类定义中指定的基类的顺序一致。这样做的用处在后面会看到。

有时,可能存在不止一种解法,就像下图中所示的。考虑4个类class A(object)class B(A)class C(object)class D(C)。如果一个新类被定义为class E(B, D),就存在多种可能的满足所有限制的解法。

图2.5 多种解法

图2.5 多种解法

A的可能的位置在图中以小珠子示出。如果采取下面的策略,则可以使顺序变得唯一:

  1. 按照类定义中基类出现的顺序从左到右排列各线。

  2. 尝试按行从底往上、从左往右排列小珠。这意味着class E(B, D)的MRO会被设置成:(E,B,A,D,C,object)。这是因为A,由于在C的左边,于是在从底往上第二行时会先被考虑。

这本质上就是Python用于为新类型产生__mro__的算法的背后思想。关于这个算法的正式的描述可以参见mro-algorithm

第三章 用法说明

这一章包括一些没有在其他章节中提到的用法说明。

特殊方法

在Python中,我们可以使用一些带有特殊名字的方法,例如__len__()__str__()__add__(),去使得对象便于被使用(例如,通过内建函数len()str(),或者+操作符,等等)。

例3.1. 特殊方法只能用在类型上

class C(object):
    def __len__(self): #1
        return 0

cobj = C()

def mylen():
    return 1

cobj.__len__ = mylen #2

print len(cobj) #3
  • 1 通常我们将特殊方法放在类中。

  • 2 我们可以试着将它们放到实例本身中,但不会起作用。

  • 3 这会直接去到类中(调用C.__len__()),而不是实例。

对于所有这样的方法都是如此,将它们放在我们想要应用的实例上不会起作用。如果真的去实例中,那么甚至像str(C)(类Cstr)都会调用C.__str__(),而它是为C的实例定义的,并非为C本身。

想允许为每个实例分别定义这样的方法,可以采用如下所示的简单技术:

例3.2. 将特殊方法传递给实例

class C(object):
    def __len__(self): #1
        return self._mylen() 

    def _mylen(self): #2
        return 0

cobj = C()

def mylen():
    return 1

cobj._mylen = mylen #3

print len(cobj) #4
  • 1 我们调用实例上的另一个方法,

  • 2 在类中我们提供这个方法的一个默认实现。

  • 3 但是它能够通过设置在实例上而被覆盖(更确切地说是隐藏)。

  • 4 现在调用的是mylen()

子类化内建类型

子类化内建类型很简单。实际上我们一直在这样做(当我们子类化<type 'object'>时)。有一些内建类型(例如types.FunctionType)不可被子类化(至少目前还不能)。但是,我们这里讨论的是子类化<type 'list'>,<type 'tuple'>和其他的基本数据类型。

例3.3. 子类化<type 'list'>

>>> class MyList(list): #1
...     "A list that converts appended items to ints"
...     def append(self, item): #2
...         list.append(self, int(item)) #3
...
>>>
>>> l = MyList() 
>>> l.append(1.3) #4
>>> l.append(444) 
>>> l
[1, 444] #5
>>> len(l) #6
2
>>> l[1] = 1.2 #7
>>> l
[1, 1.2]
>>> l.color = 'red' #8
  • 1 常规的class定义语句

  • 2 定义要覆盖的方法。在这个例子中我们将所有传给append()的项都转换成整数。

  • 3 如果有需要可以向上调用基类的方法。list.append()像一个未绑定的方法一样工作,被传入的第一个参数是实例。

  • 4 append一个浮点数,然后...

  • 5 看到它自动变成了整数。

  • 6 除此以外,它跟任何其他的list行为一样。

  • 7 这个操作不会经过append。我们需要在我们的类中定义__setitem__()去操纵我们的数据。相应向上的基类调用是list.__setitem__(self,item)。注意像__setitem__这样的特殊方法存在于内建类型上。

  • 8 我们能够在我们的实例上设置属性,因为它具有__dict__

基本的list没有__dict__(因此没有用户定义的属性),但是我们的有。这通常不是个问题而且甚至是我们想要的。但是,如果我们使用非常大量的MyList,我们可以优化我们的程序,通过告诉Python不为MyList的实例创建__dict__

例3.4. 使用stots优化程序

class MyList(list):
    "A list subclass disallowing any user-defined attributes"

    __slots__ = [] #1

ml = MyList()
ml.color = 'red' # raises exception! #2

class MyListWithFewAttrs(list):
    "A list subclass allowing specific user-defined attributes"

    __slots__ = ['color'] #3

mla = MyListWithFewAttrs()
mla.color = 'red' #4
mla.weight = 50 # raises exception! #5
  • __slots__类属性告诉Python不为这个类型的实例创建__dict__

  • 2 在它上面设置任何一个属性都会抛出异常。

  • __slots__可以包含一个字符串的列表。实例仍然不会为__dict__分配一个真正的字典,而是得到一个代理。Python在实例中为指定的属性保留空间。

  • 4 现在,如果属性有保留的空间,它就能被使用。

  • 5 否则,就不行,会抛出异常。

__slots__的目的和建议用法是优化。当一个类型被定义后,它的slots不能被改变。而且,每个子类都必须定义__slots__,否则,子类的实例仍然会有__dict__

我们甚至能够像实例化任意其他类型一样实例化list来创建它:list([1,2,3])。这意味着list.__init__()接受同样的参数(比如任何iterable)然后初始化一个list。我们可以在子类中定制初始化过程,通过重定义__init__()然后向上调用基类的__init__()方法。

元组是不可变的,和列表不同。一旦一个实例被创建,它就不能被改变。注意当__init__()方法被调用时,类型的实例已经存在(事实上实例是被作为第一人参数传递的)。类型的静态方法__new__()被调用来创建类型的一个实例。类型本身被作为第一个参数传递,然后是其他初始化参数(类似于__init__())。我们用此来定制像元组这样的不可变类型。

例3.5. 定制子类的创建

class MyList(list):

    def __init__(self, itr): #1
        list.__init__(self, [int(x) for x in itr])

class MyTuple(tuple):

    def __new__(typ, itr): #2
        seq = [int(x) for x in itr]
        return tuple.__new__(typ, seq) #3
  • 1 对于列表,我们操纵参数然后将它们传递给list.__init__()

  • 2 以于元组,我们需要覆盖__new__()

  • __new__()应该总是返回。它应该返回类型的一个实例。

__new__()方法不是为不可变类型而特别存在的,它用来所有类型里。而且它会被Python自动转换成静态方法(根据它的名字)。

相关资料

descrintro. Guido van Rossum.

pep-252. Guido van Rossum.

pep-253. Guido van Rossum.

descriptors-howto. Raymond Hettinger.

mro-algorithm. Michele Simionato.


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值