Warning: file_get_contents(/data/phpspider/zhask/data//catemap/2/python/360.json): failed to open stream: No such file or directory in /data/phpspider/zhask/libs/function.php on line 167

Warning: Invalid argument supplied for foreach() in /data/phpspider/zhask/libs/tag.function.php on line 1116

Notice: Undefined index: in /data/phpspider/zhask/libs/function.php on line 180

Warning: array_chunk() expects parameter 1 to be array, null given in /data/phpspider/zhask/libs/function.php on line 181
Python 为什么向元类属性闭包混合添加第二个属性会更改第一个属性?_Python_Properties_Closures_Metaclass - Fatal编程技术网

Python 为什么向元类属性闭包混合添加第二个属性会更改第一个属性?

Python 为什么向元类属性闭包混合添加第二个属性会更改第一个属性?,python,properties,closures,metaclass,Python,Properties,Closures,Metaclass,我想理解python元类。实际上,我正在实现一种用于编写类的声明性方法(类似于sqlalchemy.ext.declarative)。这看起来很有希望,只要我只有一个属性 但是,当我添加另一个属性时,第一个属性的某些部分会发生更改,并且第一个属性的值会根据第二个属性的模式进行验证。这可能是由元类、闭包、属性或它们的组合引起的。我试图给出一个简单、完整但可读的示例 #! /usr/bin/env python """ Something like: class Artist:

我想理解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
的值将在调用
fget
/
fset
时查找,而不是在定义时查找。因此,当您在下一次循环迭代中覆盖
模式
时,会导致所有
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)

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

谢谢,这确实解决了问题。注意:闭包绑定到变量,而不是值!(一句话,100遍)