我想了解 python 元类。为了练习,我正在实现一种用于编写类的声明性方式(类似于 sqlalchemy.ext.declarative)。只要我只有一个属性,这看起来很有希望。
但是当我添加另一个属性时,第一个属性的某些部分会发生变化,并且第一个属性的值会根据第二个属性的模式进行验证。这可能是由元类、闭包、属性或它们的组合引起的。我试着给出一个最小的、完整的但可读的例子。
#! /usr/bin/env python
"""
Something like:
class Artist:
locale = Pattern('[A-Z]{2}-[A-Z]{2}')
should be equivalent to:
class Artist:
def __init__(self):
self._locale = None
@property
def locale(self):
return self._locale
@locale.setter
def locale(self, value):
validate(value, '[A-Z]{2}-[A-Z]{2}')
self._locale = value
Problem:
The code below works if Artist has only one attribute.
When I add another one with a different pattern, only that last
pattern is used in validation.
"""
import re
import unittest
# this class (and future siblings) are used to describe attributes
class Pattern(object):
def __init__(self, pattern):
self.pattern = pattern
def validate(self, value):
if value is None:
return
if not re.match("^%s$" % self.pattern, value):
raise ValueError("invalid value: %r" % value)
def __repr__(self):
return "%s(pattern=%r)" % (self.__class__.__name__, self.pattern)
# __metaclass__ based class creation
def createClassFromDeclaration(name, bases, dct):
""" Examine dct, create initialization in __init__ and property. """
attributes = dict()
properties = dict()
for key, value in dct.iteritems():
if not isinstance(value, Pattern):
continue
pattern = value
pattern.attribute = "_%s" % key
attributes[key] = pattern
def fget(self):
return getattr(self, pattern.attribute)
def fset(self, value):
pattern.validate(value)
return setattr(self, pattern.attribute, value)
properties[key] = property(fget, fset)
def __init__(self, **kwargs):
# set all attributes found in the keyword arguments
for key, value in kwargs.iteritems():
if key in self.__attributes__:
setattr(self, key, value)
# set all attributes _NOT_ found to None
for key, declaration in attributes.iteritems():
if not hasattr(self, declaration.attribute):
setattr(self, key, None)
dct = dict(dct)
dct.update(properties)
dct['__init__'] = __init__
dct['__attributes__'] = attributes
return type(name, bases, dct)
# declarative class
class Artist(object):
__metaclass__ = createClassFromDeclaration
# FIXME: adding a second attribute changes the first pattern
locale = Pattern('[A-Z]{2}-[A-Z]{2}')
date = Pattern('[0-9]{4}-[0-9]{2}-[0-9]{2}')
# some unit tests
class TestArtist(unittest.TestCase):
def test_attributes_are_default_initialized(self):
artist = Artist()
self.assertIsNone(artist.date)
self.assertIsNone(artist.locale)
def test_attributes_are_initialized_from_keywords(self):
artist = Artist(locale="EN-US", date="2013-02-04")
self.assertEqual(artist.date, "2013-02-04")
# FIXME: the following does not work.
# it validates against the date pattern
self.assertEqual(artist.locale, "EN-US")
def test_locale_with_valid_value(self):
artist = Artist()
artist.date = "2013-02-04"
self.assertEqual(artist.locale, "2013-02-04")
# FIXME: the following does not work.
# it validates against the date pattern
artist.locale = "EN-US"
self.assertEqual(artist.locale, "EN-US")
def test_locale_with_invalid_value_throws(self):
artist = Artist()
with self.assertRaises(ValueError):
artist.locale = ""
with self.assertRaises(ValueError):
artist.locale = "EN-USA"
if __name__ == '__main__':
unittest.main()
# vim: set ft=python sw=4 et sta:
当我注释掉第二个属性('date')时,测试成功,但使用第二个属性,尝试设置第一个属性('locale')的测试失败。是什么导致单元测试失败?
免责声明:此代码仅用于培训。有一些方法可以创建不涉及元类、属性和闭包的相同功能(如你我所知)。但是,如果我们只走在我们熟悉的街道上,我们就不会学到任何新东西。请帮助我扩展我的 Python 知识。