离线下载
PDF版 ePub版

qyuhen · 更新于 2018-11-28 11:00:43

由于历史原因,Python 2.x 同时存在两种类模型,算是个不大不小的坑。面向对象思想的演变也在影响着语言的进化,单根继承在 Python 中对应的是 New-Style Class,而非 Classic Class。

Python 3 终于甩掉包袱,仅保留 New-Style Class。所以呢,就算还在用 2.x 开发,也别再折腾 Classic Class,踏踏实实从 object 继承,或在源文件设置默认元类。

>>> class User: pass

>>> type(User)    # 2.x 默认是 Classic Class。
<type 'classobj'>

>>> issubclass(User, object)  # 显然不是从 object 继承。
False

>>> __metaclass__ = type   # 指定默认元类。

>>> class Manager: pass   # 还是没有显式从 object 继承。

>>> type(Manager)    # 但已经是 New-Style Class。
<type 'type'>

>>> issubclass(Manager, object)  # 确定了!
True

本书所有内容均使用 New-Style Class。

名字空间

类型是类型,实例是实例。如同 def,关键字 class 的作用是创建类型对象。前面章节也曾提到过,类型对象很特殊,在整个进程中是单例的,是不被回收的。

typedef struct
{
    PyObject_HEAD
    PyObject*cl_bases;  /* A tuple of class objects */
    PyObject*cl_dict;  /* A dictionary */
    PyObject*cl_name;  /* A string */

    PyObject*cl_getattr;
    PyObject*cl_setattr;
    PyObject*cl_delattr;
} PyClassObject;

因为 New-Style Class,Class 和 Type 总算是一回事了。

>>> class User(object): pass
>>> u = User()

>>> type(u)
<class '__main__.User'>

>>> u.__class__
<class '__main__.User'>

类型 (class) 存储了所有的静态字段和方法 (包括实例方法),而实例 (instance) 仅存储实例字段,从基类 object 开始,所有继承层次上的实例字段。官方文档将所有成员统称为 Attribute。

typedef struct
{
    PyObject_HEAD
    PyClassObject *in_class;  /* The class object */
    PyObject *in_dict;  /* A dictionary */
    PyObject *in_weakreflist;  /* List of weak references */
} PyInstanceObject;

类型和实例各自拥有自己的名字空间。

>>> User.__dict__
<dictproxy object at 0x106eaa718>

>>> u.__dict__
{}

访问对象成员时,就从这几个名字空间中查找,而非以往的 globals、locals。

成员查找顺序: instance.__dict__ -> class.__dict__ -> baseclass.__dict__

注意分清对象成员和普通名字的差别。就算在对象方法中,普通名字依然遵循 LEGB 规则。

字段

字段 (Field) 和 属性 (Property) 是不同的。

  • 实例字段存储在 instance.dict,代表单个对象实体的状态。
  • 静态字段存储在 class.dict,为所有同类型实例共享。
  • 必须通过类型和实例对象才能访问字段。
  • 以双下划线开头的 class 和 instance 成员视为私有,会被重命名。(module 成员不变)
>>> class User(object):
...     table = "t_user"
...     def __init__(self, name, age):
...     self.name = name
...     self.age = age

>>> u1 = User("user1", 20)   # 实例字段存储在 instance.__dict__。
>>> u1.__dict__    
{'age': 20, 'name': 'user1'}

>>> u2 = User("user2", 30)   # 每个实例的状态都是相互隔离的。
>>> u2.__dict__
{'age': 30, 'name': 'user2'}

>>> for k, v in User.__dict__.items():  # 静态字段存储在 class.__dict__。
...     print "{0:12} = {1}".format(k, v)

__module__ = __main__
__dict__ = <attribute '__dict__' of 'User' objects>
__init__ = <function __init__ at 0x106eb4398>
table = t_user

可以在任何时候添加实例字段,仅影响该实例名字空间,与其他同类型实例无关。

>>> u1.x = 100

