元编程

黑魔法防御

元编程是一种黑魔法,正派人士都很畏惧。——张教主

何谓元编程

元编程的几种形式

python下元编程的几个手段

预定义方法

没啥好多说的,看下面这个例子:

class A(object):

    def __init__(self, o):
        self.__obj__ = o

    def __getattr__(self, name):
        if hasattr(self.__obj__, name):
            return getattr(self.__obj__, name)
        return self.__dict__[name]

    def __iter__(self):
        return self.__obj__.__iter__()

l = []
a = A(l)

for i in xrange(101): a.append(i)

print sum(a)

这是一个再简单不过的agent类,不过不怎么完美。因为__iter__属于预定义函数,不会调用__getattr__来获得。因此还需要额外定义。下面章节中,我们将看到一种简单的多的方法来实现agent类。

另外,提一点细节的差异。__getattr__,__setattr__相对还是比较上层的,至少在这两个函数中,可以访问__dict__。而__getattribute__这个函数中,使用self.__dict__会引发递归,需要用object.__getattribute__(self, name)。相对的,__getattribute__只能用于new style class。

同样,__getattr__,__setattr__,__getattribute__的用法不止于此。通过定义这三个函数,可以对类的成员做出非常多的变化。但是,和下面提到的手段比起来,这无疑是比较初级的。

函数赋值

我们看这个从socket.py中摘出来的例子:

_delegate_methods = ("recv", "recvfrom", "recv_into", "recvfrom_into",
                     "send", "sendto")

def __init__(self, family=AF_INET, type=SOCK_STREAM, proto=0, _sock=None):
    if _sock is None:
        _sock = _realsocket(family, type, proto)
    self._sock = _sock
    for method in _delegate_methods:
        setattr(self, method, getattr(_sock, method))

当你调用s.recv(4)的时候,你以为自己在调用_socketobject的方法?错了,那方法其实是对应的_realsocket的。这是替换实例函数的例子。


这可以做什么用?我们来看我写的一个http代理装饰器。

def http_proxy(proxyaddr, username=None, password=None):
    def reciver(func):
        def creator(family=socket.AF_INET, type=socket.SOCK_STREAM, proto=0):
            sock = func(family, type, proto)
            sock.connect(proxyaddr)
            def newconn(addr): http_connect(sock, addr, username, password)
            sock.connect, sock.connect_ex = newconn, newconn
            return sock
        return creator
    return reciver

我们再看descriptor里面的这个例子:


class A(object):

    def b(self):
        print 'ok'

a = A()
print A.b, a.b
<unbound method A.b> <bound method A.b of <__main__.A object at 0x7f81620d9990>>
print a.b.im_self == a, a.b.im_func == A.b.im_func
True True
print A.__dict__['b'], A.b.im_func
<function b at 0x7f81620db500> <function b at 0x7f81620db500>

def c(self): print 'not ok'
A.b = c

print A.b, a.b
<unbound method A.c> <bound method A.c of <__main__.A object at 0x7f81620d9990>>
print a.b.im_self == a, a.b.im_func == A.b.im_func
True True
print A.__dict__['b'], A.b.im_func
<function c at 0x7f81620db488> <function c at 0x7f81620db488>

a.b()
not ok

这同样是函数替换,不过替换的是类函数方法。

descriptor

所谓descriptor,就是带有__get__和__set__函数的对象。当访问某个对象的某个属性,这个属性又是一个descriptor时。返回值是descriptor的__get__调用的返回,set同理类推。带有__set__的称为data descriptor,只有__get__的称为non data descriptor。

python访问某个对象的某个属性时,是按照以下次序的:

  1. class的data descriptor。
  2. instance属性,无论其是否是descriptor,不调用__get__。
  3. class属性,包括non data descriptor。

使用descriptor,可以很容易的定义a.name之类获得值和设定的操作中需要执行什么。

实际上,我们使用的类函数就是基于descriptor做的。


class A(object):

    def b(self):
        print 'ok'

a = A()
print A.b, a.b
print a.b.im\_self == a, a.b.im\_func == A.b.im\_func
print A.__dict__['b'], A.b
<function b at 0x7f81620db500> <unbound method A.b>

