Подклассы: возможно ли переопределить свойство обычным атрибутом?

14

Давайте предположим, что мы хотим создать семейство классов, которые являются различными реализациями или специализациями всеобъемлющей концепции. Давайте предположим, что существует правдоподобная реализация по умолчанию для некоторых производных свойств. Мы хотели бы поместить это в базовый класс

class Math_Set_Base:
    @property
    def size(self):
        return len(self.elements)

Таким образом, подкласс автоматически сможет посчитать свои элементы в этом довольно глупом примере.

class Concrete_Math_Set(Math_Set_Base):
    def __init__(self,*elements):
        self.elements = elements

Concrete_Math_Set(1,2,3).size
# 3

Но что, если подкласс не хочет использовать это значение по умолчанию? Это не работает:

import math

class Square_Integers_Below(Math_Set_Base):
    def __init__(self,cap):
        self.size = int(math.sqrt(cap))

Square_Integers_Below(7)
# Traceback (most recent call last):
#   File "<stdin>", line 1, in <module>
#   File "<stdin>", line 3, in __init__
# AttributeError: can't set attribute

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

Это можно сделать? Если нет, каково следующее лучшее решение?

Пол Панцер
источник
Это, вероятно, указывает на проблему с самой иерархией классов. Тогда лучшим решением будет переделать эту иерархию. Вы упоминаете, что «существует правдоподобная реализация по умолчанию для некоторых производных свойств» на примере математических наборов и len(self.elements)в качестве реализации. Это очень конкретная реализация накладывает контракт на все экземпляры класса, а именно , что они обеспечивают , elementsкоторый является контейнером размера . Однако ваш Square_Integers_Belowкласс, похоже, не ведет себя таким образом (возможно, он генерирует своих членов динамически), поэтому он должен определить свое собственное поведение.
a_guest
Переопределение необходимо только потому, что оно унаследовало (неправильное) поведение в первую очередь. len(self.elements)не является подходящей реализацией по умолчанию для математических наборов, поскольку существует «множество» наборов, которые даже не имеют конечной мощности. В общем, если подкласс не хочет использовать поведение своих базовых классов, ему необходимо переопределить его на уровне класса. Атрибуты экземпляра, как следует из названия, работают на уровне экземпляра и, следовательно, регулируются поведением класса.
a_guest

Ответы:

6

Свойство - это дескриптор данных, который имеет приоритет над атрибутом экземпляра с тем же именем. Вы можете определить дескриптор без данных с помощью уникального __get__()метода: атрибут экземпляра имеет приоритет над дескриптором без данных с тем же именем, см. Документацию . Проблема здесь заключается в том, что non_data_propertyприведенное ниже описание предназначено только для вычислительных целей (вы не можете определить установщик или средство удаления), но, похоже, это имеет место в вашем примере.

import math

class non_data_property:
    def __init__(self, fget):
        self.__doc__ = fget.__doc__
        self.fget = fget

    def __get__(self, obj, cls):
        if obj is None:
            return self
        return self.fget(obj)

class Math_Set_Base:
    @non_data_property
    def size(self, *elements):
        return len(self.elements)

class Concrete_Math_Set(Math_Set_Base):
    def __init__(self, *elements):
        self.elements = elements


class Square_Integers_Below(Math_Set_Base):
    def __init__(self, cap):
        self.size = int(math.sqrt(cap))

print(Concrete_Math_Set(1, 2, 3).size) # 3
print(Square_Integers_Below(1).size) # 1
print(Square_Integers_Below(4).size) # 2
print(Square_Integers_Below(9).size) # 3

Однако это предполагает, что у вас есть доступ к базовому классу для внесения этих изменений.

