在 Python 中使用描述符协议时如何获取属性名称?

How can I get the attribute name when working with descriptor protocol in Python?

描述符协议工作正常,但我还有一个问题想解决。

我有一个描述符:

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")

这是子classed:

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:

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 字段。

如何获取模型class的属性名并在字段class中使用?

有简单的方法,也有困难的方法。

简单的方法是使用 Python 3.6(或更新版本),并为您的描述符添加一个额外的 object.__set_name__() method:

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

当创建 class 时,Python 会自动对您在 class 上设置的任何描述符调用该方法,传入 class 对象和属性名字.

对于较早的 Python 版本,下一个最佳选择是使用 metaclass;它会被创建的每个 subclass 调用,并给出一个方便的字典映射属性名称到属性值(包括你的描述符实例)。然后您可以利用这个机会将该名称传递给描述符:

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
class BaseModel(object, metaclass=BaseModelMeta):
    # Python 3

class BaseModel(object):
    __metaclass__ = BaseModelMeta
    # Python 2

您也可以使用 class 装饰器来为您装饰它的任何 class 执行 __set_name__ 调用,但这需要您装饰每个 class。 metaclass 会自动通过继承层次结构传播。

我在我的书 Python Descriptors 中讲过这个,虽然我还没有更新到第二版以在 3.6 中添加新功能。除此之外,它是关于描述符的相当全面的指南,仅一个功能就占了 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

考虑到每次使用描述符的名称,都会涉及到实例,这应该不难弄清楚如何使用。您还可以找到一种方法来缓存第一次查找的名称。