有一天闲着无聊的时候,脑子里忽然冒出一个Magic Method的有趣用法,能够用__getattr__
来实现Python版的method_missing
。
顺着这个脑洞想下去,我发现Python的Magic Method确实有不少妙用之处。故在此记下几种有趣(也可能有用的)Magic Method技巧,但愿能够抛砖引玉,打开诸位读者的脑洞,想出更加奇妙的用法。html
若是对Magic Method的了解仅仅停留在知道这个术语和若干个经常使用方法上(如__lt__
,__str__
,__len__
),能够阅读下这份教程,看看Magic Method能够用来作些什么。python
先从最初的脑洞开始吧。曾几什么时候,Ruby社区的人老是夸耀Ruby的强大的元编程能力,其中method_missing
更是不可或缺的特性。经过调用BaseObject
上的method_missing
,Ruby能够实如今调用不存在的属性时进行拦截,并动态生成对应的属性。git
Ruby例子github
# 来自于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
是同样的。
咱们能够这样动态添加方法:ruby
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__
实现延时加载的例子。.net
在C++里面,你能够重载掉operator ()
,这样就能够像调用函数同样去调用一个类的实例。这样作的目的在于,把调用过程当中的状态存储起来,借此实现带状态的调用。这种实例咱们称之为函数对象。code
在Python里面也有一样的机制。若是想要存储的状态只有一种,你须要的是一个生成器。经过send
来设置存储的状态,经过next
来获取调用的结果。不过若是你须要存储多个不一样的状态,生成器就不够用了,非得定义一个函数对象不可。htm
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实例,我只需把调用在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就更完备了。这个就交给感兴趣的读者本身动手了。