本文作为“一切皆对象”的下篇,主要介绍对象的属性(attribute)和方法(method)。

什么是属性和方法?

Python官方术语对照表中是这样定义的:

Attribute: A value associated with an object which is usually referenced by name using dotted expressions. It is possible to give an object an attribute whose name is not an identifier as defined by Identifiers and keywords, for example using setattr(), if the object allows it. Such an attribute will not be accessible using a dotted expression, and would instead need to be retrieved with getattr().

Method: A function which is defined inside a class body. If called as an attribute of an instance of that class, the method will get the instance object as its first argument (which is usually called self). See function and nested scope.

这里的“值(value)”其实没有确切定义,换成对象可能更加自洽,即属性就是从一个对象获得另一个对象的方式。属性可以是狭义上的值,也可以是函数或类。

属性字典

对象的属性是住在字典里的,这不难理解,因为一个属性名对应一个属性值,这自然就是一个字典。在Python中可以通过__dict__方法获得对象的属性字典:

class Cls:
 """I am class Cls"""
    cls_attr = 'a class attribute'  
    def foo(self): print('I am function foo, you are', self)
    class Who_am_i: print('I am who')
  
  
cls_obj = C()  
print(cls_obj.__dict__)
print(Cls.__dict__)

# {}
# {'__module__': '__main__', '__doc__': 'I am class Cls', 'cls_attr': 'a class attribute', 'foo': <function Cls.foo at 0x00000162DECFE2A0>, 'Who_am_i': <class '__main__.Cls.Who_am_i'>, '__dict__': <attribute '__dict__' of 'Cls' objects>, '__weakref__': <attribute '__weakref__' of 'Cls' objects>}

说明对象的属性字典中只存放该对象的“直接属性”。事实上,访问对象的属性时,将依次搜索以下属性字典:

  • 对象本身的字典:objectname.__dict__
  • 对象所属类的字典:objectname.__class__.__dict__
  • 对象所属类的父类的字典:superclass.__dict__ for superclass in objectname.__class__.__bases__

特别地,访问类(type)的属性时,搜索父类的字典将先于搜索所属元类的字典。最后,若找不到属性名,则报错AttributeError。属性字典的引入让人不禁想问:

  • 赋值表达式是否等价于在属性字典中直接加入键值对?
  • 在类外定义的属性是否可以被事先的实例化对象访问?
cls_obj.inst_attr = 'an instance attribute'  
cls_obj.__dict__['another_inst_attr'] = 'another instance attribute'  
print(cls_obj.__dict__)  
  
# {'inst_attr': 'an instance attribute', 'another_inst_attr': 'another instance attribute'}  
  
Cls.another_cls_attr = 'another class attribute'  
print(cls_obj.another_cls_attr)  
  
# another class attribute

说明答案都是肯定的。另外,可以通过__dir__方法获得对象的能够访问的所有属性(数组),相当于上述所有字典的键放在一起:

print(cls_obj.__dir__())  
print(Cls.__bases__)  
print(set(cls_obj.__dir__()) == set(list(cls_obj.__dict__.keys()) + list(Cls.__dict__.keys()) + list(Cls.__base__.__dict__.keys())))  
  
# ['inst_attr', 'another_inst_attr', '__module__', '__doc__', 'cls_attr', 'foo', 'Who_am_i', '__dict__', '__weakref__', 'another_cls_attr', '__new__', '__repr__', '__hash__', '__str__', '__getattribute__', '__setattr__', '__delattr__', '__lt__', '__le__', '__eq__', '__ne__', '__gt__', '__ge__', '__init__', '__reduce_ex__', '__reduce__', '__getstate__', '__subclasshook__', '__init_subclass__', '__format__', '__sizeof__', '__dir__', '__class__']  
# (<class 'object'>,)  
# True

__slots__方法

字典确实是一种十分高效的数据结构,但是每个对象都拥有一个字典似乎有些过于奢侈了。此时可以通过__slots__方法来告诉类不要再为实例创建字典了,同时实例的属性被打包放在__slots__中(可以是数组、元组或集合),实例化之后的对象无法添加__slots__以外的属性:

class Cls:  
    __slots__ = ['who']  
  
  
print(Cls.__dict__)  
cls_obj = Cls()  
cls_obj.name = 'csc'

# {'__module__': '__main__', '__slots__': ['who'], 'who': <member 'who' of 'Cls' objects>, '__doc__': None}
# AttributeError

函数和方法

由定义若属性是一个函数,则称为方法。方法作为类的属性被调用时,和普通的函数没什么区别。但是作为实例化对象的属性被调用时,会将实例作为第一个参数self传入,此时称为绑定方法(bound method)。举个栗子:

print(Cls.foo)  
Cls.foo('a Dota2 master') 

print(cls_obj.foo)  
cls_obj.foo()  

print(cls_obj.foo == Cls.foo.__get__(cls_obj))

# <function Cls.foo at 0x000002011248E2A0>  
# I am function foo, you are a Dota2 master  
# <bound method Cls.foo of <__main__.Cls object at 0x00000201124E8ED0>>  
# I am function foo, you are <__main__.Cls object at 0x00000201124E8ED0>  
# True

这里我是这么想的,比如鸭子叫是鸭子这个类里边的方法,但是某个鸭子叫的时候必须得用它自己的嗓子。另外,__get__方法把函数foo变成了实例cls_obj绑定方法,即foo.__get__(cls_obj)(*args, **kargs)等价于foo(cls_obj, *args, **kargs)

描述器(Descriptor)

定义:

Descriptor: Any object which defines the methods __get__, __set__ or __delete__

定义了其中一种方法的对象就称为描述器,比如函数就是描述器。另外,若定义了__set____delete__方法则称为数据描述器,若仅定义了__get__方法则称为非数据描述器。其区别在于,如果属性字典中有与数据描述器同名的属性,则数据描述器优先;如果属性字典中有与非数据描述器同名的属性,则该属性优先。

以下是一个简单的“指鹿为马”的例子:

class Data_desc:  
value = 'deer or horse'
 
    def __get__(self, instance, owner):  
        print('Now get sth', self, instance, owner)  
        return self.value  
  
    def __set__(self, instance, value):  
        print('Now set sth', self, instance, value)  
        self.value = 'horse'
  
  
class Cls:  
    name = Data_desc()  
  
  
cls_obj = Cls()
print(cls_obj.name)
cls_obj.name = 'deer'  
print(cls_obj.name)

# Now get sth <__main__.Data_desc object at 0x0000013A5C6D8D90> <__main__.Cls object at 0x0000013A5C6D9B10> <class '__main__.Cls'>  
# deer or horse  
# Now set sth <__main__.Data_desc object at 0x0000013A5C6D8D90> <__main__.Cls object at 0x0000013A5C6D9B10> deer  
# Now get sth <__main__.Data_desc object at 0x0000013A5C6D8D90> <__main__.Cls object at 0x0000013A5C6D9B10> <class '__main__.Cls'>  
# horse

而这样是不可以的:

class Non_data_desc:  
    def __get__(self, instance, owner): 
		return 'horse'  

  
class Cls:   
    name = Non_data_desc()  
  
  
cls_obj = Cls()  
print(cls_obj.name)  
cls_obj.name = 'deer'  
print(cls_obj.name)

# horse
# deer

描述器允许我们在属性的访问、赋值和删除操作时动些手脚,简单的比如设定取值范围和设置只读等等,复杂的可以看这里

几个内置描述器

Python中有几个内置描述器:

  • property: 在类内直接创建描述器
  • classmethod: 将实例绑定方法变成类绑定方法,即实例化对象调用时将类作为第一个参数传入
  • staticmethod: 将方法变成函数,即实例化对象调用时不再将实例作为第一个参数传入
class Cls:  
 # property: 在类内直接创建描述器
    def get_prop(self):  
        print('get_prop')  
        return self._name  
  
    def set_prop(self, name):  
        print('set_prop')  
        self._name = 'horse'  
  
    def del_prop(self):  
        print('del_prop')  
        del self._name  
  
    name = property(get_prop, set_prop, del_prop, 'here is for doc')  

 # classmethod: 将实例绑定方法变成类绑定方法
    def cls_method(cls):  
        return cls  

    class_method = classmethod(cls_method)  

 # staticmethod: 将方法变成函数
    def stc_method(self):  
        return self  
  
    static_method = staticmethod(stc_method)  
  
  
cls_obj = Cls()  
cls_obj.name = 'deer'  
print(cls_obj.name)  

print(cls_obj.class_method()) 

print(cls_obj.static_method('not cls_obj'))  
  
# set_prop  
# get_prop  
# horse  
# <class '__main__.Cls'>  
# not cls_obj

另外,关于描述器property还有一些别的用法,这个坑等到装饰器的时候再填吧。

MRO

MRO的全称是Method Resolution Order,即方法解析序列。其解决的问题是,在搜索父类(可能有多个)的属性字典时,如果多个父类同时拥有被访问的属性,访问的优先级如何确定。举个栗子:

class A(object):  
    @classmethod  
    def do(cls): print('A', cls)  
  
  
class B(object):  
    @classmethod  
    def do(cls): print('B', cls)  
  
  
class C(A, B): pass  
  
  
class D(B, A): pass  


