Подклассы: возможно ли переопределить свойство обычным атрибутом? - PullRequest
16 голосов
/ 19 апреля 2020

Давайте предположим, что мы хотим создать семейство классов, которые являются различными реализациями или специализациями всеобъемлющей концепции. Давайте предположим, что существует правдоподобная реализация по умолчанию для некоторых производных свойств. Мы хотели бы поместить это в базовый класс

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

Я понимаю, что есть способы переопределить свойство свойством, но я бы хотел этого избежать. Поскольку цель базового класса состоит в том, чтобы максимально упростить жизнь его пользователю, а не добавлять вздор путем наложения (с узкой точки зрения подкласса) сложного и излишнего метода доступа.

Может ли это быть сделанный? Если нет, каково следующее лучшее решение?

Ответы [ 8 ]

11 голосов
/ 26 апреля 2020

Это будет многословный ответ, который может послужить лишь дополнительным ... но ваш вопрос заставил меня прокатиться по кроличьей норе, поэтому я хотел бы также поделиться своими выводами (и болью).

Возможно, в конечном итоге этот ответ окажется бесполезным для вашей реальной проблемы. На самом деле, мой вывод таков: я бы этого не делал вообще. Сказав это, фон к этому выводу может вас немного развлечь, так как вы ищете более подробную информацию.


Устранение некоторого заблуждения

Первый ответ, хотя и правильный в большинстве случаи, это не всегда дело. Например, рассмотрим этот класс:

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>

Обратите внимание, как:

  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. Если атрибут класса существует и , то это property, получить его значение через getter или fget (подробнее об этом позже). Текущий класс первым до базового класса последним.
  2. В противном случае, если атрибут экземпляра существует, получить значение атрибута экземпляра.
  3. В противном случае получите атрибут класса не 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. Если вы также хотите, чтобы эти методы достигли другой цели, вы просто напрашиваетесь на неприятности. Возможно, я бы переосмыслил подход:

  1. Мне действительно нужен property для этого?
  2. Может ли метод служить мне по-другому?
  3. Если мне нужен property, есть ли какая-то причина, по которой мне нужно его перезаписать?
  4. Подкласс действительно принадлежит одному и тому же семейству, если эти property не применяются?
  5. Если мне нужно перезаписать какие-либо / все 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.

Если вы сделали это так далеко - спасибо за то, что прошли это путешествие со мной. Это было забавное маленькое упражнение.

9 голосов
/ 29 апреля 2020

Свойство - это дескриптор данных, который имеет приоритет над атрибутом экземпляра с тем же именем. Вы можете определить дескриптор без данных с уникальным методом __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

Однако это предполагается, что у вас есть доступ к базовому классу для внесения этих изменений.

7 голосов
/ 19 апреля 2020

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 обычно использует некоторые махинации для управления этим процессом (т. Е. Применяя методы получения и установки), но когда имя уровня класса не определено как свойство, ничего особенного не происходит, и поэтому он действует так, как вы ожидаете от любого другая переменная.

6 голосов
/ 28 апреля 2020

Вам вообще не нужно присваивать (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)))

Вы можете (микро) оптимизировать его, предварительно вычислив квадрат root:

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
2 голосов
/ 28 апреля 2020

Я предлагаю добавить установщик следующим образом:

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
2 голосов
/ 26 апреля 2020

Похоже, вы хотите определить size в своем классе:

class Square_Integers_Below(Math_Set_Base):
    size = None

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

Другой вариант - сохранить cap в вашем классе и вычислить его с size, определенным как свойство (которое переопределяет свойство базового класса size).

1 голос
/ 27 апреля 2020

Также вы можете сделать следующее

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))
0 голосов
/ 04 мая 2020

Я думаю, что можно установить свойство базового класса для другого свойства из производного класса внутри производного класса, а затем использовать свойство базового класса с новым значением.

В в исходном коде существует своего рода конфликт между именем size из базового класса и атрибутом self.size из производного класса. Это может быть видно, если мы заменим имя self.size из производного класса на self.length. Это выведет:

3
<__main__.Square_Integers_Below object at 0x000001BCD56B6080>

Затем, если мы заменим имя метода size на length во всех вхождениях в программе, это приведет к тому же исключению:

Traceback (most recent call last):
  File "C:/Users/Maria/Downloads/so_1.2.py", line 24, in <module>
    Square_Integers_Below(7)
  File "C:/Users/Maria/Downloads/so_1.2.py", line 21, in __init__
    self.length = int(math.sqrt(cap))
AttributeError: can't set attribute

Фиксированный код, или в любом случае версия, которая каким-то образом работает, состоит в том, чтобы сохранить код точно таким же, за исключением класса Square_Integers_Below, который установит метод size из базового класса в другое значение.

class Square_Integers_Below(Math_Set_Base):

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

    def __repr__(self):
        return str(self.size)

И затем, когда мы запустим всю программу, получится:

3
2

Надеюсь, это так или иначе помогло.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...