Skip to content

Commit 20ae22b

Browse files
committed
add magic method
1 parent ef4590b commit 20ae22b

1 file changed

Lines changed: 334 additions & 0 deletions

File tree

Class/magic_method.md

Lines changed: 334 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,334 @@
1+
# 定制类和魔法方法
2+
3+
在 Python 中,我们可以经常看到以双下划线 `__` 包裹起来的方法,比如最常见的 `__init__`,这些方法被称为**魔法方法(magic method)或特殊方法(special method)**。简单地说,这些方法可以给 Python 的类提供特殊功能,方便我们定制一个类,比如 `__init__` 方法可以对类属性进行初始化。
4+
5+
完整的特殊方法列表可在[这里](https://docs.python.org/2/reference/datamodel.html#special-method-names)查看,本文只介绍以下一些常用的特殊方法:
6+
7+
- `__str__` , `__repr__`
8+
- `__iter__`
9+
- `__getitem__` , `__setitem__` , `__delitem__`
10+
- `__getattr__` , `__setattr__` , `__delattr__`
11+
- `__call__`
12+
13+
# str & repr
14+
15+
先看一个简单的例子:
16+
17+
```python
18+
class Foo(object):
19+
def __init__(self, name):
20+
self.name = name
21+
22+
>>> print Foo('ethan')
23+
<__main__.Foo object at 0x10c37aa50>
24+
```
25+
26+
在上面,我们使用 print 打印一个实例对象,但如果我们想打印更多信息呢,比如把 name 也打印出来,这时,我们可以在类中加入 `__str__` 方法,如下:
27+
28+
```python
29+
class Foo(object):
30+
def __init__(self, name):
31+
self.name = name
32+
def __str__(self):
33+
return 'Foo object (name: %s)' % self.name
34+
35+
>>> print Foo('ethan') # 使用 print
36+
Foo object (name: ethan)
37+
>>>
38+
>>> str(Foo('ethan')) # 使用 str
39+
'Foo object (name: ethan)'
40+
>>>
41+
>>> Foo('ethan') # 直接显示
42+
<__main__.Foo at 0x10c37a490>
43+
```
44+
45+
可以看到,使用 print 和 str 输出的是 `__str__` 方法返回的内容,但如果直接显示则不是,那能不能修改它的输出呢?当然可以,我们只需在类中加入 `__repr__` 方法,比如:
46+
47+
```python
48+
class Foo(object):
49+
def __init__(self, name):
50+
self.name = name
51+
def __str__(self):
52+
return 'Foo object (name: %s)' % self.name
53+
def __repr__(self):
54+
return 'Foo object (name: %s)' % self.name
55+
56+
>>> Foo('ethan')
57+
'Foo object (name: ethan)'
58+
```
59+
60+
可以看到,现在直接使用 `Foo('ethan')` 也可以显示我们想要的结果了,然而,我们发现上面的代码中,`__str__``__repr__` 方法的代码是一样的,能不能精简一点呢,当然可以,如下:
61+
62+
```python
63+
class Foo(object):
64+
def __init__(self, name):
65+
self.name = name
66+
def __str__(self):
67+
return 'Foo object (name: %s)' % self.name
68+
__repr__ = __str__
69+
```
70+
71+
# iter
72+
73+
在某些情况下,我们希望实例对象可被用于 `for...in` 循环,这时我们需要在类中定义 `__iter__``next`(在 Python3 中是 `__next__`)方法,其中,`__iter__` 返回一个迭代对象,`next` 返回容器的下一个元素,在没有后续元素时抛出 `StopIteration` 异常。
74+
75+
看一个斐波那契数列的例子:
76+
77+
```python
78+
class Fib(object):
79+
def __init__(self):
80+
self.a, self.b = 0, 1
81+
82+
def __iter__(self): # 返回迭代器对象本身
83+
return self
84+
85+
def next(self): # 返回容器下一个元素
86+
self.a, self.b = self.b, self.a + self.b
87+
return self.a
88+
89+
>>> fib = Fib()
90+
>>> for i in fib:
91+
... if i > 10:
92+
... break
93+
... print i
94+
...
95+
1
96+
1
97+
2
98+
3
99+
5
100+
8
101+
```
102+
103+
# getitem
104+
105+
有时,我们希望可以使用 `obj[n]` 这种方式对实例对象进行取值,比如对斐波那契数列,我们希望可以取出其中的某一项,这时我们需要在类中实现 `__getitem__` 方法,比如下面的例子:
106+
107+
```python
108+
class Fib(object):
109+
def __getitem__(self, n):
110+
a, b = 1, 1
111+
for x in xrange(n):
112+
a, b = b, a + b
113+
return a
114+
115+
>>> fib = Fib()
116+
>>> fib[0], fib[1], fib[2], fib[3], fib[4], fib[5]
117+
(1, 1, 2, 3, 5, 8)
118+
```
119+
120+
我们还想更进一步,希望支持 `obj[1:3]` 这种切片方法来取值,这时 `__getitem__` 方法传入的参数可能是一个整数,也可能是一个切片对象 slice,因此,我们需要对传入的参数进行判断,可以使用 `isinstance` 进行判断,改后的代码如下:
121+
122+
```python
123+
class Fib(object):
124+
def __getitem__(self, n):
125+
if isinstance(n, slice): # 如果 n 是 slice 对象
126+
a, b = 1, 1
127+
start, stop = n.start, n.stop
128+
L = []
129+
for i in xrange(stop):
130+
if i >= start:
131+
L.append(a)
132+
a, b = b, a + b
133+
return L
134+
if isinstance(n, int): # 如果 n 是 int 型
135+
a, b = 1, 1
136+
for i in xrange(n):
137+
a, b = b, a + b
138+
return a
139+
```
140+
141+
现在,我们试试用切片方法:
142+
143+
```python
144+
>>> fib = Fib()
145+
>>> fib[0:3]
146+
[1, 1, 2]
147+
>>> fib[2:6]
148+
[2, 3, 5, 8]
149+
```
150+
151+
上面,我们只是简单地演示了 getitem 的操作,但是它还很不完善,比如没有对负数处理,不支持带 step 参数的切片操作 `obj[1:2:5]` 等等,读者有兴趣的话可以自己实现看看。
152+
153+
`__geitem__` 用于获取值,类似地,`__setitem__` 用于设置值,`__delitem__` 用于删除值,让我们看下面一个例子:
154+
155+
```python
156+
class Point(object):
157+
def __init__(self):
158+
self.coordinate = {}
159+
160+
def __str__(self):
161+
return "point(%s)" % self.coordinate
162+
163+
def __getitem__(self, key):
164+
return self.coordinate.get(key)
165+
166+
def __setitem__(self, key, value):
167+
self.coordinate[key] = value
168+
169+
def __delitem__(self, key):
170+
del self.coordinate[key]
171+
print 'delete %s' % key
172+
173+
def __len__(self):
174+
return len(self.coordinate)
175+
176+
__repr__ = __str__
177+
```
178+
179+
在上面,我们定义了一个 Point 类,它有一个属性 coordinate(坐标),是一个字典,让我们看看使用:
180+
181+
```python
182+
>>> p = Point()
183+
>>> p['x'] = 2 # 对应于 p.__setitem__('x', 2)
184+
>>> p['y'] = 5 # 对应于 p.__setitem__('y', 5)
185+
>>> p # 对应于 __repr__
186+
point({'y': 5, 'x': 2})
187+
>>> len(p) # 对应于 p.__len__
188+
2
189+
>>> p['x'] # 对应于 p.__getitem__('x')
190+
2
191+
>>> p['y'] # 对应于 p.__getitem__('y')
192+
5
193+
>>> del p['x'] # 对应于 p.__delitem__('x')
194+
delete x
195+
>>> p
196+
point({'y': 5})
197+
>>> len(p)
198+
1
199+
```
200+
201+
# getattr
202+
203+
当我们获取对象的某个属性,如果该属性不存在,会抛出 AttributeError 异常,比如:
204+
205+
```python
206+
class Point(object):
207+
def __init__(self, x=0, y=0):
208+
self.x = x
209+
self.y = y
210+
211+
>>> p = Point(3, 4)
212+
>>> p.x, p.y
213+
(3, 4)
214+
>>> p.z
215+
---------------------------------------------------------------------------
216+
AttributeError Traceback (most recent call last)
217+
<ipython-input-547-6dce4e43e15c> in <module>()
218+
----> 1 p.z
219+
220+
AttributeError: 'Point' object has no attribute 'z'
221+
```
222+
223+
那有没有办法不让它抛出异常呢?当然有,只需在类的定义中加入 `__getattr__` 方法,比如:
224+
225+
```python
226+
class Point(object):
227+
def __init__(self, x=0, y=0):
228+
self.x = x
229+
self.y = y
230+
def __getattr__(self, attr):
231+
if attr == 'z':
232+
return 0
233+
234+
>>> p = Point(3, 4)
235+
>>> p.z
236+
0
237+
```
238+
239+
现在,当我们调用不存在的属性(比如 z)时,解释器就会试图调用 `__getattr__(self, 'z')` 来获取值,但是,上面的实现还有一个问题,当我们调用其他属性,比如 w ,会返回 None,因为 `__getattr__` 默认返回就是 None,只有当 attr 等于 'z' 时才返回 0,如果我们想让 `__getattr__` 只响应几个特定的属性,可以加入异常处理,修改 `__getattr__` 方法,如下:
240+
241+
```python
242+
def __getattr__(self, attr):
243+
if attr == 'z':
244+
return 0
245+
raise AttributeError("Point object has no attribute %s" % attr)
246+
```
247+
248+
这里再强调一点,`__getattr__` 只有在属性不存在的情况下才会被调用,对已存在的属性不会调用 `__getattr__`
249+
250+
`__getattr__` 一起使用的还有 `__setattr__`, `__delattr__`,类似 `obj.attr = value`, `del obj.attr`,看下面一个例子:
251+
252+
```python
253+
class Point(object):
254+
def __init__(self, x=0, y=0):
255+
self.x = x
256+
self.y = y
257+
258+
def __getattr__(self, attr):
259+
if attr == 'z':
260+
return 0
261+
raise AttributeError("Point object has no attribute %s" % attr)
262+
263+
def __setattr__(self, *args, **kwargs):
264+
print 'call func set attr (%s, %s)' % (args, kwargs)
265+
return object.__setattr__(self, *args, **kwargs)
266+
267+
def __delattr__(self, *args, **kwargs):
268+
print 'call func del attr (%s, %s)' % (args, kwargs)
269+
return object.__delattr__(self, *args, **kwargs)
270+
271+
>>> p = Point(3, 4)
272+
call func set attr (('x', 3), {})
273+
call func set attr (('y', 4), {})
274+
>>> p.z
275+
0
276+
>>> p.z = 7
277+
call func set attr (('z', 7), {})
278+
>>> p.z
279+
7
280+
>>> p.w
281+
Traceback (most recent call last):
282+
File "<stdin>", line 1, in <module>
283+
File "<stdin>", line 8, in __getattr__
284+
AttributeError: Point object has no attribute w
285+
>>> p.w = 8
286+
call func set attr (('w', 8), {})
287+
>>> p.w
288+
8
289+
>>> del p.w
290+
call func del attr (('w',), {})
291+
>>> p.__dict__
292+
{'y': 4, 'x': 3, 'z': 7}
293+
```
294+
295+
# call
296+
297+
我们一般使用 `obj.method()` 来调用对象的方法,那能不能**直接在实例本身上调用**呢?在 Python 中,只要我们在类中定义 `__call__` 方法,就可以对实例进行调用,比如下面的例子:
298+
299+
```python
300+
class Point(object):
301+
def __init__(self, x, y):
302+
self.x, self.y = x, y
303+
def __call__(self, z):
304+
return self.x + self.y + z
305+
```
306+
307+
使用如下:
308+
309+
```python
310+
>>> p = Point(3, 4)
311+
>>> callable(p) # 使用 callable 判断对象是否能被调用
312+
True
313+
>>> p(6) # 传入参数,对实例进行调用,对应 p.__call__(6)
314+
13 # 3+4+6
315+
```
316+
317+
可以看到,对实例进行调用就好像对函数调用一样。
318+
319+
# 小结
320+
321+
- `__str__` 是用 print 和 str 显示的结果,`__repr__` 是直接显示的结果。
322+
- `__getitem__` 用类似 `obj[key]` 的方式对对象进行取值
323+
- `__getattr__` 用于获取不存在的属性 obj.attr
324+
- `__call__` 使得可以对实例进行调用
325+
326+
327+
# 参考资料
328+
329+
- [定制类](http://www.liaoxuefeng.com/wiki/001374738125095c955c1e6d8bb493182103fac9270762a000/0013946328809098c1be08a2c7e4319bd60269f62be04fa000)
330+
- [__setitem__ implementation in Python for Point(x,y) class - Stack Overflow](http://stackoverflow.com/questions/15774804/setitem-implementation-in-python-for-pointx-y-class)
331+
- [Python对象的特殊属性和方法 | Hom](http://gohom.win/2015/10/09/pySpecialObjMethod/)
332+
- [介绍Python的魔术方法 - Magic Method - 旺酱的专栏 - SegmentFault](https://segmentfault.com/a/1190000007256392)
333+
334+

0 commit comments

Comments
 (0)