Skip to main content
 首页 » 编程设计

python之为什么将第二个属性添加到元类-属性-闭包组合会更改第一个属性

2025年05月04日12wayfarer

我想了解 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 知识。

请您参考如下方法:

这个问题实际上与元类或属性本身没有任何关系。它与您定义 get/set 函数的方式有关。您的 fget 和 fset 从封闭函数中引用变量 pattern。这创建了一个闭包。 pattern 的值将在 fget/fset 被调用时查找,而不是在它们调用时查找'重新定义。因此,当您在下一个循环迭代中覆盖 pattern 时,您会导致所有 fget/fset 函数现在引用新模式。

这是一个更简单的示例,显示了正在发生的事情:

def doIt(x): 
    funs = [] 
    for key, val in x.iteritems(): 
        thingy = val + 1 
        def func(): 
            return thingy 
        funs.append(func) 
    return funs 
 
>>> dct = {'a': 1, 'b': 2, 'c': 3} 
>>> funs = doIt(dct) 
>>> for f in funs: 
...     print f() 
 
3 
3 
3 

请注意,尽管这三个函数是在 thingy 具有不同值时定义的,但当我稍后调用它们时,它们都会返回相同的值。这是因为它们在调用时都在查找 thingy,这是在循环完成之后,所以 thingy 正好等于它设置的最后一个值。

解决这个问题的通常方法是将要关闭的变量作为附加函数参数的默认值传递。尝试像这样做你的 getter 和 setter:

def fget(self, pattern=pattern): 
    return getattr(self, pattern.attribute) 
def fset(self, value, pattern=pattern): 
    pattern.validate(value) 
    return setattr(self, pattern.attribute, value) 

默认参数是在函数定义时计算的,而不是调用时,因此这会强制每个函数“保存”它要使用的模式的值。