最后一个A.__dict__['b'], A.b,揭示了一个问题,两者不一致。至于为什么?那是因为descriptor在起作用,在A.b的时候,调用了某个__get__,将函数和类组合成和method对象丢了出来。这个__get__在哪里呢?我们来看这么个例子。


def f(self): print self['a'], 'ok'

print dir(f)
['__call__', '__class__', '__closure__', '__code__', '__defaults__', '__delattr__', '__dict__', '__doc__', '__format__', '__get__', '__getattribute__', '__globals__', '__hash__', '__init__', '__module__', '__name__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', 'func_closure', 'func_code', 'func_defaults', 'func_dict', 'func_doc', 'func_globals', 'func_name']  
f({'a': 1})
1 ok

o = {'a': 1}
m = f.__get__(o, dict)
print m
<bound method dict.f of {'a': 1}>
m()
1 ok

这可说的不能再明白了,function对象本身就具备__get__,是non data descriptor。按照上述的规则,排在instance之后。所以,我们给instance加载属性,可以重载掉类的函数。

我们看下面这个例子,这同样是从本公司的业务系统中摘出来简化的。


class Meta(type):
    def __new__(cls, name, bases, attrs):
        for k, v in attrs.items():
            if hasattr(v, '__meta_init__'): v.__meta_init__(k)
        return type.__new__(cls, name, bases, attrs)

class AttrBase(object):

    def __meta_init__(self, k): self.name = k
    def __get__(self, obj, cls): return obj[self.name]
    def __set__(self, obj, value): obj[self.name] = value

class Base(dict):
    __metaclass__ = Meta

class User(Base):

    name = AttrBase()

b = User()
b.name = 'shell'
print b
print b.name

注意到,当你访问b.name的时候,实际上是去访问了b['name']。这个过程不是通过User类重载__getattr__实现的,而是通过descriptor。另外,我们处理这个例子的时候,用到了元类。下面一节介绍一下元类。

元类

我们先看这么一个例子:

class Base(dict):
    __metaclass__ = Meta
    def output(self, o): print 'hello, %s' % o
b = Base()
b.output('world')

你认为输出是什么?

再加上下面的代码呢?

class Meta(type):
    def __new__(cls, name, bases, attrs):
        output = attrs['output']
        attrs['output'] = lambda self, x: output(self, 'python')
        return type.__new__(cls, name, bases, attrs)

实际上,输出是hello, python。


为什么?我们要从type说起。在python中,出乎我们的意料,type不是一个函数,而是一个类。type的作用不仅仅可以显示某个对象属于哪个类,更重要的是,type可以动态的创建类。就像下面这样。

A = type('A', (object,), {'b': 1})
a = A()
print A, a.b

我们稍加变化,可以变成这样的代码。没什么区别。

def f(name, bases, attrs):
    attrs['c'] = 2
    return type(name, bases, attrs)

A = f('A', (object,), {'b': 1})
a = A()
print A, a.b, a.c

最后,我们把代码变成这个样子。

def f(name, bases, attrs):
    attrs['c'] = 2
    return type(name, bases, attrs)

class A(object):
    __metaclass__ = f
    b = 1

a = A()
print A, a.b, a.c

__metaclass__实际上,就是指创建类A的时候,要用什么函数进行生成。


可是且慢,type并不是一个函数,而是一个类阿。其实我们不妨这么看,类本身,可以视作是一个构造函数。

class A(object): pass
def B(): return A()

a = A()
b = B()
print a, b

由两者创建出来的对象并没有什么本质区别。所以,以下两个东西,其实在使用上是等价的。

class M(type):
    def __new__(cls, name, bases, attrs):
        attrs['c'] = 2
        return type.__new__(cls, name, bases, attrs)

def f(name, bases, attrs):
    attrs['c'] = 2
    return type(name, bases, attrs)

A = M('A', (object,), {'b': 1})
a = A()
print A, a.b, a.c

既然如此,我们当然可以在__metaclass__中,将f替换为M。

