2017-02-03 2 views
6

Протокол дескриптора работает нормально, но у меня все еще есть одна проблема, которую я хотел бы решить.Как получить имя атрибута при работе с протоколом дескриптора в 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.

Как можно получить имя атрибута класса модели и использовать его в полевом классе?

ответ

9

Существует простой способ, и есть трудный путь.

Простой способ заключается в использовании 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__ вызовов для любого класса вы украсите его, но это требует, чтобы украсить каждый класс. Вместо этого метаклас автоматически распространяется через иерархию наследования.

+0

Я добавил обновление после реализации ваших предложений. Не могли бы вы взглянуть на это? –

+0

Вы не используете 'six.with_metaclass()' правильно, он должен использоваться как базовый класс. См. Http://stackoverflow.com/questions/18513821/python-metaclass-understanding-the-with-metaclass. Вместо этого вы можете начать с того, чтобы заставить его работать только с Python 2 или 3 перед использованием 'six' :-) –

+0

Я заменяю' six.with_metaclass() 'на' __metaclass__ = BaseModelMeta'. (Я использую Python 2) Это дает ту же ошибку. –

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 

Учитывая каждый раз, когда вы используете имя дескриптора экземпляр участвуют, это не должно быть слишком трудно понять, как использовать.Вы также можете найти способ кэшировать имя, как только вы его просмотрите в первый раз.

+0

Этого недостаточно; дескрипторы следуют за MRO. –

+0

Ваш комментарий довольно сложно расшифровать; Предполагаю, вы хотели сказать, что функция не получает членов суперкласса? Я работаю над этим; Я сделаю это через несколько минут. –

+0

Редактирование завершено –

Смежные вопросы