定制类和魔法方法

在 Python 中,我们可以经常看到以双下划线 __ 包裹起来的方法,比如最常见的 __init__,这些方法被称为魔法方法(magic method)或特殊方法(special method)。简单地说,这些方法可以给 Python 的类提供特殊功能,方便我们定制一个类,比如 __init__ 方法可以对实例属性进行初始化。

完整的特殊方法列表可在这里查看,本文介绍部分常用的特殊方法:

  • __new__
  • __str__ , __repr__
  • __iter__
  • __getitem__ , __setitem__ , __delitem__
  • __getattr__ , __setattr__ , __delattr__
  • __call__

new

在 Python 中,当我们创建一个类的实例时,类会先调用 __new__(cls[, ...]) 来创建实例,然后 __init__ 方法再对该实例(self)进行初始化。

关于 __new____init__ 有几点需要注意:

  • __new__ 是在 __init__ 之前被调用的;
  • __new__ 是类方法,__init__ 是实例方法;
  • 重载 __new__ 方法,需要返回类的实例;

一般情况下,我们不需要重载 __new__ 方法。但在某些情况下,我们想控制实例的创建过程,这时可以通过重载 __new_ 方法来实现。

让我们看一个例子:

  1. class A(object):
  2. _dict = dict()
  3. def __new__(cls):
  4. if 'key' in A._dict:
  5. print "EXISTS"
  6. return A._dict['key']
  7. else:
  8. print "NEW"
  9. return object.__new__(cls)
  10. def __init__(self):
  11. print "INIT"
  12. A._dict['key'] = self

在上面,我们定义了一个类 A,并重载了 __new__ 方法:当 keyA._dict 中时,直接返回 A._dict['key'],否则创建实例。

执行情况:

  1. >>> a1 = A()
  2. NEW
  3. INIT
  4. >>> a2 = A()
  5. EXISTS
  6. INIT
  7. >>> a3 = A()
  8. EXISTS
  9. INIT

str & repr

先看一个简单的例子:

  1. class Foo(object):
  2. def __init__(self, name):
  3. self.name = name
  4. >>> print Foo('ethan')
  5. <__main__.Foo object at 0x10c37aa50>

在上面,我们使用 print 打印一个实例对象,但如果我们想打印更多信息呢,比如把 name 也打印出来,这时,我们可以在类中加入 __str__ 方法,如下:

  1. class Foo(object):
  2. def __init__(self, name):
  3. self.name = name
  4. def __str__(self):
  5. return 'Foo object (name: %s)' % self.name
  6. >>> print Foo('ethan') # 使用 print
  7. Foo object (name: ethan)
  8. >>>
  9. >>> str(Foo('ethan')) # 使用 str
  10. 'Foo object (name: ethan)'
  11. >>>
  12. >>> Foo('ethan') # 直接显示
  13. <__main__.Foo at 0x10c37a490>

可以看到,使用 print 和 str 输出的是 __str__ 方法返回的内容,但如果直接显示则不是,那能不能修改它的输出呢?当然可以,我们只需在类中加入 __repr__ 方法,比如:

  1. class Foo(object):
  2. def __init__(self, name):
  3. self.name = name
  4. def __str__(self):
  5. return 'Foo object (name: %s)' % self.name
  6. def __repr__(self):
  7. return 'Foo object (name: %s)' % self.name
  8. >>> Foo('ethan')
  9. 'Foo object (name: ethan)'

可以看到,现在直接使用 Foo('ethan') 也可以显示我们想要的结果了,然而,我们发现上面的代码中,__str____repr__ 方法的代码是一样的,能不能精简一点呢,当然可以,如下:

  1. class Foo(object):
  2. def __init__(self, name):
  3. self.name = name
  4. def __str__(self):
  5. return 'Foo object (name: %s)' % self.name
  6. __repr__ = __str__

iter

在某些情况下,我们希望实例对象可被用于 for...in 循环,这时我们需要在类中定义 __iter__next(在 Python3 中是 __next__)方法,其中,__iter__ 返回一个迭代对象,next 返回容器的下一个元素,在没有后续元素时抛出 StopIteration 异常。

