类
由于历史原因,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.
>>> 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。
而 getattribute 连 dict 都会拦截,只能用基类的 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
面向对象理论很复杂,涉及到的内容十分繁复,应该找本经典的大部头好好啃啃。