Arkelis
источник
Это довольно близко к идеалу. Таким образом, это означает, что poperty, даже если вы определяете только метод get, неявно добавляет метод установки, который ничего не делает, кроме блокирующего присваивания? Интересно. Я, вероятно, приму этот ответ.
Пол Панцер
Да, согласно документам, свойство всегда определяет все три метода дескриптора ( __get__(), __set__()и __delete__()), и они вызывают, AttributeErrorесли вы не предоставляете им какую-либо функцию. Смотрите Python эквивалент реализации свойства .
Аркелис
1
Пока вы не заботитесь о setterили __set__собственности, это будет работать. Однако я хотел бы предупредить вас, что это также означает, что вы можете легко перезаписать свойство не только на уровне класса ( self.size = ...), но даже на уровне экземпляра (например Concrete_Math_Set(1, 2, 3).size = 10, будет одинаково допустимым). Пища для размышлений :)
r.ook
И Square_Integers_Below(9).size = 1также действует как простой атрибут. В этом конкретном случае использования «size» это может показаться неудобным (вместо этого используйте свойство), но в общем случае «легко переопределить вычисляемый реквизит в подклассе», в некоторых случаях это может быть полезно. Вы также можете контролировать доступ к атрибутам с помощью, __setattr__()но это может быть огромным.
Аркелис
1
Я не согласен с тем, что для них может быть действительный вариант использования, я просто хотел бы упомянуть о предостережении, поскольку это может усложнить отладку в будущем. До тех пор, пока вы и OP знаете о последствиях, все в порядке.
r.ook
10

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

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


Решение некоторых заблуждений

Первый ответ, хотя и правильный в большинстве случаев, не всегда так. Например, рассмотрим этот класс:

class Foo:
    def __init__(self):
        self.name = 'Foo!'
        @property
        def inst_prop():
            return f'Retrieving {self.name}'
        self.inst_prop = inst_prop

inst_propбудучи propertyбезвозвратно атрибутом экземпляра:

>>> Foo.inst_prop
Traceback (most recent call last):
  File "<pyshell#60>", line 1, in <module>
    Foo.inst_prop
AttributeError: type object 'Foo' has no attribute 'inst_prop'
>>> Foo().inst_prop
<property object at 0x032B93F0>
>>> Foo().inst_prop.fget()
'Retrieving Foo!'

Все зависит от того, где ваш propertyопределяется в первую очередь. Если ваш @propertyопределяется в пределах класса «видимость» (или действительно namespace), он становится атрибутом класса. В моем примере сам класс не знает о них, inst_propпока не будет создан экземпляр. Конечно, это не очень полезно, как свойство здесь вообще.


Но сначала давайте обратимся к вашему комментарию о разрешении наследования ...

Так как именно фактор наследования входит в этот вопрос? Эта следующая статья немного углубляется в тему, и порядок разрешения методов несколько связан, хотя в нем обсуждается в основном широта наследования, а не глубина.

В сочетании с нашими выводами, приведенными ниже настройками:

@property
def some_prop(self):
    return "Family property"

class Grandparent:
    culture = some_prop
    world_view = some_prop

class Parent(Grandparent):
    world_view = "Parent's new world_view"

class Child(Parent):
    def __init__(self):
        try:
            self.world_view = "Child's new world_view"
            self.culture = "Child's new culture"
        except AttributeError as exc:
            print(exc)
            self.__dict__['culture'] = "Child's desired new culture"

Представьте, что происходит, когда выполняются эти строки:

print("Instantiating Child class...")
c = Child()
print(f'c.__dict__ is: {c.__dict__}')
print(f'Child.__dict__ is: {Child.__dict__}')
print(f'c.world_view is: {c.world_view}')
print(f'Child.world_view is: {Child.world_view}')
print(f'c.culture is: {c.culture}')
print(f'Child.culture is: {Child.culture}')

Результат таков:

Instantiating Child class...
can't set attribute
c.__dict__ is: {'world_view': "Child's new world_view", 'culture': "Child's desired new culture"}
Child.__dict__ is: {'__module__': '__main__', '__init__': <function Child.__init__ at 0x0068ECD8>, '__doc__': None}
c.world_view is: Child's new world_view
Child.world_view is: Parent's new world_view
c.culture is: Family property
Child.culture is: <property object at 0x00694C00>

