Це буде довга звивиста відповідь, яка могла б слугувати лише безкоштовною, але ваше запитання взяло мене за проїзд по кролячій норі, тому я хотів би поділитися своїми висновками (і болем) також.
Зрештою, ця відповідь може бути корисною для вашої реальної проблеми. Насправді мій висновок такий: я б цього взагалі не робив. Сказавши це, передумови цього висновку можуть вас трохи розважити, оскільки ви шукаєте більше деталей.
Вирішення деяких помилкових уявлень
Перша відповідь, хоча в більшості випадків правильна, не завжди така. Наприклад, розглянемо цей клас:
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властивості ми миттєво втрачаємо доступ до атрибута екземпляра.
Тому ми можемо надати наступний порядок вирішення :
- Якщо атрибут класу існує, і він є a
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'
Та-да! Childтепер має власну cultureоснову на насильницькій вставці в c.__dict__. Child.cultureЗвичайно, не існує, оскільки він ніколи не був визначений в атрибуті Parentабо Childкласі, і Grandparent's був видалений.
Це першопричина моєї проблеми?
Власне, ні . Помилка, яку ви отримуєте, яку ми все-таки спостерігаємо при призначенні 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-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для цього?
- Чи може метод мені служити інакше?
- Якщо мені потрібен a
property, чи є якась причина, мені потрібно було б його перезаписати?
- Чи справді підклас належить до однієї сім'ї, якщо вони
propertyне застосовуються?
- Якщо мені все-таки потрібно перезаписати будь-який / all
propertys, чи корисний би мені окремий метод, ніж просто перепризначення, оскільки перепризначення може випадково втратити чинність 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
Як ви бачите, хоча це ще трохи надумано, це принаймні більш явно, ніж криптовалюта size = None. Це означає, що в кінцевому рахунку я взагалі не перезаписую властивість і перегляну мою конструкцію з кореня.
Якщо ви до цього встигли - дякую, що прогулялися зі мною. Це була весела маленька вправа.