>>> u1.__dict__
{'x': 100, 'age': 20, 'name': 'user1'}

>>> u2.__dict__
{'age': 30, 'name': 'user2'}

要访问静态字段,除了 class. 外,也可以用 instance.。按照成员查找规则,只要没有同名的实例成员,那么就继续查找 class.dict

>>> User.table    # 使用 class.<name> 查找静态成员。
't_user'

>>> u1.table    # 使用 instance.<name> 查找静态成员。
't_user'

>>> u2.table    # 静态成员为所有实例对象共享。
't_user'

>>> u1.table = "xxx"   # 在 instance.__dict__ 创建一个同名成员。

>>> u1.table    # 这回按照查找顺序,命中的就是实例成员了。
'xxx'

>>> u2.table    # 当然,这不会影响其他实例对象。
't_user'

面向对象一个很重要的特征就是封装,它隐藏对象内部实现细节,仅暴露用户所需的接口。因此私有字段是极重要的,可避免非正常逻辑修改。

私有字段以双下划线开头,无论是静态还是实例成员,都会被重命名: ___

>>> class User(object):
...     __table = "t_user"
...
...     def __init__(self, name, age):
...         self.__name = name
...         self.__age = age
...
...     def __str__(self):
...         return "{0}: {1}, {2}".format(
...             self.__table,   # 编码时无需关心重命名。
...             self.__name,
...             self.__age)

>>> u = User("tom", 20)

>>> u.__dict__      # 可以看到私有实例字段被重命名了。
{'_User__name': 'tom', '_User__age': 20}

>>> str(u)
't_user: tom, 20'

>>> User.__dict__.keys()     # 私有静态字段也被重命名。
['_User__table', ...]

某些时候,我们既想使用私有字段,又不想放弃外部访问权限。

  • 用重命名后的格式访问。
  • 只用一个下划线,仅提醒,不重命名。

不必过于纠结 "权限" 这个词,从底层来看,本就没有私有一说。

属性

属性 (Property) 是由 getter、setter、deleter 几个方法构成的逻辑。属性可能直接返回字段值,也可能是动态逻辑运算的结果。

属性以装饰器或描述符实现,原理以后再说。实现规则很简单,也很好理解。

>>> class User(object):
...     @property
...     def name(self): return self.__name  # 注意几个方法是同名的。
...
...     @name.setter
...     def name(self, value): self.__name = value
...
...     @name.deleter
...     def name(self): del self.__name

>>> u = User()
>>> u.name = "Tom"  

>>> u.__dict__   # 从 instance.__dict__ 可以看出属性和字段的差异。
{'_User__name': 'Tom'}

>>> u.name    # instance.__dict__ 中并没有 name,显然是 getter 起作用了。
'Tom'
>>> del u.name   # 好吧,这是 deleter。
>>> u.__dict__
{}

>>> for k, v in User.__dict__.items():
...     print "{0:12} = {1}".format(k, v)
...
__module__  = __main__
__dict__    =<attribute '__dict__' of 'User' objects>
name        = <property object at 0x106ed6100>

从 class.dict 可以看出,几个属性方法最终变成了 property object。这也解释了几个同名方法为何没有引发错误。既然如此,我们可以直接用 property() 实现属性。

>>> class User(object):
...     def get_name(self): return self.__name
...     def set_name(self, value): self.__name = value
...     def del_name(self): del self.__name
...     name = property(get_name, set_name, del_name, "help...")

>>> for k, v in User.__dict__.items():
...     print "{0:12} = {1}".format(k, v)

__module__ = __main__
__dict__   = <attribute '__dict__' of 'User' objects>
set_name   = <function set_name at 0x106eb4b18>
del_name   = <function del_name at 0x106eb4b90>
get_name   = <function get_name at 0x106eb4aa0>
name = <property object at 0x106ec8db8>

>>> u = User()
>>> u.name = "Tom"
>>> u.__dict__
{'_User__name': 'Tom'}

