如果说Python最迷人的特性是什么,我的回答一定是“一致性”。什么是“一致性”?举个例子,Python中“万物皆对象”,没有Java那种“原生类型”和“封装类型”所造成的割裂感。这种一致性体现在了Python的方方面面,是Python的设计哲学。个人认为,能保持这种一致性的代码,就能称为“Pythonic”。那么怎样才能写出这样的代码呢?一个方式就是采用“魔术方法”(magic method)。
所谓魔术方法,表现形式上,就是以双下划线开头和结尾的方法,最常见的就是__init__()
方法。魔术方法是供解释器调用的,我们一般不应该直接使用魔术方法,也不应该自创一个非标准的魔术方法;也就是说,我们写出的代码中,不应该有类似于ClassA.__foo__()
的使用方式。
下面我们来看一下有哪些常见的魔术方法,以及如何使用这些方法,使代码更加 “Pythonic”
1. __len__
求一个对象的长度,Python提供了一个内建的方法:len()
。无论是字符串对象,还是list 对象,都可以用len(obj)
来获取长度(作为对比,Java中求数组的长度用lengh属性,求String对象的长度则为length()方法,显得很“割裂”)。其实在使用len()
时,解释器会调用对象中的__len()__
方法。所以为保持一致性,最好也能用len()
求自定义对象的长度,我们可以在类的内部,实现__len()__
方法。
Class Train:
def __init__(self):
self.length = 50
def __len__(self):
return self.length
我们定义了一个 Trian 类,并实现了__len()__
方法,那么就可以用len()
来求对象的长度:
>>> train = Train()
>>> print(len(train))
>>)> 50
这样的结果是,对于求长度这个操作,自定义的类的行为和内置类型(str, list等)完全一样,使用者不用了解是用train.get_size()
还是 train.length
或者其他什么方法、属性,只需调用len()
这个通用的方法。当然,如果你非要实现类似于 get_size() 的方法,也可以获取对象的长度,但远没有使用len()
简洁优雅。这就是上文所说的”一致性“。
2. __getitem__ 与 __setitem__
对字典对象的取值或修改操作,都会用到[]
符号,这背后实际上就是__getitem__
和__setitem__
方法,所以我们可以自行实现这两个方法,以获得和字典对象类似的行为。
class Box:
def __setitem__(self, key, value):
setattr(self, key, value)
def __getitem__(self, key):
try:
return getattr(self, key)
except AttributeError:
raise KeyError(key)
>>> b=Box()
>>> b["name"]="box"
>>> b["name"]
'box'
例子中,Box
对象实现了__getitem__
和__setitem__
方法,用中括号操作符时,表现行为和内置的字典类型一致。
3. __enter__ 与 __exit__
这一对魔术方法用于实现上下文管理器。比如我们打开一个文件时,为了防止忘记关闭,往往用上下文管理器的方式:
with open("test.txt") as f:
pass
对于自己创建的对象,可以实现__enter__
与__exit__
方法,其中在with语句开始时,__enter__
方法被调用,退出with语句块时,__exit__
方法被调用。我们来看一个实例,来自Python的requests
库源码:
# requests.sessions
class Session(SessionRedirectMixin):
...
def __enter__(self):
return self
def __exit__(self,*args):
self.close()
...
Session
类的__enter__
方法返回自身实例,__exit__
方法调用关闭方法,确保退出时能正确关闭会话,所以用with
语法调用时,无需关注会话关闭的问题。使用方法同open
函数:
>>> with Session() as s:
>>> s.get('https://httpbin.org/get')
4. __call__
在Python中,只要是可调用对象,就能像函数一样调用该对象,其行为跟普通函数没有区别,其实函数本身就是一个可调用对象。如果自己想构建一个可调用对象,实现__call__
方法即可。我们看一下来自 Werkzeug 库的 Response 对象:
class Response(object):
...
def __call__(self, environ, start_response):
app_iter, status, headers = self.get_wsgi_response(environ)
start_response(status, headers)
return app_iter
该类实现了__call__
方法,当调用该类的对象时,就像调用一个普通函数,传入__call__
方法定义的参数:
>>> response = Response()
# 像函数一样调用 response:
>>> response(environ,start_response)
5. __iter__ 与 __next__
这一对魔术方法与迭代器和可迭代对象密切相关。简单来说,实现了__iter__
方法的即是可迭代对象,实现了__next__
方法的即为迭代器。我们可以利用 for 循环来遍历可迭代对象,在实际使用中更加方便:
>>> for i in MyIterable():
... print(i)
...
关于迭代器与可迭代对象,可以参考我的另一篇文章Python中的迭代器与可迭代对象,这里不再赘述。
6. 小结
我们在编写代码的时候,应该考虑到语言本身的特性,尽量贴近这种特性。对于Python,我们应该多利用魔术方法,写出更加优雅、一致的代码,提供符合Python习惯的接口。魔术方法还有很多,比如跟运算符相关的就有一大串,本文仅仅是列出了常见的几个,仅供参考。