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

时间:2017-02-03 12:02:05

标签: python descriptor python-descriptors

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

我有一个描述符:

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

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

2 个答案:

答案 0 :(得分:17)

有一种简单的方法,而且有一种困难的方法。

简单的方法是使用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__调用,但这需要您装饰每个类。元类通过继承层次结构自动传播。

答案 1 :(得分:0)

我在我的书 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

考虑到每次使用描述符的名称时,都涉及实例,这不应该太难以弄清楚如何使用。一旦你第一次查看它,你也可以找到一种方法来缓存这个名字。