2

Pythonメタクラスを理解したい。練習のために、クラスを作成するための宣言型の方法を実装しています(sqlalchemy.ext.declarativeと同様)。属性が1つしかない限り、これは有望に見えます。

しかし、別の属性を追加すると、最初の属性の一部が変更され、最初の属性の値が2番目の属性のパターンに対して検証されます。これは、メタクラス、クロージャ、プロパティ、またはそれらの組み合わせが原因である可能性があります。最小限の、完全であるが読みやすい例を挙げようとしています。

#! /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:

2番目の属性('date')をコメントアウトすると、テストは成功しますが、2番目の属性を使用すると、最初の属性('locale')を設定しようとするテストは失敗します。ユニットテストが失敗する原因は何ですか?

免責事項:このコードはトレーニング専用です。メタクラス、プロパティ、クロージャを含まない同じ機能を作成する方法があります(あなたと私が知っているように)。しかし、私たちが知っている通りを歩くだけでは、新しいことは何も学びません。Pythonの知識を広げるのを手伝ってください。

4

1 に答える 1

1

この問題は、メタクラスやプロパティ自体とは実際には何の関係もありません。これは、get/set関数をどのように定義しているかに関係しています。とは、囲んでいる関数から変数fgetを参照します。これにより、クロージャが作成されます。の値は、定義された時点ではなく、/が呼び出された時点で検索されます。したがって、次のループ反復で上書きすると、すべての/関数が新しいパターンを参照するようになります。fsetpatternpatternfgetfsetpatternfgetfset

何が起こっているかを示す簡単な例を次に示します。

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

3つの関数は、thingy異なる値を持つときに定義されますが、後でそれらを呼び出すと、すべて同じ値を返すことに注意してください。thingyこれは、ループが完了した後、呼び出されたときにすべて検索しているためthingy、最後に設定された値と同じになります。

これを回避する通常の方法は、クローズする変数を追加の関数引数のデフォルト値として渡すことです。次のようにゲッターとセッターを実行してみてください。

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)

デフォルトの引数は、呼び出し時ではなく関数定義時に評価されるため、これにより、各関数は使用するパターンの値を「保存」します。

于 2013-02-04T07:37:30.817 に答える