Как я могу получить имя атрибута при работе с протоколом дескриптора в 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")
Это подкласс:
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 ответа
Есть простой путь, и есть трудный путь.
Самый простой способ - использовать 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__
вызывает любой класс, которым вы его украшаете, но для этого требуется, чтобы вы украсили каждый класс. Вместо этого метакласс автоматически распространяется через иерархию наследования.
Я рассматриваю это в своей книге " Дескрипторы Python", хотя я не обновил второе издание, чтобы добавить новую функцию в 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
Учитывая каждый раз, когда вы используете имя дескриптора, включается экземпляр, это не должно быть слишком сложно, чтобы понять, как использовать. Вы также можете найти способ кешировать имя, как только вы посмотрели его в первый раз.