>>> u.name
'Tom'

>>> del u.name
>>> u.__dict__
{}

区别不大,只是 class.dict 中保留了几个方法。

属性方法多半都很简单,用 lambda 实现会更加简洁。鉴于 lambda 函数不能使用赋值语句,故改用 setattr。还得注意别用会被重命名的私有字段名做参数。

>>> class User(object):
...     def __init__(self, uid):
...         self._uid = uid
...
...     uid = property(lambda o: o._uid)    # 只读属性。
...
...     name = property(lambda o: o._name, \    # 可读写属性。
...         lambda o, v: setattr(o, "_name", v))

>>> u = User(1)

>>> u.uid
1
>>> u.uid = 100
AttributeError: can't set attribute

>>> u.name = "Tom"
>>> u.name
'Tom'

不同于前面提过的对象成员查找规则,属性总是比同名实例字段优先。

>>> u = User(1)

>>> u.name = "Tom"
>>> u.__dict__
{'_uid': 1, '_name': 'Tom'}

>>> u.__dict__["uid"] = 1000000   # 显式在 instance.__dict__ 创建同名实例字段。
>>> u.__dict__["name"] = "xxxxxxxx"

>>> u.__dict__
{'_uid': 1, 'uid': 1000000, 'name': 'xxxxxxxx', '_name': 'Tom'}

>>> u.uid      # 访问的依旧是属性。
1

>>> u.name
'Tom'

尽可能使用属性,而不是直接暴露内部字段。

方法

实例方法和函数的最大区别是 self 这个隐式参数。

>>> class User(object):
...     def print_id(self):
...         print hex(id(self))

>>> u = User()

>>> u.print_id
<bound method User.print_id of <__main__.User object at 0x10cf58b50>>

>>> u.print_id()
0x10cf58b50

>>> User.print_id
<unbound method User.print_id>

>>> User.print_id(u)
0x10cf58b50

从上面的代码可以看出实例方法的特殊性。当用实例调用时,它是个 bound method,动态绑定到对象实例。而当用类型调用时,是 unbound method,必须显式传递 self 参数。

那么静态方法呢?为什么必须用 staticmethod、classmethod 装饰器?

>>> class User(object):
...     def a(): pass
...
...     @staticmethod
...     def b(): pass
...
...     @classmethod
...     def c(cls): pass

>>> User.a
<unbound method User.a>

>>> User.b
<function b at 0x10c8ef320>

>>> User.c
<bound method type.c of <class '__main__.User'>>

不使用装饰器的方法 a,将被当做了实例方法,自然不能以静态方法调用。

>>> User.a()
TypeError: unbound method a() must be called with User instance as first argument (got
nothing instead)

装饰器 classmethod 绑定了类型对象作为隐式参数。

>>> User.b()

>>> User.c()
<class '__main__.User'>

除了上面说的这些特点外,方法的使用和普通函数类似,可以有默认值、变参。实例方法隐式参数 self 只是习惯性命名,可以用你喜欢的任何名字。

说到对象,总会有几个特殊的可选方法:

  • new: 创建对象实例。
  • init: 初始化对象状态。
  • del: 对象回收前被调用。
>>> class User(object):
...     def __new__(cls, *args, **kwargs):
...         print "__new__", cls, args, kwargs
...         return object.__new__(cls)
...
...     def __init__(self, name, age):
...         print "__init__", name, age
...
...     def __del__(self):
...         print "__del__"

>>> u = User("Tom", 23)
__new__ <class '__main__.User'> ('Tom', 23) {}
__init__ Tom 23

>>> del u
__del__

构造方法 new 可返回任意类型,但不同的类型会导致 init 方法不被调用。

>>> class User(object):
...     def __new__(cls, *args, **kwargs):
...         print "__new__"
...         return 123
...
...     def __init__(self):
...         print "__init__"

>>> u = User()
__new__

>>> type(u)
<type 'int'>

>>> u
123