class M(type):
    def __new__(cls, name, bases, attrs):
        attrs['c'] = 2
        return type.__new__(cls, name, bases, attrs)

class A(object):
    __metaclass__ = M
    b = 1

a = A()
print A, a.b, a.c

这就是本文最上面的元类的来历。

我们甚至可以创建元类的元类。

class M1(type):
    def __new__(cls, name, bases, attrs):
        def f(cls, name, bases, attrs):
            attrs['c'] = 2
            return type.__new__(cls, name, bases, attrs)
        attrs['__new__'] = f
        return type.__new__(cls, name, (type,), attrs)

class M2(object):
    __metaclass__ = M1

class A(object):
    __metaclass__ = M2
    b = 1

a = A()
print A, a.b, a.c

当然,大家可能疑惑,为什么舍弃function,而使用元类。function固然简单,但是function是无法继承的。这里不仅仅指我们无法创建一个Meta的子类,扩充meta的行为。而且,使用function的类,一旦继承,其子类是不会管父类的__metaclass__定义的。

eval

大家看看下面这个程序,谁能看出是干什么的?

exec(compile(__import__('zlib').decompress(
__import__('base64').b64decode('eJylU9Fq4zAQfPdX7FGKpOIqDZQ+BPIVfTlognDs\
dSLOlowkN02/vru20jR3HIU7gbG0u5qZ1Ug3PxZjDIuddYvhlA7eFbYffEgQsIR4iiW8d3ZX\
wq6K+PRYwh6TH1LRBt+Dj5CLhyodiqLBlmb1L9naDjmkVgXQONp0AD+g+0yUIIJQUEVo7VzD\
I8A68+jd0yO62jcomV7Xvh8CxkgAOmDVSKXU36GPGdpfoFuvj8EmlEKImz9axjesJXMQhjRm\
bsoYKZhcKN3gp4Cv2Vkr5Uktl5BacRuFUqRB0MewtCJKuIWgiiKg4Ri1GVCf+SjNwc1ZwOb5\
jmnpd6GlxUzGkzPZRgqp75TYqM0VI68JVE1+jO5/HGl9gM46BOuu4jxsO6V0TFVIkRFl7ng1\
OZmb1X2V6oPkUqX3wY9DlEv18rD9N/+m6/DFj8t9yQ4EvhpT6wfsBpkbHoJ1CcgdeF3qB2Cs\
hA52J3imsk7/HNkj5teM6KoeJd1+XYX9K2lVv4G83I9boL7pNXyzr6BjMobjxsB6DcKYvrLO\
GDELo8fU2ZhK4B10avP70vPvArVcbelgRjELaalwNpZdEPckngxqbJ1kxlOAXcTpNRbZLMa5\
dpYivG9KQCunLvBtqFwzRgyS4vmVMdYqn2fxAdHyTCM=')),'', 'exec'))

看不出来是吧?那先看看这个例子:

def remove_list(li, obj):
    lic = li[:]
    lic.remove(obj)
    return lic

ops = ["+", "-", "*", "/"]
def gen_make(nums, *exes):
    if len(nums) == 0:
        try:
            if eval("".join(exes)) == 24: print "".join(exes).replace(".0", "")
        except: pass
    elif len(exes) == 0:
        for n in nums: gen_make(remove_list(nums, n), str(n) + ".0")
    else:
        if len(exes) > 1:
            exes = list(exes)
            exes.insert(0, '(')
            exes.append(')')
        for n in nums:
            for op in ops:
                gen_make(remove_list(nums, n), str(n) + ".0", op, *exes)

gen_make([3, 4, 6, 8])

这是我写的一个24点计算程序,相对有点取巧。核心是利用字符串拼装表达式,然后用eval看看是不是等于24。相对来说,不使用eval的代码就要复杂很多。当然,下面这个版本要完整很多。

from itertools import combinations

class opt(object):
    def __init__(self, name, func, ex=True):
        self.name, self.func, self.exchangable = name, func, ex
    def __str__(self): return self.name
    def __call__(self, l, r): return self.func(l, r)
    def fmt(self, l, r):
        return '(%s %s %s)' % (fmt_exp(l), str(self), fmt_exp(r))

