Підкласифікація: Чи можливо замінити властивість звичайним атрибутом?


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

Я усвідомлюю, що існують способи змінити властивість із властивістю, але я хотів би цього уникнути. Оскільки мета базового класу - зробити життя максимально простим для свого користувача, а не додавати розквіт, накладаючи (з вузької точки зору підкласу) перекручений і зайвий метод доступу.

Чи можна це зробити? Якщо ні, то наступне найкраще рішення?

Відповіді:


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

Однак це передбачає, що для внесення цих змін у вас є доступ до базового класу.


Це досить близько до ідеального. Отже, це означає властивість, навіть якщо ви лише визначите, що геттер неявно додає сеттер, який не робить нічого, крім блокування призначення? Цікаво. Я, мабуть, прийму цю відповідь.
Пол Панцер

Так, в відповідно до Документами, властивість завжди визначає всі три методи дескриптора ( __get__(), __set__()і __delete__()) , і вони піднімають , AttributeErrorякщо ви не надасте яку - небудь функцію для них. Див. Еквівалент реалізації власності Python .
Аркеліс

Поки ви не піклуєтесь про майно setterчи __set__майно, це працюватиме. Однак я застережу вас, що це також означає, що ви можете легко перезаписати властивість не лише на рівні класу ( self.size = ...), але навіть на рівні екземпляра (наприклад, Concrete_Math_Set(1, 2, 3).size = 10було б однаково справедливо). Їжа для думок :)
r.ook

І Square_Integers_Below(9).size = 1також діє, оскільки це простий атрибут. У цьому конкретному випадку використання "розміру" це може здатися аквардним (замість цього використовувати властивість), але в загальному випадку "легко переосмислити обчислювальну опору в підкласі", це може бути добре в деяких випадках. Ви також можете керувати доступом до атрибутів, __setattr__()але це може бути непосильним.
Аркеліс

1
Я не погоджуюся, що для цих випадків може бути дійсним використання, я просто хотів зазначити застереження, оскільки це може ускладнити зусилля налагодження в майбутньому. Поки ви і ОП знаєте про наслідки, тоді все добре.
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. Якщо атрибут класу існує, і він є a 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'

Та-да! 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. Якщо ви також хочете, щоб ці методи могли досягти іншої мети, ви просто просите неприємностей. Я б, можливо, переосмислив підхід:

  1. Мені справді потрібні propertyдля цього?
  2. Чи може метод мені служити інакше?
  3. Якщо мені потрібен a property, чи є якась причина, мені потрібно було б його перезаписати?
  4. Чи справді підклас належить до однієї сім'ї, якщо вони propertyне застосовуються?
  5. Якщо мені все-таки потрібно перезаписати будь-який / 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. Це означає, що в кінцевому рахунку я взагалі не перезаписую властивість і перегляну мою конструкцію з кореня.

Якщо ви до цього встигли - дякую, що прогулялися зі мною. Це була весела маленька вправа.


2
Вау, спасибі велике! Для того, щоб переварити це, знадобиться трохи часу, але, мабуть, я просив цього.
Пол Панцер

6

A @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__? Також, чи є способи автоматизувати це? Так, це лише один рядок, але це та річ, яку дуже чітко читати, якщо ви не знайомі з деталями горішних деталей тощо.
Paul Panzer

@PaulPanzer Я не розглядав процедури, що стоїть за цим, тому я не міг дати тобі задовільну відповідь. Ви можете спробувати розгадати його з вихідного коду cpython, якщо хочете. Щодо автоматизації процесу, я не думаю, що існує хороший спосіб зробити це, окрім того, щоб або не зробити його властивістю в першу чергу, або просто додати коментар / docstring, щоб ті, хто читає ваш код, знали, що ви робите . Щось на кшталт# declare size to not be a @property
Зелений плащ Гай

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

Це чудовий момент, просто замініть батьківську власність із дочірнім майном! +1
r.ook

Це певна річ, яку я маю зробити прямо, але мене зацікавило і запитало конкретно про способи, що дозволяють переохочувати невластивість, щоб не наносити незручності необхідності писати властивість, що запам'ятовується, де просте призначення буде робити робота.
Пол Панцер

Я не впевнений, чому ви хочете так ускладнити свій код. За невеликої ціни визнання того, що sizeце властивість, а не атрибут екземпляра, не потрібно робити нічого фантазійного.
чепнер

Мій випадок використання - це багато дитячих класів з великою кількістю атрибутів; відсутність необхідності писати 5 рядків замість одного для всіх, виправдовує певну кількість фантазії, IMO.
Пол Панцер

2

Схоже, ви хочете визначитись sizeу своєму класі:

class Square_Integers_Below(Math_Set_Base):
    size = None

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

Інший варіант - збереження capу вашому класі та обчислення його із sizeвизначеним властивістю (яке переосмислює властивість базового класу size).


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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.