На этом шаге мы рассмотрим решение этой задачи.
Внутри подкласса вы хотите расширить функциональность свойства, определенного в родительском классе.
Рассмотрите следующий код, в котором определяется свойство:
>>> 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.
На следующем шаге мы рассмотрим создание нового типа атрибута класса или экземпляра.