def eval_exp(e):
    if not isinstance(e, tuple): return e
    try: return e[0](eval_exp(e[1]), eval_exp(e[2]))
    except: return None

def fmt_exp(e): return e[0].fmt(e[1], e[2]) if isinstance(e, tuple) else str(e)
def print_exp(e): print fmt_exp(e), eval_exp(e)

def chkexp(target):
    def do_exp(e):
        if abs(eval_exp(e) - target) < 0.001: print fmt_exp(e), '=', target
    return do_exp

def iter_all_exp(f, ops, ns, e=None):
    if not ns: return f(e)
    for r in set(ns):
        ns.remove(r)
        if e is None: iter_all_exp(f, ops, ns, r)
        else:
            for op in ops:
                iter_all_exp(f, ops, ns, (op, e, r))
                if not op.exchangable:
                    iter_all_exp(f, ops, ns, (op, r, e))
        ns.append(r)

opts = [
    opt('+', lambda x, y: x+y),
    opt('-', lambda x, y: x-y, False),
    opt('*', lambda x, y: x*y),
    opt('/', lambda x, y: float(x)/y, False),]

if __name__ == '__main__':
    iter_all_exp(chkexp(24), opts, [3, 4, 6, 8])

回到最上面的那个表达式,那是一个程序被zip后base64的结果。当然,这个结果字符串被写入一个程序中,程序会自动解开内容进行eval。这种方法能够将任何代码变为一行流。而这个被变换的程序,就是实现这个功能的。

语法合成转换

语法转换的最著名例子是orm,为什么?orm实际上,将python语法转换成了sql语法。

慎用元类

正派人士为什么畏惧黑魔法?因为元编程会破坏直觉。

作为一个受到多年训练的程序员,你应当对你熟练使用的语言有一种直觉。看到dict(zip(title, data))就应当想到,这是一个拼接数据生成字典的代码。看到[(k, v) for k, v in d.iteritems() if k...]就应当知道,这是一个过滤算法。这是在长期使用程序后形成的一种条件反射。

而元编程会很大程度的破坏这种直觉。这也是为什么我很讨厌C++的算符重载的原因。你能够想像么?o = a + b;这个表达式,其实想表达的是两颗特定条件的树的拼和(concat)过程,而非合并(merge)过程。每次使用重载过的系统,我都需要重新训练我的直觉系统。

python的元编程具有同样可怕的效果。还记得eval中那个自压缩的例子么?那是一个极端,将人类可理解的程序编码为了人类无法理解的。而meta的那个例子说明,元编程可以在不知不觉中修改原始的定义。

python中的元编程手段远远不止上述这些,很多时候,我们自己都毫无感觉。甚至,要修改一个行为,不一定需要元编程,重载同样也可以让人摸不着头脑。但是由于元编程的复杂性,用户更难在其中进行源码阅读,跟踪,调试。

在设计,规划这类代码的时候,必须注意。首先,你的设计需要尽量符合直觉,尽量让使用者感到舒服。其次,你需要比常规程序更多的文档,尽量减少用户在阅读源码上的时间——除非你万分的有信心,用户能够毫无障碍的阅读你的源码。最后,你需要比较精细的测试,和更多的,更友好错误处理。因为一旦发生异常,用户可能无法处理不友好的抛出。

ORM的意义和目标

为什么要用ORM

ORM的根本目的,是将关系型数据库模型转换为面对对象模型。此外,他还兼具了一些其他功能。例如:

对象缓存

对象缓存的目的在于减少SQL的执行,增加程序执行速度,减少数据库开销。从某种意义来说,写的好的程序是不需要对象缓存的。但是这个“写的好”对程序设计提出了及其变态的要求。他要求无论程序由多少个组件组成,他们都必须能彼此传递数据,甚至知道对方的存在和细节,这样才能消除无效的查询和提交。但是这一要求使得代码之间产生了紧耦合,不利于系统的扩展。

lazy evaluation

