db.Property机制解析

标签:Google App Engine, Python

最开始接触GAE的API时,就对它的Property很感兴趣。因为我明明只是在Model定义时设置了类属性,但是它的实例的属性却必须和类属性的类型一致,还要接受各种检查之类的。只不过当时Python了解不深,看不懂其中的奥秘,而如今总算可以把它弄懂了。

首先,所有的属性都是db.Property或其子类的实例,而这个类定义了__get__和__set__方法,这也就意味着Property实际上是个data descriptor
所以考虑这样一个Model:
class A(db.Model):
	name = db.StringProperty(required=True)

a = A(name='123')
当构造a的时候,实际上并不是把a.__dict__['name']设为'123',而是调用了db.StringProperty.__set__(self, a, 'name')。
而__set__干了这件事:
  def __set__(self, model_instance, value):
    value = self.validate(value)
    setattr(model_instance, self._attr_name(), value)
很显然,它做了个检查后,把属性名加了个'_',然后设置到model_instance上了。于是检查一下a._name和a.__dict__['_name'],会发现值为'123'。因此这也说明,不要随便在model_instance里写'_'开头的属性,要注意别与Property名相冲突。

顺便看看validate做了什么:
  def validate(self, value):
    if self.empty(value):
      if self.required:
        raise BadValueError('Property %s is required' % self.name)
    else:
      if self.choices:
        match = False
        for choice in self.choices:
          if choice == value:
            match = True
        if not match:
          raise BadValueError('Property %s is %r; must be one of %r' %
                              (self.name, value, self.choices))
    if self.validator is not None:
      self.validator(value)
    return value
由于我这个例子很简单,只设置了required=True,因此当self.empty(value)为真时,就会抛出BadValueError('Property %s is required' % self.name)。
同时也能注意到,它的返回值才是真正的value,因此把str转成unicode或db.Text就能在这里自动完成了。

而当访问a.name时,获取的也不是a.__dict__['name'],而是调用db.StringProperty.__get__(self, a, A)。
不用说也知道,它只是返回a._name而已:
  def __get__(self, model_instance, model_class):
    if model_instance is None:
      return self

    try:
      return getattr(model_instance, self._attr_name())
    except AttributeError:
      return None
至此,Property可以检查属性值的秘密就揭开了,可Google为什么要用这么复杂的descriptor来实现这个机制呢?
熟悉Java或C#的,一定知道getter和setter这2个玩意,为了在设置值时做些检查,不得不实现这2个接口。Python当然也有相应的property函数可以实现这个目的,但是这样一来,我们的模型定义就会变成:
class A(object):
  def _get_name(self):
    return self._name

  def _set_name(self, name):
    self._name = name

  name = property(_get_name, _set_name)
我这里还没做属性检查什么的,但是代码量就已经远超过Google的实现了。因此,说白了Google只是为了开发者的方便,而让实现变得复杂的。同时这也获得了一个好处:可以重用__get__和__set__的代码,不需要每定义一个属性,就去写一个validator。

当然,它的秘密还不止这一点。看db.Model的定义,你会发现这个函数:
  @classmethod
  def properties(cls):
    return dict(cls._properties)
可是我们在定义A类的时候,明明没有去手动维护A._properties,为什么也能获取到它的属性呢?

其实在db.Model定义的第一行就揭示了这个秘密:
class Model(object):
  __metaclass__ = PropertiedClass
因此在A定义完后,会自动引入它的metaclass,而这个PropertiedClass主要是做了这样一个操作:_initialize_properties(cls, name, bases, dct)。
至于_initialize_properties的代码,由于太长,我就不贴出来, 主要就是维护_properties和_unindexed_properties。
Google自然也不会是为了好玩而这样引入一个metaclass,不然_initialize_properties的代码不可能这么冗长。仔细想想就会发现,继承一个Model的时候,如果子类定义了和父类同名的属性,就会将其覆盖了。而如果2者的定义不同,初始化父类时可能就报错了。因此_initialize_properties不得不检查这种情况。

此外,前面的__set__和__get__都去取了self._attr_name(),而它又会去取self.name,可是我们在初始化Property时并没有显式地对其赋值,那么self.name是怎么来的呢?
其实_initialize_properties还对所有属性调用了attr.__property_config__(model_class, attr_name),这样就把self.name设定上去了。

于是,Property机制就解析到这里了,有兴趣的还可以自行研究一下ReferenceProperty、_ReverseReferenceProperty、ComputedProperty和Expando。其实本文主要就是展示一下descriptor和metaclass的用法,Python提供了那么多好玩的东西,不会用就太可惜了。

0条评论 你不来一发么↓

    想说点什么呢?