Шаг 137.
Python: сборник рецептов.
Классы и объекты. Расширение свойства в подклассе

    На этом шаге мы рассмотрим решение этой задачи.

Задача

    Внутри подкласса вы хотите расширить функциональность свойства, определенного в родительском классе.

Решение

    Рассмотрите следующий код, в котором определяется свойство:

>>> class Person:
    def __init__(self, name):
        self.name = name

    # Функция-геттер
    @property
    def name(self):
        return self._name

    # Функция-сеттер
    @name.setter
    def name(self, value):
        if not isinstance(value, str):
            raise TypeError('Expected a string')
        self._name = value

    # Функция-делитер
    @name.deleter
    def name(self):
        raise AttributeError("Can't delete attribute")

>>> 

    Вот пример класса, который наследует от Person и расширяет свойство name новой функциональностью:

>>> class SubPerson(Person):
    @property
    def name(self):
        print('Getting name')
        return super().name

    @name.setter
    def name(self, value):
        print('Setting name to', value)
        super(SubPerson, SubPerson).name.__set__(self, value)

    @name.deleter
    def name(self):
        print('Deleting name')
        super(SubPerson, SubPerson).name.__delete__(self)

>>> 

    Вот пример использования нового класса:

>>> s = SubPerson('Guido')
Setting name to Guido
>>> s.name
Getting name
'Guido'
>>> s.name = 'Larry'
Setting name to Larry
>>> s.name = 42
Setting name to 42
Traceback (most recent call last):
  File "<pyshell#42>", line 1, in <module>
    s.name = 42
  File "<pyshell#38>", line 10, in name
    super(SubPerson, SubPerson).name.__set__(self, value)
  File "<pyshell#36>", line 14, in name
    raise TypeError('Expected a string')
TypeError: Expected a string
>>> 

    Если вы хотите расширить только один из методов свойства, используйте такой код:

class SubPerson(Person):
    @Person.name.getter
    def name(self):
        print('Getting name')
        return super().name

    Или, альтернативно, только для сеттера, используйте такой код:

class SubPerson(Person):
    @Person.name.setter 
    def name(self, value):
        print('Setting name to', value)
        super(SubPerson, SubPerson).name.__set__(self, value)


Обсуждение

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

    В первом примере все методы свойства переопределяются вместе. В каждом методе используется super() для вызова предыдущей реализации. Использование super(SubPerson, SubPerson).name.__set__(self, value) в функции-сеттере - это не ошибка. Чтобы делегировать предыдущую реализацию сеттера, поток управления должен пройти через метод __set__() ранее определенного свойства name. Однако единственный способ получить этот метод - это доступ к нему как к переменной класса, а не как к переменной экземпляра. Это происходит в операции super(SubPerson, SubPerson).

    Если хотите переопределить только один из методов, недостаточно использовать @property. Например, вот такой код не работает:

>>> class SubPerson(Person):
	@property # Не работает
	def name(self):
		print('Getting name')
		return super().name


    Если вы попробуете использовать получившийся код, то обнаружите, что функция-сеттер полностью исчезла:

>>> s = SubPerson('Guido')
Traceback (most recent call last):
  File "<pyshell#46>", line 1, in <module>
    s = SubPerson('Guido')
  File "<pyshell#36>", line 3, in __init__
    self.name = name
AttributeError: can't set attribute
>>> 

    Вместо этого вы должны были изменить код так, как показано в решении:

>>> class SubPerson(Person):
	@Person.getter
	def name(self):
		print('Getting name')
		return super().name

    Когда вы это сделаете, все ранее определенные методы свойства будут скопированы, а функция-геттер заменена. Теперь все работает так, как ожидается:

>>> s = SubPerson('Guido')
>>> s.name 
Getting name 
'Guido'
>>> s.name = 'Larry'
>>> s.name 
Getting name 'Larry'
>>> s.name = 42
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "example.py", line 16, in name
raise TypeError('Expected a string')
TypeError: Expected a string 
>>>

    В этом конкретном решении нет способа заменить напрямую прописанное имя класса Person чем-то более общим. Если вы не знаете, в каком базовом классе определено свойство, то должны использовать решение, в котором все методы свойства переопределяются, а super() используется для передачи управления предыдущей реализации.

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

# Дескриптор
class String:
    def __init__(self, name):
        self.name = name
    def __get__(self, instance, cls):
        if instance is None:
            return self
        return instance.__dict__[self.name]

    def __set__(self, instance, value):
        if not isinstance(value, str):
            raise TypeError('Expected a string')
        instance.__dict__[self.name] = value

# Класс с дескриптором
class Person:
    name = String('name')
    def __init__(self, name):
        self.name = name

# Расширение дескриптора свойством
class SubPerson(Person):
    @property
    def name(self):
        print('Getting name')
        return super().name

    @name.setter
    def name(self, value):
        print('Setting name to', value)
        super(SubPerson, SubPerson).name.__set__(self, value)

    @name.deleter
    def name(self):
        print('Deleting name')
        super(SubPerson, SubPerson).name.__delete__(self)

    Наконец, стоит отметить, что к тому моменту, когда вы это прочитаете, расширение сеттеров и делитеров в подклассах может быть уже как-то упрощено.

    Показанное решение работает, но баг, описанный на странице багов Python, может быть исправлен путем реализации более ясного подхода в будущих версиях Python.


https://bugs.python.org/issue14965.

    На следующем шаге мы рассмотрим создание нового типа атрибута класса или экземпляра.




Предыдущий шаг Содержание