在方法里访问对象成员时,必须使用对象实例引用。否则会当做普通名字,依照 LEGB 规则查找。

>>> table = "TABLE"

>>> class User(object):
...     table = "t_user"
...
...     def __init__(self, name, age):
...         self.__name = name
...         self.__age = age
...
...     def tostr(self):
...         return "{0}, {1}".format(
...             self.__name, self.__age) # 使用 self 引用实例字段。
...
...     def test(self):
...         print self.tostr()   # 使用 self 调用其他实例方法。
...         print self.table   # 使用 self 引用静态字段。
...         print table    # 按 LEGB 查找外部名字空间。

>>> User("Tom", 23).test()
Tom, 23
t_user
TABLE

因为所有方法都存储在 class.dict,不可能出现同名主键,所以不支持方法重载 (overload)。

继承

除了所有基类的实例字段都存储在 instance.dict 外,其他成员依然是各归各家。

>>> class User(object):
...     table = "t_user"
...
...     def __init__(self, name, age):
...         self._name = name
...         self._age = age
...
...     def test(self):
...         print self._name, self._age

>>> class Manager(User):
...     table = "t_manager"
...
...     def __init__(self, name, age, title):
...         User.__init__(self, name, age)  # 必须显式调用基类初始化方法。
...         self._title = title
...
...     def kill(self):
...         print "213..."

>>> m = Manager("Tom", 40, "CXO")

>>> m.__dict__      # 实例包含了所有基类的字段。
{'_age': 40, '_title': 'CXO', '_name': 'Tom'}

>>> for k, v in Manager.__dict__.items():  # 派生类名字空间里没有任何基类成员。
... print "{0:5} = {1}".format(k, v)

table = t_manager
kill = <function kill at 0x10c9032a8>

>>> for k, v in User.__dict__.items():
... print "{0:5} = {1}".format(k, v)

table = t_user
test = <function test at 0x10c903140>

如果派生类不提供初始化方法,则默认会查找并使用基类的方法。

基类引用存储在 base,直接派生类存储在 subclasses

>>> Manager.__base__
<class '__main__.User'>
>>> User.__subclasses__()
[<class '__main__.Manager'>]

可以用 issubclass() 判断是否继承自某个类型,或用 isinstance() 判断实例对象的基类。

>>> issubclass(Manager, User)
True

>>> issubclass(Manager, object)  # 可以是任何层级的基类。
True

>>> isinstance(m, Manager)
True

>>> isinstance(m, object)
True

成员查找规则允许我们用实例引用基类所有成员,包括实例方法、静态方法、静态字段。 但这里有个坑:如果派生类有一个与基类实例方法同名的静态成员,那么首先被找到的是该静态成员,而不是基类的实例方法了。因为派生类的名字空间优先于基类。

>>> class User(object):
...     def abc(self):
...         print "User.abc"

>>> class Manager(User):
...     @staticmethod
...     def abc():
...         print "Manager.static.abc"
...
...     def test(self):
...         self.abc()   # 按照查找顺序,首先找到的是 static abc()。
...         User.abc(self)  # 只好显式调用基类方法。

>>> Manager().test()
Manager.static.abc
User.abc

同样因为优先级的缘故,只需在派生类创建一个同名实例方法,就可实现 "覆盖 (override)",签名可完全不同。

>>> class User(object):
...     def test(self):
...         print "User.test"

>>> class Manager(User):
...     def test(self, s):   # 依然是因为派生类名字空间优先于基类。
...         print "Manager.test:", s
...         User.test(self)   # 显式调用基类方法。

>>> Manager().test("hi")
Manager.test: hi
User.test

多重继承

Python 诞生的时候,单继承还不是主流思想。至于多重继承好不好,估计要打很久的口水仗。

>>> class A(object):
...     def __init__(self, a):
...         self._a = a

>>> class B(object):
...     def __init__(self, b):
...         self._b = b

