web-dev-qa-db-ja.com

Pythonで記述子プロトコルを操作するときに属性名を取得するにはどうすればよいですか?

記述子プロトコルは正常に機能しますが、解決したい問題がまだ1つあります。

私は記述子があります:

class Field(object):
    def __init__(self, type_, name, value=None, required=False):
        self.type = type_
        self.name = "_" + name
        self.required = required
        self._value = value

    def __get__(self, instance, owner):
        return getattr(instance, self.name, self.value)

    def __set__(self, instance, value):
        if value:
            self._check(value)
            setattr(instance, self.name, value)
        else:
            setattr(instance, self.name, None)

    def __delete__(self, instance):
        raise AttributeError("Can't delete attribute")

    @property
    def value(self):
        return self._value

    @value.setter
    def value(self, value):
        self._value = value if value else self.type()

    @property
    def _types(self):
        raise NotImplementedError

    def _check(self, value):
        if not isinstance(value, Tuple(self._types)):
            raise TypeError("This is bad")

これはサブクラス化されています:

class CharField(Field):
    def __init__(self, name, value=None, min_length=0, max_length=0, strip=False):
        super(CharField, self).__init__(unicode, name, value=value)
        self.min_length = min_length
        self.max_length = max_length
        self.strip = strip

    @property
    def _types(self):
        return [unicode, str]

    def __set__(self, instance, value):
        if self.strip:
            value = value.strip()

        super(CharField, self).__set__(instance, value)

そして、モデルクラスが使用されます:

class Country(BaseModel):
    name = CharField("name")
    country_code_2 = CharField("country_code_2", min_length=2, max_length=2)
    country_code_3 = CharField("country_code_3", min_length=3, max_length=3)

    def __init__(self, name, country_code_2, country_code_3):
        self.name = name
        self.country_code_2 = country_code_2
        self.country_code_3 = country_code_3

これまでのところ、とても良い、これは期待どおりに機能します。

ここでの唯一の問題は、フィールドが宣言されるたびにフィールド名を指定する必要があることです。例えば"country_code_2" のために country_code_2フィールド。

モデルクラスの属性名を取得してフィールドクラスで使用するにはどうすればよいでしょうか。

11
Johan Vergeer

簡単な方法と難しい方法があります。

簡単な方法は、Python 3.6(またはそれ以降)を使用し、記述子に追加の object.__set_name__() method を与えることです:

_def __set_name__(self, owner, name):
    self.name = '_' + name
_

クラスが作成されると、Pythonは自動的にクラスに設定した記述子でそのメソッドを呼び出し、クラスオブジェクトと属性名を渡します。

以前のPythonバージョンの場合、次の最良のオプションは metaclass を使用することです。作成されたすべてのサブクラスに対して呼び出され、便利な辞書マッピング属性が与えられます名前と属性値(記述子インスタンスを含む)。この機会を使用して、その名前を記述子に渡すことができます。

_class BaseModelMeta(type):
    def __new__(mcls, name, bases, attrs):
        cls = super(BaseModelMeta, mcls).__new__(mcls, name, bases, attrs)
        for attr, obj in attrs.items():
            if isinstance(obj, Field):
                obj.__set_name__(cls, attr)
        return cls
_

これは、フィールドの同じ__set_name__()メソッドを呼び出し、Python 3.6はネイティブでサポートします。次に、それをBaseModelのメタクラスとして使用します。

_class BaseModel(object, metaclass=BaseModelMeta):
    # Python 3
_

または

_class BaseModel(object):
    __metaclass__ = BaseModelMeta
    # Python 2
_

クラスデコレータを使用して、デコレートするクラスの___set_name___呼び出しを行うこともできますが、これにはすべてのクラスをデコレートする必要があります。代わりに、メタクラスは継承階層を介して自動的に伝達されます。

20
Martijn Pieters

私はこれを私の本、Python Descriptorsで調べていますが、新しいバージョンを追加するために第2版に更新していません3.6の機能。それ以外は、ディスクリプターに関するかなり包括的なガイドであり、1つの機能について60ページを取り上げています。

とにかく、メタクラスなしで名前を取得する方法は、次の非常に単純な関数を使用することです。

def name_of(descriptor, instance):
    attributes = set()
    for cls in type(instance).__mro__:
        # add all attributes from the class into `attributes`
        # you can remove the if statement in the comprehension if you don't want to filter out attributes whose names start with '__'
        attributes |= {attr for attr in dir(cls) if not attr.startswith('__')}
    for attr in attributes:
        if type(instance).__dict__[attr] is descriptor:
            return attr

記述子の名前を使用するたびにインスタンスが関係することを考えると、これは使用方法を理解するのにそれほど難しくないはずです。また、最初に名前を調べた後で、名前をキャッシュする方法を見つけることもできます。

0
Jacob Zimmerman