看一个斐波那契数列的例子:

  1. class Fib(object):
  2. def __init__(self):
  3. self.a, self.b = 0, 1
  4. def __iter__(self): # 返回迭代器对象本身
  5. return self
  6. def next(self): # 返回容器下一个元素
  7. self.a, self.b = self.b, self.a + self.b
  8. return self.a
  9. >>> fib = Fib()
  10. >>> for i in fib:
  11. ... if i > 10:
  12. ... break
  13. ... print i
  14. ...
  15. 1
  16. 1
  17. 2
  18. 3
  19. 5
  20. 8

getitem

有时,我们希望可以使用 obj[n] 这种方式对实例对象进行取值,比如对斐波那契数列,我们希望可以取出其中的某一项,这时我们需要在类中实现 __getitem__ 方法,比如下面的例子:

  1. class Fib(object):
  2. def __getitem__(self, n):
  3. a, b = 1, 1
  4. for x in xrange(n):
  5. a, b = b, a + b
  6. return a
  7. >>> fib = Fib()
  8. >>> fib[0], fib[1], fib[2], fib[3], fib[4], fib[5]
  9. (1, 1, 2, 3, 5, 8)

我们还想更进一步,希望支持 obj[1:3] 这种切片方法来取值,这时 __getitem__ 方法传入的参数可能是一个整数,也可能是一个切片对象 slice,因此,我们需要对传入的参数进行判断,可以使用 isinstance 进行判断,改后的代码如下:

  1. class Fib(object):
  2. def __getitem__(self, n):
  3. if isinstance(n, slice): # 如果 n 是 slice 对象
  4. a, b = 1, 1
  5. start, stop = n.start, n.stop
  6. L = []
  7. for i in xrange(stop):
  8. if i >= start:
  9. L.append(a)
  10. a, b = b, a + b
  11. return L
  12. if isinstance(n, int): # 如果 n 是 int 型
  13. a, b = 1, 1
  14. for i in xrange(n):
  15. a, b = b, a + b
  16. return a

现在,我们试试用切片方法:

  1. >>> fib = Fib()
  2. >>> fib[0:3]
  3. [1, 1, 2]
  4. >>> fib[2:6]
  5. [2, 3, 5, 8]

上面,我们只是简单地演示了 getitem 的操作,但是它还很不完善,比如没有对负数处理,不支持带 step 参数的切片操作 obj[1:2:5] 等等,读者有兴趣的话可以自己实现看看。

__geitem__ 用于获取值,类似地,__setitem__ 用于设置值,__delitem__ 用于删除值,让我们看下面一个例子:

  1. class Point(object):
  2. def __init__(self):
  3. self.coordinate = {}
  4. def __str__(self):
  5. return "point(%s)" % self.coordinate
  6. def __getitem__(self, key):
  7. return self.coordinate.get(key)
  8. def __setitem__(self, key, value):
  9. self.coordinate[key] = value
  10. def __delitem__(self, key):
  11. del self.coordinate[key]
  12. print 'delete %s' % key
  13. def __len__(self):
  14. return len(self.coordinate)
  15. __repr__ = __str__

在上面,我们定义了一个 Point 类,它有一个属性 coordinate(坐标),是一个字典,让我们看看使用:

  1. >>> p = Point()
  2. >>> p['x'] = 2 # 对应于 p.__setitem__('x', 2)
  3. >>> p['y'] = 5 # 对应于 p.__setitem__('y', 5)
  4. >>> p # 对应于 __repr__
  5. point({'y': 5, 'x': 2})
  6. >>> len(p) # 对应于 p.__len__
  7. 2
  8. >>> p['x'] # 对应于 p.__getitem__('x')
  9. 2
  10. >>> p['y'] # 对应于 p.__getitem__('y')
  11. 5
  12. >>> del p['x'] # 对应于 p.__delitem__('x')
  13. delete x
  14. >>> p
  15. point({'y': 5})
  16. >>> len(p)
  17. 1

getattr