>>> class C(A, B):     # 多重继承。基类顺序影响成员搜索顺序。
...     def __init__(self, a, b):
...         A.__init__(self, a)   # 依次调用所有基类初始化方法。
...         B.__init__(self, b)

>>> C.__bases__

(<class '__main__.A'>, <class '__main__.B'>)

>>> c = C(1, 2)

>>> c.__dict__     # 包含所有基类实例字段。
{'_b': 2, '_a': 1}

>>> issubclass(C, A), isinstance(c, A)
(True, True)

>>> issubclass(C, B), isinstance(c, B)
(True, True)

多重继承成员搜索顺序,也就是 mro (method resolution order) 要稍微复杂一点。归纳一下就是:从下到上 (深度优先,从派生类到基类),从左到右 (基类声明顺序)。mro 和我们前面提及的成员查找规则是有区别的,mro 列表中并没有 instance。所以在表述时,需要注意区别。

>>> C.mro()
[<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <type 'object'>]

>>> C.__mro__
(<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <type 'object'>)

super

super() 起到其他语言 base 关键字的作用,它依照 mro 顺序搜索基类成员。

>>> class A(object):
...     def a(self): print "a"

>>> class B(object):
...     def b(self): print "b"

>>> class C(A, B):
...     def test(self):
...         base = super(C, self) # 可以考虑放在 __init__。
...         base.a()   # A.a(self)
...         base.b()   # B.b(self)

>>> C().test()
a
b

super 的类型参数决定了在 mro 列表中的搜索起始位置,总是返回该参数后续类型的成员。单继承时总是搜索该参数的基类型。

>>> class A(object):
...     def test(self): print "a"

>>> class B(A):
...     def test(self): print "b"

>>> class C(B):
...     def __init__(self):
...         super(C, self).test() # 从 mro 中 C 的后续类型,也就是 B 开始查找。
...         super(B, self).test() # 从 B 的后续类型 A 开始查找。

>>> C.__mro__
[<class '__main__.C'>, <class '__main__.B'>, <class '__main__.A'>, <type 'object'>]

>>> C()
b
a
<__main__.C object at 0x101498f90>

不建议用 self.class 代替当前类型名,因为这可能会引发混乱。

>>> class A(object):
...     def test(self):
...     print "a"

>>> class B(A):
...     def test(self):     # 以 c instance 调用,那么
...         super(self.__class__, self).test() # self.__class__ 就是 C 类型对象。
...         print "b"     # super(C, self) 总是查找其基类 B。
        # 于是死循环发生了。

>>> class C(B):
...     pass

>>> C().test()
RuntimeError: maximum recursion depth exceeded while calling a Python object

在多重继承初始化方法中使用 super 可能会引发一些奇怪的状况。

>>> class A(object):
...     def __init__(self):
...         print "A"
...         super(A, self).__init__()  # 找到的是 B.__init__

>>> class B(object):
...     def __init__(self):
...         print "B"
...         super(B, self).__init__()  # object.__init__

>>> class C(A, B):
...     def __init__(self):
...         A.__init__(self)
...         B.__init__(self)

>>> o = C()    # 对输出结果很意外?
A     # super 按照 mro 列表顺序查找后续类型。
B     # 那么在 A.__init__ 中的 super(A, self) 实际返回 B,
B     # super(A, self).__init__() 实际是 B.__init__()。

>>> C.__mro__
(<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <type 'object'>)

多重继承将很多问题复杂化,建议改用组合模式实现类似的功能。

bases

类型对象有两个相似的成员:

  • base: 只读,总是返回 bases[0]。
  • bases: 基类列表,可直接修改来更换基类,影响 mro 顺序。
>>> class A(object): pass
>>> class B(object): pass
>>> class C(B): pass

>>> C.__bases__   # 直接基类型元组
(<class '__main__.B'>,)

>>> C.__base__   # __bases__[0]
<class '__main__.B'>

>>> C.__mro__   # mro
(<class '__main__.C'>, <class '__main__.B'>, <type 'object'>)

