17

The descriptor protocol works fine but I still have one issue I would like to resolve.

I have a descriptor:

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

This is subclassed:

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)

And then used is a model 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

So far, so good, this works just as expected.

The only issue I have here is that we have to give a field name every time a field is declared. e.g. "country_code_2" for the country_code_2 field.

How would it be possible to get the attribute name of the model class and use it in the field class?

4

2 回答 2

32

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

简单的方法是使用 Python 3.6(或更高版本),并为您的描述符提供一个附加object.__set_name__()方法

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

当一个类被创建时,Python 会自动调用你在类上设置的任何描述符上的该方法,并传入类对象和属性名称。

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

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__调用你装饰它的任何类,但这需要你装饰每个类。元类会自动通过继承层次结构传播。

于 2017-02-03T12:06:07.717 回答
2

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

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

于 2017-02-10T07:06:48.683 回答