当我们获取对象的某个属性,如果该属性不存在,会抛出 AttributeError 异常,比如:

  1. class Point(object):
  2. def __init__(self, x=0, y=0):
  3. self.x = x
  4. self.y = y
  5. >>> p = Point(3, 4)
  6. >>> p.x, p.y
  7. (3, 4)
  8. >>> p.z
  9. ---------------------------------------------------------------------------
  10. AttributeError Traceback (most recent call last)
  11. <ipython-input-547-6dce4e43e15c> in <module>()
  12. ----> 1 p.z
  13. AttributeError: 'Point' object has no attribute 'z'

那有没有办法不让它抛出异常呢?当然有,只需在类的定义中加入 __getattr__ 方法,比如:

  1. class Point(object):
  2. def __init__(self, x=0, y=0):
  3. self.x = x
  4. self.y = y
  5. def __getattr__(self, attr):
  6. if attr == 'z':
  7. return 0
  8. >>> p = Point(3, 4)
  9. >>> p.z
  10. 0

现在,当我们调用不存在的属性(比如 z)时,解释器就会试图调用 __getattr__(self, 'z') 来获取值,但是,上面的实现还有一个问题,当我们调用其他属性,比如 w ,会返回 None,因为 __getattr__ 默认返回就是 None,只有当 attr 等于 ‘z’ 时才返回 0,如果我们想让 __getattr__ 只响应几个特定的属性,可以加入异常处理,修改 __getattr__ 方法,如下:

  1. def __getattr__(self, attr):
  2. if attr == 'z':
  3. return 0
  4. raise AttributeError("Point object has no attribute %s" % attr)

这里再强调一点,__getattr__ 只有在属性不存在的情况下才会被调用,对已存在的属性不会调用 __getattr__

__getattr__ 一起使用的还有 __setattr__, __delattr__,类似 obj.attr = value, del obj.attr,看下面一个例子:

  1. class Point(object):
  2. def __init__(self, x=0, y=0):
  3. self.x = x
  4. self.y = y
  5. def __getattr__(self, attr):
  6. if attr == 'z':
  7. return 0
  8. raise AttributeError("Point object has no attribute %s" % attr)
  9. def __setattr__(self, *args, **kwargs):
  10. print 'call func set attr (%s, %s)' % (args, kwargs)
  11. return object.__setattr__(self, *args, **kwargs)
  12. def __delattr__(self, *args, **kwargs):
  13. print 'call func del attr (%s, %s)' % (args, kwargs)
  14. return object.__delattr__(self, *args, **kwargs)
  15. >>> p = Point(3, 4)
  16. call func set attr (('x', 3), {})
  17. call func set attr (('y', 4), {})
  18. >>> p.z
  19. 0
  20. >>> p.z = 7
  21. call func set attr (('z', 7), {})
  22. >>> p.z
  23. 7
  24. >>> p.w
  25. Traceback (most recent call last):
  26. File "<stdin>", line 1, in <module>
  27. File "<stdin>", line 8, in __getattr__
  28. AttributeError: Point object has no attribute w
  29. >>> p.w = 8
  30. call func set attr (('w', 8), {})
  31. >>> p.w
  32. 8
  33. >>> del p.w
  34. call func del attr (('w',), {})
  35. >>> p.__dict__
  36. {'y': 4, 'x': 3, 'z': 7}

call

我们一般使用 obj.method() 来调用对象的方法,那能不能直接在实例本身上调用呢?在 Python 中,只要我们在类中定义 __call__ 方法,就可以对实例进行调用,比如下面的例子:

  1. class Point(object):
  2. def __init__(self, x, y):
  3. self.x, self.y = x, y
  4. def __call__(self, z):
  5. return self.x + self.y + z

使用如下:

  1. >>> p = Point(3, 4)
  2. >>> callable(p) # 使用 callable 判断对象是否能被调用
  3. True
  4. >>> p(6) # 传入参数,对实例进行调用,对应 p.__call__(6)
  5. 13 # 3+4+6

可以看到,对实例进行调用就好像对函数调用一样。

小结

  • __new____init__ 之前被调用,用来创建实例。
  • __str__ 是用 print 和 str 显示的结果,__repr__ 是直接显示的结果。
  • __getitem__ 用类似 obj[key] 的方式对对象进行取值
  • __getattr__ 用于获取不存在的属性 obj.attr
  • __call__ 使得可以对实例进行调用

参考资料