>>> C.__bases__ = (A,)  # 通过 __bases__ 修改基类

>>> C.__base__   # __base__ 变化
<class '__main__.A'>

>>> C.__mro__   # mro 变化
(<class '__main__.C'>, <class '__main__.A'>, <type 'object'>)

对多继承一样有效,比如调整基类顺序。

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

>>> C.__bases__
(<class '__main__.A'>, <class '__main__.B'>)

>>> C.__base__
<class '__main__.A'>

>>> C.__mro__
(<class '__main__.C'>, <class '__main__.A'>, <class '__main__.B'>, <type 'object'>)

>>> C.__bases__ = (B, A)  # 交换基类型顺序

>>> C.__base__   # __base__ 总是返回 __bases__[0]
<class '__main__.B'>

>>> C.__mro__   # mro 顺序也发生变化
(<class '__main__.C'>, <class '__main__.B'>, <class '__main__.A'>, <type 'object'>)

通过更换基类,我们可实现代码注入 (Code Inject),影响既有类型的行为。事实上,我们还可以更改实例的类型。

>>> class A(object): pass
>>> class B(object): pass
>>> a = A()
>>> a.__class__ = B
>>> type(a)
__main__.B

抽象类

抽象类 (Abstract Class) 无法实例化,且派生类必须 "完整" 实现所有抽象成员才可创建实例。

>>> from abc import ABCMeta, abstractmethod, abstractproperty

>>> class User(object):
...     __metaclass__ = ABCMeta   # 通过元类来控制抽象类行为。
...
...     def __init__(self, uid):
...         self._uid = uid
...
...     @abstractmethod
...     def print_id(self): pass  # 抽象方法
...
...     name = abstractproperty()  # 抽象属性

>>> class Manager(User):
...     def __init__(self, uid):
...         User.__init__(self, uid)
...
...     def print_id(self): 
...         print self._uid, self._name
...
...     name = property(lambda s: s._name, lambda s, v: setattr(s, "_name", v))

>>> u = User(1)     # 抽象类无法实例化。
TypeError: Can't instantiate abstract class User with abstract methods name, print_id

>>> m = Manager(1)
>>> m.name = "Tom"
>>> m.print_id()
1 Tom

如果派生类也是抽象类型,那么可以部分实现或完全不实现基类抽象成员。

>>> class Manager(User):
...     __metaclass__ = ABCMeta
...
...     def __init__(self, uid, name):
...         User.__init__(self, uid)
...         self.name = name
...
...     uid = property(lambda o: o._uid)
...     name = property(lambda o: o._name, lambda o, v: setattr(o, "_name", v))
...     title = abstractproperty()

>>> class CXO(Manager):
...     def __init__(self, uid, name):
...         Manager.__init__(self, uid, name)
...
...     def print_id(self):
...         print self.uid, self.name, self.title
...
...     title = property(lambda s: "CXO")

>>> c = CXO(1, "Tom")
>>> c.print_id()
1 Tom CXO

派生类 Manager 也是抽象类,它实现了部分基类的抽象成员,又增加了新的抽象成员。这种做法在面向对象模式里很常见,只须保证整个继承体系走下来,所有层次的抽象成员都被实现即可。

开放类

Open Class 几乎是所有动态语言的标配,也是精华所在。即便是运行期,我们也可以随意改动对象,增加或删除成员。

增加成员时,要明确知道放到哪儿,比如将实例方法放到 instance.dict 是没效果的。

>>> class User(object): pass

>>> def print_id(self): print hex(id(self))

>>> u = User()

>>> u.print_id = print_id   # 添加到 instance.__dict__

>>> u.__dict__
{'print_id': <function print_id at 0x10c88e320>}

>>> u.print_id()     # 失败,不是 bound method。
TypeError: print_id() takes exactly 1 argument (0 given)

>>> u.print_id(u)     # 仅当做一个普通函数字段来用。
0x10c91c0d0

