Це буде довга звивиста відповідь, яка могла б слугувати лише безкоштовною, але ваше запитання взяло мене за проїзд по кролячій норі, тому я хотів би поділитися своїми висновками (і болем) також.
Зрештою, ця відповідь може бути корисною для вашої реальної проблеми. Насправді мій висновок такий: я б цього взагалі не робив. Сказавши це, передумови цього висновку можуть вас трохи розважити, оскільки ви шукаєте більше деталей.
Вирішення деяких помилкових уявлень
Перша відповідь, хоча в більшості випадків правильна, не завжди така. Наприклад, розглянемо цей клас:
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
property
s, чи корисний би мені окремий метод, ніж просто перепризначення, оскільки перепризначення може випадково втратити чинність 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
. Це означає, що в кінцевому рахунку я взагалі не перезаписую властивість і перегляну мою конструкцію з кореня.
Якщо ви до цього встигли - дякую, що прогулялися зі мною. Це була весела маленька вправа.