有一天闲着无聊的时候,脑子里突然冒出一个Magic Method的有趣用法,可以用__getattr__
来实现Python版的method_missing
。
顺着这个脑洞想下去,我发现Python的Magic Method确实有很多妙用之处。故在此记下几种有趣(也可能有用的)Magic Method技巧,希望可以抛砖引玉,打开诸位读者的脑洞,想出更加奇妙的用法。
如果对Magic Method的了解仅仅停留在知道这个术语和若干个常用方法上(如__lt__
,__str__
,__len__
),可以阅读下这份教程,看看Magic Method可以用来做些什么。
Python method_missing
先从最初的脑洞开始吧。曾几何时,Ruby社区的人总是夸耀Ruby的强大的元编程能力,其中method_missing
更是不可或缺的特性。通过调用BaseObject
上的method_missing
,Ruby可以实现在调用不存在的属性时进行拦截,并动态生成对应的属性。
Ruby例子
# 来自于Ruby文档: http://ruby-doc.org/core-2.2.0/BasicObject.html#method-i-method_missing
class Roman
def roman_to_int(str)
# ...
end
def method_missing(methId)
str = methId.id2name
roman_to_int(str)
end
end
r = Roman.new
r.iv #=> 4
r.xxiii #=> 23
r.mm #=> 2000
method_missing
的应用是如此地广泛,以至于只要是成规模的Ruby库,多多少少都会用到它。像是ActiveRecord
就是靠这一特性去动态生成关联属性。
其实Python一早就内置了这一功能。Python有一个Magic Method叫__getattr__
,它会在找不到属性的时候调用,正好跟Ruby的method_missing
是一样的。
我们可以这样动态添加方法:
class MyClass(object):
def __getattr__(self, name):
"""called only method missing"""
if name == 'missed_method':
setattr(self, name, lambda : True)
return lambda : True
myClass = MyClass()
print(dir(myClass))
print(myClass.missed_method())
print(dir(myClass))
于是乎,前面的Ruby例子可以改写成下面的Python版本:
class Roman(object):
roman_int_map = {
"i": 1, "v": 5, "x": 10, "l": 50,
"c":100, "d": 500, "m": 1000
}
def roman_to_int(self, s):
decimal = 0
for i in range(len(s), 0, -1):
if (i == len(s) or
self.roman_int_map[s[i-1]] >= self.roman_int_map[s[i]]):
decimal += self.roman_int_map[s[i-1]]
else:
decimal -= self.roman_int_map[s[i-1]]
return decimal
def __getattr__(self, s):
return self.roman_to_int(s)
r = Roman()
print(r.iv)
r.iv #=> 4
r.xxiii #=> 23
r.mm #=> 2000
很有可能你会觉得这个例子没有什么意义,你是对的!其实它就是把方法名当做一个罗马数字字符串,传入roman_to_int
而已。不过正如递归不仅仅能用来计算斐波那契数列,__getattr__
的这一特技实际上还是挺有用的。你可以用它来进行延时计算,或者方法分派,抑或像基于Ruby的DSL一样动态地合成方法。这里有个用__getattr__
实现延时加载的例子。
函数对象
在C++里面,你可以重载掉operator ()
,这样就可以像调用函数一样去调用一个类的实例。这样做的目的在于,把调用过程中的状态存储起来,借此实现带状态的调用。这种实例我们称之为函数对象。
在Python里面也有同样的机制。如果想要存储的状态只有一种,你需要的是一个生成器。通过send
来设置存储的状态,通过next
来获取调用的结果。不过如果你需要存储多个不同的状态,生成器就不够用了,非得定义一个函数对象不可。
Python里面可以重载__call__
来实现operator ()
的功能。下面的例子里面,就是一个存储有两个状态value和called_times的函数对象:
class CallableCounter(object):
def __init__(self, initial_value=0, start_times=0):
self.value = initial_value
self.called_times = start_times
def __call__(self):
print("Call the object and do something with value %d" % self.value)
self.value += 1
self.called_times += 1
def reset(self):
self.called_times = 0
cc = CallableCounter(initial_value=5)
for i in range(10):
cc()
print(cc.called_times)
cc.reset()
伪造一个Dict
最后请允许我奉上一个大脑洞,伪造一个Dict类。(这个可就没有什么实用价值了)
首先确定下把数据存在哪里。我打算把数据存储在类的__dict__
属性中。由于__dict__
属性的值就是一个Dict实例,我只需把调用在FakeDict
上的方法直接转发给对应的__dict__
的方法。代价是只能接受字符串类型的键。
class FakeDict:
def __init__(self, iterable=None, **kwarg):
if iterable is not None:
if isinstance(iterable, dict):
self.__dict__ = iterable
else:
for i in iterable:
self[i] = None
self.__dict__.update(kwarg)
def __len__(self):
"""len(self)"""
return len(self.__dict__)
def __str__(self):
"""it looks like a dict"""
return self.__dict__.__str__()
__repr__ = __str__
接下来开始做点实事。Dict最基本的功能是给一个键设置值和返回一个键对应的值。通过定义__setitem__
和__getitem__
方法,我们可以重载掉[]=
和[]
。
def __setitem__(self, k, v):
"""self[k] = v"""
self.__dict__[k] = v
def __getitem__(self, k):
"""self[k]"""
return self.__dict__[k]
别忘了del
方法:
def __delitem__(self, k):
"""del self[k]"""
del self.__dict__[k]
Dict的一个常用用途是允许我们迭代里面所有的键。这个可以通过定义__iter__
实现。
def __iter__(self):
"""it iterates like a dict"""
return iter(self.__dict__)
Dict的另一个常用用途是允许我们查找一个键是否存在。其实只要定义了__iter__
,Python就能判断if x in y
,不过这个过程中会遍历对象的所有值。对于真正的Dict而言,肯定不会用这种O(n)的判断方式。定义了__contains__
之后,Python会优先使用它来判断if x in y
。
def __contains__(self, k):
"""key in self"""
return k in self.__dict__
接下要实现==
的重载,不但要让FakeDict和FakeDict之间可以进行比较,而且要让FakeDict和正牌的Dict也能进行比较。
def __eq__(self, other):
"""
implement self == other FakeDict,
also implement self == other dict
"""
if isinstance(other, dict):
return self.__dict__ == other
return self.__dict__ == other.__dict__
要是继续实现了__subclass__
和__class__
,那么我们的伪Dict就更完备了。这个就交给感兴趣的读者自己动手了。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。