关于Python Magic Method的若干脑洞

有一天闲着无聊的时候,脑子里突然冒出一个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就更完备了。这个就交给感兴趣的读者自己动手了。

你可能感兴趣的:(python)