print(C.__mro__)  
print(D.__mro__) 
c = C()
d = D()  
c.do()  
d.do()   

# (<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <class 'object'>)  
# (<class '__main__.D'>, <class '__main__.B'>, <class '__main__.A'>, <class 'object'>)
# A <class '__main__.C'>  
# B <class '__main__.D'> 

说明访问do时,实例c先搜索所属类C的父类A的字典,而实例d先搜索所属类D的父类B的字典,即继承关系决定MRO决定搜索顺序。

确定和计算MRO

类的MRO以元组形式出现,其中包含该类本身及其继承的所有父类,并且遵循以下四条规则:

  1. 若类A继承类B,则类A优先于类B(即在MRO中类A在类B的左边),以下记为A > B
  2. 若在某个类的继承顺序中类C先于类D(比如E(C, D)),以下记为C >> D,则C > D
  3. 若在某个MRO中F > G,则在任意MRO中F > G
  4. 在以上三条满足的情况下,若H > I,H >> J,则I > J

其中第四条是后于前三条结算的,作用是防止多解的情况。另外,MRO的最后一个元素一定是类object,因为其是所有类(type)的父类。

MRO一定存在吗?答案是不一定,一个最简单的反例是:

class A: pass  
  
  
class B(A): pass  # B > A
  
  
class C(A, B): pass  # A > B

# TypeError: Cannot create a consistent method resolution order (MRO) for bases A, B

基于以上规则我们可以大概了解MRO的原理,但是并不方便实现。Python中可以直接使用__mro__方法查看类的MRO,该方法底层是使用C3算法实现的,这里就不介绍辣,见这里还有这里

super

对于类super我们首先可以通过__doc__方法查看其文档(docstring):

print(super.__doc__)

# super() -> same as super(__class__, <first argument>)  
# super(type) -> unbound super object  
# super(type, obj) -> bound super object; requires isinstance(obj, type)  
# super(type, type2) -> bound super object; requires issubclass(type2, type)  
# Typical use to call a cooperative superclass method:  
# class C(B):  
#     def meth(self, arg):  
#         super().meth(arg)  
# This works for class methods too:  
# class C(B):  
#     @classmethod  
#     def cmeth(cls, arg):  
#         super().cmeth(arg)

即:

  • 当不传入参数时,super会将所身处的类作为第一个参数,所身处的函数的第一个参数(selfcls)作为第二个参数。
  • 当仅传入第一个参数type(是一个类)时,super返回一个非绑定实例化对象,其“长得就像”一个描述器一样,即super(type).__get__(obj)等价于super(type, obj)
  • 当传入两个参数时,第二个参数可以是任何对象obj,但是必须是第一个参数type的实例或是子类。此时super返回一个obj绑定实例化对象,这个对象非常之神秘(目前还没有搞懂),但是“长得就像”obj所属类(如果是类则为自身)的MRO中type的后一个元素。
  • super用来调用父类的方法。

举个例子:

class A(object):  
    @classmethod  
    def do(cls):  
        print('A', cls)  
  
  
class B(A):  
    @classmethod  
    def do(cls):  
        super().do()  
        print('B', cls)  
  
  
class C(A):  
    @classmethod  
    def do(cls):  
        super(C).__get__(cls).do()  
        print('C', cls)  
  
  
class D(B, C):  
    @classmethod  
    def do(cls):  
        super(D, cls).do()  
        print('D', cls)


D.do()

# A <class '__main__.D'>  
# C <class '__main__.D'>  
# B <class '__main__.D'>  
# D <class '__main__.D'>

总之,super想要回答的是MRO中“谁是下一个”的问题,更多关于super的内容见这里还有这里

总结

本文从对象的属性和方法讲起,浅尝了Python中的属性字典、描述器、MRO和类super等概念和工具。

  • 什么是属性?在哪看属性? $\Rightarrow$ 属性字典 $\Rightarrow$ 访问属性的搜索过程 $\Rightarrow$ __slots__方法取消字典
  • 属性访问自定义? $\Rightarrow$ 描述器 $\Rightarrow$ 两种描述器 $\Rightarrow$ 三个内置描述器
  • 多类继承时的搜索优先级? $\Rightarrow$ MRO $\Rightarrow$ MRO的确定和计算 $\Rightarrow$ “谁是下一个”:类super 虽然和上一篇一样还有很多坑没有填,但是就这样吧,也许之后会填呢。

参考

  1. Python官方术语对照表
  2. Python Attributes and Methods
  3. Descriptor HowTo Guide
  4. The Python 2.3 Method Resolution Order
  5. Python 进阶技巧: 类与继承
  6. Python’s super() considered super!
  7. Python多重继承super()的MRO坑