Это будет многословный ответ, который может послужить лишь дополнительным ... но ваш вопрос заставил меня прокатиться по кроличьей норе, поэтому я хотел бы также поделиться своими выводами (и болью).
В конечном итоге этот ответ может оказаться бесполезным для вашей реальной проблемы. На самом деле, мой вывод таков: я бы этого не делал вообще. Сказав это, фон к этому выводу может вас развлечь немного, так как вы ищете больше деталей.
Решение некоторых заблуждений
Первый ответ, хотя и правильный в большинстве случаев, не всегда так. Например, рассмотрим этот класс:
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>
Обратите внимание, как:
self.world_view
удалось применить, пока self.culture
не удалось
culture
не существует в Child.__dict__
( mappingproxy
класса, не путать с экземпляром __dict__
)
- Несмотря на то, что
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
свойства мы немедленно теряем доступ к атрибуту экземпляра.
Следовательно, мы можем предположить следующий порядок разрешения :
- Если атрибут класса существует и он есть
property
, извлеките его значение с помощью getter
или fget
(подробнее об этом позже). Текущий класс первым до базового класса последним.
- В противном случае, если атрибут экземпляра существует, получить значение атрибута экземпляра.
- Иначе, получить
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
. Если вы также хотите, чтобы эти методы достигли другой цели, вы просто напрашиваетесь на неприятности. Я бы, возможно, переосмыслил подход:
- Мне действительно нужно
property
для этого?
- Может ли метод служить мне по-другому?
- Если мне нужно
property
, есть ли какая-то причина, по которой мне нужно было бы перезаписать это?
- Подкласс действительно принадлежит той же семье, если они
property
не применяются?
- Если мне нужно перезаписать какие-либо / все
property
s, будет ли отдельный метод мне лучше, чем просто переназначение, так как переназначение может случайно аннулировать property
s?
Для пункта 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
. Тем не менее, в конечном счете, я бы вообще не перезаписывал свойство и пересмотрел бы мой дизайн с самого начала.
Если вы зашли так далеко - спасибо, что прошли это путешествие со мной. Это было забавное маленькое упражнение.
len(self.elements)
в качестве реализации. Это очень конкретная реализация накладывает контракт на все экземпляры класса, а именно , что они обеспечивают ,elements
который является контейнером размера . Однако вашSquare_Integers_Below
класс, похоже, не ведет себя таким образом (возможно, он генерирует своих членов динамически), поэтому он должен определить свое собственное поведение.