Это будет многословный ответ, который может послужить лишь дополнительным ... но ваш вопрос заставил меня прокатиться по кроличьей норе, поэтому я хотел бы также поделиться своими выводами (и болью).
Возможно, в конечном итоге этот ответ окажется бесполезным для вашей реальной проблемы. На самом деле, мой вывод таков: я бы этого не делал вообще. Сказав это, фон к этому выводу может вас немного развлечь, так как вы ищете более подробную информацию.
Устранение некоторого заблуждения
Первый ответ, хотя и правильный в большинстве случаи, это не всегда дело. Например, рассмотрим этот класс:
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
определен в классе "scope" (или на самом деле, namespace
), он становится атрибутом класса. В моем примере сам класс не знает о inst_prop
до момента его создания. Конечно, это не очень полезно как свойство вообще здесь.
Но сначала давайте обратимся к вашему комментарию по разрешению наследования ...
Так как именно фактор наследования в этом выдавать? Эта следующая статья немного углубляется в topi c, и Порядок разрешения методов несколько связан, хотя в нем обсуждается в основном ширина наследования, а не глубина.
в сочетании с нашими находя, учитывая приведенные ниже настройки:
@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
. Текущий класс первым до базового класса последним.
В таком случае, давайте удалим root 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'
Та-да! Child
теперь имеет свои собственные culture
, основанные на принудительном добавлении в c.__dict__
. Child.culture
, конечно, не существует, поскольку он никогда не определялся в Parent
или Child
атрибуте класса, а Grandparent
был удален.
Это root причина моей проблемы?
На самом деле, нет . Ошибка, которую вы получаете, которую мы все еще наблюдаем при назначении 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
. Это дает вам больше контроля над вашим объектом ... с нашими предыдущими результатами мы знаем, что нам нужно переписать атрибут класса до того, как достигнет свойства. Это может быть реализовано в базовом классе как триггер. Вот пример sh:
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-DAH! Теперь вы можете перезаписать ваш собственный атрибут экземпляра унаследованного свойства!
Итак, проблема решена?
... Не совсем. Проблема этого подхода в том, что теперь у вас не может быть правильного 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
с, будет ли отдельный метод мне лучше, чем просто переназначение, поскольку переназначение может случайно аннулировать property
с?
Для пункта 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
Как вы можете видеть хотя все еще немного надуманный, он, по крайней мере, более явный, чем крипти c size = None
. Тем не менее, в конечном счете, я бы вообще не перезаписывал это свойство и пересмотрел бы мой дизайн из root.
Если вы сделали это так далеко - спасибо за то, что прошли это путешествие со мной. Это было забавное маленькое упражнение.