Обратите внимание, как:

  1. self.world_viewудалось применить, пока self.cultureне удалось
  2. cultureне существует в Child.__dict__( mappingproxyкласса, не путать с экземпляром __dict__)
  3. Несмотря на то, что cultureсуществует в нем c.__dict__, на него нет ссылок.

Возможно, вы сможете догадаться, почему - world_viewбыл переписан Parentклассом как не свойство, поэтому Childсмог перезаписать его. В то же время, так как cultureнаследуется, она существует только в пределах mappingproxyотGrandparent :

Grandparent.__dict__ is: {
    '__module__': '__main__', 
    'culture': <property object at 0x00694C00>, 
    'world_view': <property object at 0x00694C00>, 
    ...
}

На самом деле, если вы попытаетесь удалить Parent.culture:

>>> del Parent.culture
Traceback (most recent call last):
  File "<pyshell#67>", line 1, in <module>
    del Parent.culture
AttributeError: culture

Вы заметите, что это даже не существует для Parent. Потому что объект напрямую ссылается на Grandparent.culture.


Итак, что насчет Порядка разрешения?

Поэтому мы заинтересованы в том, чтобы соблюсти фактический порядок разрешения, попробуем Parent.world_viewвместо этого удалить :

del Parent.world_view
print(f'c.world_view is: {c.world_view}')
print(f'Child.world_view is: {Child.world_view}')

Интересно, каков результат?

c.world_view is: Family property
Child.world_view is: <property object at 0x00694C00>

Это вернулось к бабушке и дедушке world_view property, хотя мы успешно сумели назначить self.world_viewраньше! Но что, если мы решительно изменим world_viewна уровне класса, как и другой ответ? Что если мы удалим это? Что если мы присвоим атрибуту текущего класса свойство?

Child.world_view = "Child's independent world_view"
print(f'c.world_view is: {c.world_view}')
print(f'Child.world_view is: {Child.world_view}')

del c.world_view
print(f'c.world_view is: {c.world_view}')
print(f'Child.world_view is: {Child.world_view}')

Child.world_view = property(lambda self: "Child's own property")
print(f'c.world_view is: {c.world_view}')
print(f'Child.world_view is: {Child.world_view}')

Результат:

# Creating Child's own world view
c.world_view is: Child's new world_view
Child.world_view is: Child's independent world_view

# Deleting Child instance's world view
c.world_view is: Child's independent world_view
Child.world_view is: Child's independent world_view

# Changing Child's world view to the property
c.world_view is: Child's own property
Child.world_view is: <property object at 0x020071B0>

Это интересно, потому что c.world_viewвосстанавливается его атрибут экземпляра, а Child.world_viewтот, который мы присвоили. После удаления атрибута экземпляра он возвращается к атрибуту класса. И после переназначения Child.world_viewсвойства мы немедленно теряем доступ к атрибуту экземпляра.

Следовательно, мы можем предположить следующий порядок разрешения :

  1. Если атрибут класса существует и он есть property, извлеките его значение с помощью getterили fget(подробнее об этом позже). Текущий класс первым до базового класса последним.
  2. В противном случае, если атрибут экземпляра существует, получить значение атрибута экземпляра.
  3. Иначе, получить propertyатрибут не класса. Текущий класс первым до базового класса последним.

В этом случае давайте удалим рут property:

del Grandparent.culture
print(f'c.culture is: {c.culture}')
print(f'Child.culture is: {Child.culture}')

Который дает:

c.culture is: Child's desired new culture
Traceback (most recent call last):
  File "<pyshell#74>", line 1, in <module>
    print(f'Child.culture is: {Child.culture}')
AttributeError: type object 'Child' has no attribute 'culture'

Ta-д! Childтеперь имеет свои собственные, cultureоснованные на принудительной вставке в c.__dict__. Child.cultureконечно, не существует, так как он никогда не был определен в атрибуте Parentили Childклассе и Grandparentбыл удален.


Это коренная причина моей проблемы?

На самом деле нет . Ошибка, которую вы получаете, которую мы все еще наблюдаем при назначении self.culture, совершенно другая . Но порядок наследования устанавливает фон для ответа - самого propertyсебя.