lazy evaluation,中文翻译为惰性求值。指的是表达式的执行被延缓到真正需要值的时候。在ORM中,lazy evaluation一般是指查询过程不发生在查询语句生成的时候,而发生在实际发生数据请求的时候。

两者的区别在于,非lazy evaluation需要一次性完成表达式拼装,因此其逻辑是集中式的,不利于模块化。而lazy evaluation则可以将表达式逻辑的拼装分散在各个系统中。这同样是从系统耦合性和扩展性上来的需求。

另一种的lazy evaluation则是,在请求数据的时候只返回数据的一部分,当枚举到后续部分时再继续请求数据。如果情况合适,这个技巧可以有效减少计算开销和网络负载,或者减小响应时间。但是返回片段过小,请求过于频繁,应用场景不正确,反而会降低效率。

redis和RDBMS的区别

ACID

ACID是RDBMS的四个基本特性,即:

redis的ACID:

ACID不完整造成的问题

redisobj

redisobj的对象框架

大家应该猜到了,在基于redis的ORM中,我们主要需要使用元类和descriptor两种元编程方法。下面是使用时的样例代码:

class User(redisobj.Base):
    username = redisobj.String()
    password = redisobj.String()
    priv = redisobj.Integer()
    domain = redisobj.ForeignKey('Domain')

class Domain(redisobj.Base):
    name = redisobj.String()

class UserGroup(redisobj.Base):
    name = redisobj.String()

def run(c):
    d1 = Domain(name='domain0')
    c.save(d1)

    u1 = User(username='user1', priv=1, domain=d1)
    u2 = User(username='user2', priv=1, domain=d1)
    ug1 = UserGroup(name='usergroup1')
    ug2 = UserGroup(name='usergroup2')
    c.save(u1, u2, ug1, ug2)

    c.flush()
    u3 = c.load_by_id(User, 1)
    print u3.copy()

    u3.priv = 2
    u3.password = 'abc'
    del u3.username
    c.save(u3)
    c.flush()
    u4 = c.load_by_id(User, 1)
    print u4.copy()

    c.delete(u2)
    try: c.load_by_id(User, 2)
    except LookupError: print 'yes, User1 disappeared.'
    print c.list(User)

我们分析一下上述代码。User,Domain,UserGroup三者,都是继承自redisobj.Base,而这个类,则是由元类创建的。因此,元类Meta可以轻易的替换其中的属性。我将所有继承自AttrBase的全部归并到一起。具体的DataType,例如Integer,String,都是派生自这个类。于是,Base的所有继承者,都可以用Class.__attrs__访问属性列表。而使用instance.prarmeter_name的时候,descriptor发生作用,读取Base中的具体数据。这大概构成了redisobj的对象框架。

redisobj的Manager

manager是整个redisobj的核心,所有的保存,加载,都直接和manager打交道。当然,一种更加好看的方法是将manager全局化,然后在Base中添加方法(相应的,子类中需要添加类方法)来进行save/delete等行为。然而这将manager限定为全局只有一个(包括集群)。实际中碰到的很多例子,一个程序需要处理超过一个的redis(或者redis集群)。因此,我们在设计的时候保持了manager和object分离的设计思路。

不完整的ForeignKey

ForeignKey是所有数据类型中最特殊的一个,因为它重载了预定的descriptor。在我们的数据字典中,他和Integer没有区别(在关系上,ForeignKey也是Integer的一个子类)。然而,由于重载__get__和__set__。因此你可以认为obj.fk是一个对象。

注意,这里为什么重载descriptor,而不是直接将对象load入数字字典。因此被load入的对象也可能具备引用。反复引用之下,我们直接load对象的行为可能引发整个数据库被load入缓存的风险。而通过descriptor,我们可以在需要的时候载入对象,从而实现lazy evaluation。

但是这是不完整的行为!注意到redisobj里面只有FK,从来没有说反向引用,关系之类的说法。也就是说,当你在一个对象中保存另一个对象,可没有反向引用自动生成,当然也没有办法找到到底有多少个对象引用了当前对象。诚然,你可以自己做反向引用,然后自行添加。然而其中的不一致性问题需要自行解决。