因为不是 bound method,所以必须显式传递对象引用。正确的做法是放到 class.dict

>>> User.__dict__["print_id"] = print_id # dictproxy 显然是只读的。
TypeError: 'dictproxy' object does not support item assignment

>>> User.print_id = print_id   # 同 setattr(User, "print_id", print_id)

>>> User.__dict__["print_id"]
<function print_id at 0x10c88e320>

>>> u = User()

>>> u.print_id     # 总算是 bound method 了。
<bound method User.print_id of <__main__.User object at 0x10c91c090>>

>>> u.print_id()     # 测试通过。
0x10c91c090

静态方法必须用装饰器 staticmethod、classmethod 包装一下,否则会被当做实例方法。

>>> def mstatic(): print "static method"

>>> User.mstatic = staticmethod(mstatic) # 使用装饰器包装。

>>> User.mstatic     # 正常的静态方法。
<function mstatic at 0x10c88e398>

>>> User.mstatic()     # 调用正常。
static method

>>> def cstatic(cls):     # 注意 classmethod 和 staticmethod 的区别。
...     print "class method:", cls

>>> User.cstatic = classmethod(cstatic)

>>> User.cstatic     # classmethod 绑定到类型对象。
<bound method type.cstatic of <class '__main__.User'>>

>>> User.cstatic()     # 调用成功。
class method: <class '__main__.User'>

在运行期调整对象成员,时常要用到几个以字符串为参数的内置函数。其中 hasattr、getattr 依照成员查找规则搜索对象成员,而 setattr、delattr 则直接操作实例和类型的名字空间。

>>> class User(object):pass
>>> u = User()

>>> setattr(u, "name", "tom")   # u.name = "tom"

>>> u.__dict__
{'name': 'tom'}
>>> setattr(User, "table", "t_user")  # User.table = "t_user"

>>> User.table
't_user'

>>> u.table
't_user'

>>> hasattr(u, "table")    # mro: User.__dict__["table"]
True

>>> getattr(u, "table", None)
't_user'

>>> delattr(u, "table")    # Error: "table" not in u.__dict__
AttributeError: table

>>> delattr(User, "table")

>>> delattr(u, "name")    # del u.__dict__["name"]
>>> u.__dict__
{}

slots

slots 属性会阻止虚拟机创建实例 dict,仅为名单中的指定成员分配内存空间。这有助于减少内存占用,提升执行性能,尤其是在需要大量此类对象的时候。

>>> class User(object):
...     __slots__ = ("_name", "_age")
...
...     def __init__(self, name, age):
...         self._name = name
...         self._age = age

>>> u = User("Tom", 34)

>>> hasattr(u, "__dict__")
False

>>> u.title = "CXO"    # 动态增加字段失败。
AttributeError: 'User' object has no attribute 'title'

>>> del u._age     # 已有字段可被删除。
>>> u._age = 18     # 将坑补回是允许的。
>>> u._age
18

>>> del u._age     # 该谁的就是谁的,换个主是不行滴。
>>> u._title = "CXO"
AttributeError: 'User' object has no attribute '_title'

>>> vars(u)      # 因为没有 __dict__,vars 失败。
TypeError: vars() argument must have __dict__ attribute

虽然没有了 dict,但依然可以用 dir() 和 inspect.getmembers() 获取实例成员信息。

>>> import inspect  

>>> u = User("Tom", 34)

>>> {k:getattr(u, k) for k in dir(u) if not k.startswith("__")}
{'_age': 34, '_name': 'Tom'}

>>> {k:v for k, v in inspect.getmembers(u) if not k.startswith("__")}
{'_age': 34, '_name': 'Tom'}

其派生类同样必须用 slots 为新增字段分配存储空间 (即便是空 slots = []),否则依然会创建 dict,反而导致更慢的执行效率。

>>> class Manager(User):
...     __slots__ = ("_title")
...
...     def __init__(self, name, age, title):
...         User.__init__(self, name, age)
...         self._title = title