Помимо ранее упомянутого getterметода, propertyтакже есть несколько аккуратных хитростей в рукавах. Наиболее актуальным в этом случае является метод setterили fset, который запускается self.culture = ...строкой. Поскольку вы propertyне реализовали никакой функции setterили fgetфункции, python не знает, что делать, и AttributeErrorвместо этого выдает (т.е. can't set attribute).

Однако, если вы реализовали setterметод:

@property
def some_prop(self):
    return "Family property"

@some_prop.setter
def some_prop(self, val):
    print(f"property setter is called!")
    # do something else...

При создании экземпляра Childкласса вы получите:

Instantiating Child class...
property setter is called!

Вместо того, чтобы получить AttributeError, вы теперь фактически вызываете some_prop.setterметод. Что дает вам больший контроль над вашим объектом ... с нашими предыдущими результатами мы знаем, что нам нужно перезаписать атрибут класса, прежде чем он достигнет свойства. Это может быть реализовано в базовом классе как триггер. Вот свежий пример:

class Grandparent:
    @property
    def culture(self):
        return "Family property"

    # add a setter method
    @culture.setter
    def culture(self, val):
        print('Fine, have your own culture')
        # overwrite the child class attribute
        type(self).culture = None
        self.culture = val

class Parent(Grandparent):
    pass

class Child(Parent):
    def __init__(self):
        self.culture = "I'm a millennial!"

c = Child()
print(c.culture)

Что приводит к:

Fine, have your own culture
I'm a millennial!

TA-Д! Теперь вы можете перезаписать свой собственный атрибут экземпляра поверх унаследованного свойства!


Итак, проблема решена?

... На самом деле, нет. Проблема этого подхода в том, что теперь у вас не может быть правильного setterметода. Есть случаи, когда вы хотите установить значения на вашем property. Но теперь всякий раз, когда вы устанавливаете self.culture = ...его, он всегда будет перезаписывать любую функцию, которую вы определили в getter(которая в данном случае на самом деле является просто @propertyобернутой частью. Вы можете добавить больше нюансов, но так или иначе, это всегда будет включать больше, чем просто self.culture = .... например:

class Grandparent:
    # ...
    @culture.setter
    def culture(self, val):
        if isinstance(val, tuple):
            if val[1]:
                print('Fine, have your own culture')
                type(self).culture = None
                self.culture = val[0]
        else:
            raise AttributeError("Oh no you don't")

# ...

class Child(Parent):
    def __init__(self):
        try:
            # Usual setter
            self.culture = "I'm a Gen X!"
        except AttributeError:
            # Trigger the overwrite condition
            self.culture = "I'm a Boomer!", True

Это waaaaay более сложные , чем другой ответ, size = Noneна уровне класса.

Вы также можете написать собственный дескриптор, чтобы обрабатывать __get__и __set__или дополнительные методы. Но в конце дня, когда на self.cultureнего ссылаются, __get__всегда будет запускаться первым, а когда на self.culture = ...него ссылаются, __set__всегда будет запускаться первым. Там нет возможности обойти это, насколько я пытался.


Суть вопроса, ИМО

Проблема, которую я вижу здесь, - ты не можешь есть свой пирог и есть его тоже. propertyподразумевается как дескриптор с удобным доступом из таких методов, как getattrили setattr. Если вы также хотите, чтобы эти методы достигли другой цели, вы просто напрашиваетесь на неприятности. Я бы, возможно, переосмыслил подход:

  1. Мне действительно нужно propertyдля этого?
  2. Может ли метод служить мне по-другому?
  3. Если мне нужно property, есть ли какая-то причина, по которой мне нужно было бы перезаписать это?
  4. Подкласс действительно принадлежит той же семье, если они propertyне применяются?
  5. Если мне нужно перезаписать какие-либо / все propertys, будет ли отдельный метод мне лучше, чем просто переназначение, так как переназначение может случайно аннулировать propertys?

Для пункта 5, мой подход будет иметь overwrite_prop()метод в базовом классе, который перезаписывает атрибут текущего класса, так что propertyбольше не будет срабатывать:

class Grandparent:
    # ...
    def overwrite_props(self):
        # reassign class attributes
        type(self).size = None
        type(self).len = None
        # other properties, if necessary

# ...

# Usage
class Child(Parent):
    def __init__(self):
        self.overwrite_props()
        self.size = 5
        self.len = 10

Как вы можете видеть, хотя он все еще немного надуман, он, по крайней мере, более явный, чем загадочный size = None. Тем не менее, в конечном счете, я бы вообще не перезаписывал свойство и пересмотрел бы мой дизайн с самого начала.

Если вы зашли так далеко - спасибо, что прошли это путешествие со мной. Это было забавное маленькое упражнение.

r.ook
источник
2
Вау, спасибо большое! Мне понадобится немного времени, чтобы переварить это, но я полагаю, что попросил об этом.
Пол Панцер
6

А @propertyопределяется на уровне класса. Документация содержит подробные сведения о том, как она работает, но достаточно сказать, что установка или получение свойства приводят к вызову определенного метода. Однако propertyобъект, который управляет этим процессом, определяется собственным определением класса. То есть он определен как переменная класса, но ведет себя как переменная экземпляра.

Одним из следствий этого является то, что вы можете свободно переназначить его на уровне класса :

print(Math_Set_Base.size)
# <property object at 0x10776d6d0>

Math_Set_Base.size = 4
print(Math_Set_Base.size)
# 4

И, как и любое другое имя уровня класса (например, методы), вы можете переопределить его в подклассе, просто явно определив его по-другому:

class Square_Integers_Below(Math_Set_Base):
    # explicitly define size at the class level to be literally anything other than a @property
    size = None

    def __init__(self,cap):
        self.size = int(math.sqrt(cap))

print(Square_Integers_Below(4).size)  # 2
print(Square_Integers_Below.size)     # None

Когда мы создаем фактический экземпляр, переменная экземпляра просто затеняет переменную класса с тем же именем. Обычно propertyобъект использует некоторые махинации для управления этим процессом (т. Е. Применяя методы получения и установки), но когда имя уровня класса не определено как свойство, ничего особенного не происходит, и поэтому он действует так, как вы ожидаете от любой другой переменной.

Зеленый плащ парень
источник
Спасибо, это очень просто. Означает ли это, что даже если в моем классе и его предках никогда не было каких-либо свойств, он все равно сначала будет искать во всем дереве наследования имя уровня класса, прежде чем даже потрудится посмотреть на __dict__? Кроме того, есть ли способы автоматизировать это? Да, это всего лишь одна строчка, но это та вещь, которую очень загадочно читать, если вы не знакомы с кровавыми подробностями собственности и т. Д.
Пол Панцер
@PaulPanzer Я не изучал процедуры, стоящие за этим, поэтому я не мог дать вам удовлетворительный ответ сам. Вы можете попытаться разобраться в этом из исходного кода cpython, если хотите. Что касается автоматизации процесса, я не думаю, что есть хороший способ сделать это, кроме как вообще не делать его свойством или просто добавить комментарий / строку документации, чтобы те, кто читает ваш код, знали, что вы делаете , Что-то вроде# declare size to not be a @property
Green Cloak Guy
4

Вам не нужно назначение (для size) вообще. sizeэто свойство в базовом классе, поэтому вы можете переопределить это свойство в дочернем классе:

class Math_Set_Base:
    @property
    def size(self):
        return len(self.elements)

    # size = property(lambda self: self.elements)


class Square_Integers_Below(Math_Set_Base):

    def __init__(self, cap):
        self._cap = cap

    @property
    def size(self):
        return int(math.sqrt(self._cap))

    # size = property(lambda self: int(math.sqrt(self._cap)))

Вы можете (микро) оптимизировать это, предварительно вычислив квадратный корень:

class Square_Integers_Below(Math_Set_Base):

    def __init__(self, cap):
        self._size = int(math.sqrt(self._cap))

    @property
    def size(self):
        return self._size
chepner
источник
Это замечательно, просто переопределите родительское свойство дочерним свойством! +1
r.ook
Это в некотором смысле прямая вещь, которую нужно сделать, но я был заинтересован и специально спросил, каким образом можно разрешить переопределение не-свойства, чтобы не навязывать неудобство записи свойства-памятки, где простое присваивание будет выполнять работа.
Пол Панцер
Я не уверен, почему вы захотите усложнить свой код таким образом. За небольшую цену признания, что sizeэто свойство, а не атрибут экземпляра, вам не нужно делать ничего особенного .
Чепнер
Мой вариант использования - это множество дочерних классов с множеством атрибутов; не нужно писать 5 строк вместо одной, что оправдывает определенную фантазию, ИМО.
Пол Панцер
2

Похоже, вы хотите определить sizeв своем классе:

class Square_Integers_Below(Math_Set_Base):
    size = None

    def __init__(self, cap):
        self.size = int(math.sqrt(cap))

Другой вариант - сохранить capв своем классе и рассчитать его, sizeопределив его как свойство (которое переопределяет свойство базового класса size).

Uri
источник
2

Я предлагаю добавить сеттер так:

class Math_Set_Base:
    @property
    def size(self):
        try:
            return self._size
        except:
            return len(self.elements)

    @size.setter
    def size(self, value):
        self._size = value

Таким образом, вы можете переопределить .sizeсвойство по умолчанию следующим образом:

class Concrete_Math_Set(Math_Set_Base):
    def __init__(self,*elements):
        self.elements = elements

class Square_Integers_Below(Math_Set_Base):
    def __init__(self,cap):
        self.size = int(math.sqrt(cap))

print(Concrete_Math_Set(1,2,3).size) # 3
print(Square_Integers_Below(7).size) # 2
эмир
источник
1

Также вы можете сделать следующее

class Math_Set_Base:
    _size = None

    def _size_call(self):
       return len(self.elements)

    @property
    def size(self):
        return  self._size if self._size is not None else self._size_call()

class Concrete_Math_Set(Math_Set_Base):
    def __init__(self, *elements):
        self.elements = elements


class Square_Integers_Below(Math_Set_Base):
    def __init__(self, cap):
        self._size = int(math.sqrt(cap))
Рябченко Александр
источник
Это здорово, но вам не нужно _sizeили _size_callтам. Вы могли бы запечь в вызове функции в sizeкачестве условия и использовать try... except... для проверки _sizeвместо того, чтобы брать дополнительную ссылку на класс, которая не будет использоваться. Несмотря на это, я чувствую, что это даже более загадочно, чем простая size = Noneперезапись в первую очередь.
r.ook
0

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

В исходном коде существует своего рода конфликт между именем sizeиз базового класса и атрибутом self.sizeиз производного класса. Это может быть видно, если мы заменим имя self.sizeиз производного класса на self.length. Это выведет:

3
<__main__.Square_Integers_Below object at 0x000001BCD56B6080>

Тогда, если мы заменим имя метода sizeс lengthво всех вхождений по всей программе, это приведет к тем же исключением:

Traceback (most recent call last):
  File "C:/Users/Maria/Downloads/so_1.2.py", line 24, in <module>
    Square_Integers_Below(7)
  File "C:/Users/Maria/Downloads/so_1.2.py", line 21, in __init__
    self.length = int(math.sqrt(cap))
AttributeError: can't set attribute

Фиксированный код или, в любом случае, версия, которая каким-то образом работает, должна сохранять код точно таким же, за исключением класса Square_Integers_Below, который установит метод sizeиз базового класса в другое значение.

class Square_Integers_Below(Math_Set_Base):

    def __init__(self,cap):
        #Math_Set_Base.__init__(self)
        self.length = int(math.sqrt(cap))
        Math_Set_Base.size = self.length

    def __repr__(self):
        return str(self.size)

И затем, когда мы запустим всю программу, вывод будет:

3
2

Я надеюсь, что это было так или иначе полезно.

Sofia190
источник