Свойства класса и __setattr__

В классе Python, когда я использую __setattr__, он имеет приоритет над свойствами, определенными в этом классе (или любыми базовыми классами). Рассмотрим следующий код:

class Test(object):
    def get_x(self):
        x = self._x
        print "getting x: %s" % x
        return x
    def set_x(self, val):
        print "setting x: %s" % val
        self._x = val
    x = property(get_x, set_x)
    def __getattr__(self, a):
        print "getting attr %s" % a
        return -1
    def __setattr__(self, a, v):
        print "setting attr %s" % a

Когда я создаю класс и пытаюсь установить x, вместо set_x вызывается __setattr__:

>>> test = Test()
>>> test.x = 2
setting attr x
>>> print test.x
getting attr x_
getting x: -1
-1

Я хочу добиться того, что фактический код в __setattr__ был вызван только в том случае, если нет соответствующего свойства, т.е. test.x = 2 должен вызывать set_x. Я знаю, что я могу легко добиться этого, вручную проверив, является ли a "x" __setattr__, однако это создало бы плохой дизайн. Есть ли более умный способ обеспечить правильное поведение в __setattr__ для каждого свойства, определенного в классе и всех базовых классах?

Ответы

Ответ 1

Порядок поиска, используемый Python для атрибутов, выглядит следующим образом:

  • __getattribute__ и __setattr__
  • Дескрипторы данных, например property
  • Переменные экземпляра объекта __dict__ (при установке атрибута поиск заканчивается здесь)
  • Дескрипторы без данных (например, методы) и другие переменные класса
  • __getattr__

Так как __setattr__ является первым в строке, если у вас есть один, вам нужно сделать его умным, если не хотите, чтобы он обрабатывал все настройки атрибутов для вашего класса. Он может быть умным одним из двух способов: заставить обработать только определенные атрибуты набора или заставить обрабатывать все, кроме некоторого набора атрибутов. Для тех, которые вы не хотите обрабатывать, вызовите super().__setattr__.

Для вашего класса example обработка "всех атрибутов, кроме" x ", вероятно, проще всего:

def __setattr__(self, name, value):
    if name == "x":
        super(Test, self).__setattr__(name, value)
    else:
        print "setting attr %s" % name

Ответ 2

Это не пуленепробиваемое решение, но, как вы сказали, вы можете проверить, является ли свойство setattr ed, пытаясь получить доступ к объекту property, из атрибутов класса (используя getattr на объект класса).

class Test(object):

    def get_x(self):
        x = self._x
        print "getting x: %s" % x
        return x
    def set_x(self, val):
        print "setting x: %s" % val
        self._x = val
    x = property(get_x, set_x)

    @property  # no fset
    def y(self):
        print "getting y: 99"
        return 99

    def __getattr__(self, a):
        print "getting attr %s" % a
        return -1
    def __setattr__(self, a, v):
        propobj = getattr(self.__class__, a, None)
        if isinstance(propobj, property):
            print "setting attr %s using property fset" % a
            if propobj.fset is None:
                raise AttributeError("can't set attribute")
            propobj.fset(self, v)
        else:
            print "setting attr %s" % a
            super(Test, self).__setattr__(a, v)


test = Test()
test.x = 2
print test.x
#test.y = 88  # raises AttributeError: can't set attribute
print test.y
test.z = 3
print test.z

EDIT: замените self.__dict__[a] = v на super(Test, self).__setattr__(a, v), как показано на @Blckknght answer

Ответ 3

AFAIK, нет чистого способа сделать это. Проблема здесь возникает из асимметрии между __getattr__ и __setattr__. Первый вызывается только в том случае, если атрибут по нормальному средству выходит из строя, но последний называется безоговорочно. Поскольку нет общего способа, чтобы __setattr__ завершился с ошибкой, я не знаю, есть ли способ изменить это поведение.

В конечном счете, я считаю, что единственный способ получить поведение, которое вы хотите, - это свернуть действие set_ вашего свойства в вашу функцию __setattr__. И если вы это сделаете, вы также можете сбросить поведение геттеры в __getattr__, чтобы все это поддерживалось с 1 места.

Ответ 4

Я столкнулся с этими проблемами несколько раз, мое предпочтительное решение следующее:

  • Для каждого свойства [property] определите get_[property] и set_[property] функции, как вы, но без использования декораторов
  • Измените __getattr__ и __setattr__, чтобы они проверяли наличие этих функций и использовали их, если они доступны.

Здесь минимальный пример:

class SmartSetter(object):

  def __init__(self,name):
    self.set_name(name)

  def get_name(self):
    #return the name property
    return self._name

  def set_name(self,value):
    #set the name property
    self._name = value

  def __getattr__(self,key):
    #first condition is to avoid recursive calling of this function by
    #__setattr__ when setting a previously undefined class attribute.
    if not key.startswith('get_') and hasattr(self,'get_'+key):
      return getattr(self,'get_'+key)()
    #implement your __getattr__ magic here...
    raise AttributeError("no such attribute: %s" % key)

  def __setattr__(self,key,value):
    try:
      return getattr(self,'set_'+key)(value)
    except AttributeError:
      #implement your __setattr__ magic here...
      return super(SmartSetter,self).__setattr__(key,value)

if __name__ == '__main__':
  smart_setter = SmartSetter("Bob the Builder")

  print smart_setter.name
  #Will print "Bob the Builder"

  smart_setter.name = "Spongebob Squarepants"

  print smart_setter.name
  #Will print "Spongebob Squarepants"

Преимущество этого метода заключается в том, что он сохраняет нормальное поведение для всех атрибутов, кроме тех, которые имеют методы "getter" и "setter", и что он не требует каких-либо изменений функций __getattr__ и __setattr__, когда вы добавляете или удаляете свойства.