如果需要创建 "海量" 对象实例,优先考虑 slots 将节约大量内存。

操作符重载

setitem

又称索引器,像序列或字典类型那样操作对象。

>>> class A(object):
...     def __init__(self, **kwargs):
...         self._data = kwargs
...
...     def __getitem__(self, key):
...         return self._data.get(key)
...
...     def __setitem__(self, key, value):
...         self._data[key] = value
...
...     def __delitem__(self, key):
...         self._data.pop(key, None)
...
...     def __contains__(self, key):
...         return key in self._data.keys()

>>> a = A(x = 1, y = 2)

>>> a["x"]
1

>>> a["z"] = 3
>>> "z" in a
True

>>> del a["y"]

>>> a._data
{'x': 1, 'z': 3}

call

像函数那样调用对象,也就是传说中的 callable。

>>> class A(object):
...     def __call__(self, *args, **kwargs):
...         print hex(id(self)), args, kwargs

>>> a = A()

>>> a(1, 2, s = "hi")   # 完全可以把对象实例伪装成函数接口。
0x10c8957d0 (1, 2) {'s': 'hi'}

dir

配合 slots 隐藏内部成员。

>>> class A(object):
...     __slots__ = ("x", "y")
...
...     def __init__(self, x, y):
...         self.x = x
...         self.y = y
...
...     def __dir__(self):  # 必须返回 list,而不是 tuple。
...         return ["x"]

>>> a = A(1, 2)

>>> dir(a)     # y 不见了。
['x']

getattr

先看看这几个方法的触发时机。

  • getattr: 访问不存在的成员。
  • setattr: 对任何成员的赋值操作。
  • delattr: 删除成员操作。
  • getattribute: 访问任何存在或不存在的成员,包括 dict

不要在这几个方法里直接访问对象成员,也不要用 hasattr/getattr/setattr/delattr 函数,因为它们会被再次拦截,形成无限循环。正确的做法是直接操作 dict

getattributedict 都会拦截,只能用基类的 getattribute 返回结果。

>>> class A(object):
...     def __init__(self, x):
...         self.x = x    # 会被 __setattr__ 捕获。
...
...     def __getattr__(self, name):
...         print "get:", name
...         return self.__dict__.get(name)
...
...     def __setattr__(self, name, value):
...         print "set:", name, value
...         self.__dict__[name] = value
...
...     def __delattr__(self, name):
...         print "del:", name
...         self.__dict__.pop(name, None)
...
...     def __getattribute__(self, name):
...         print "attribute:", name
...         return object.__getattribute__(self, name)

>>> a = A(10)   # __init__ 里面的 self.x = x 被 __setattr__ 捕获。
set: x 10
attribute: __dict__

>>> a.x    # 访问已存在字段,仅被 __getattribute__ 捕获。
attribute: x
10

>>> a.y = 20   # 创建新的字段,被 __setattr__ 捕获。
set: y 20
attribute: __dict__

>>> a.z    # 访问不存在的字段,被 __getattr__ 捕获。
attribute: z
get: z
attribute: __dict__

>>> del a.y    # 删除字段被 __delattr__ 捕获。
del: y
attribute: __dict__

cmp

cmp 通过返回数字来判断大小,而 eq 仅用于相等判断。

>>> class A(object):
...     def __init__(self, x):
...         self.x = x
...
...     def __eq__(self, o):
...         if not o or not isinstance(o, A): return False
...         return o.x == self.x
...
...     def __cmp__(self, o):
...         if not o or not isinstance(o, A): raise Exception()
...         return cmp(self.x, o.x)

>>> A(1) == A(1)
True
>>> A(1) == A(2)
False
>>> A(1) < A(2)
True
>>> A(1) <= A(2)
True

面向对象理论很复杂,涉及到的内容十分繁复,应该找本经典的大部头好好啃啃。

上一篇: 模块 下一篇: 异常