Python · Синтаксис · Продвинутый уровень
Декораторы методов классов ООП
Освоение @classmethod, @staticmethod, @property, @property.setter и @property.deleter. Когда каждый уместен и как они взаимодействуют с наследованием.
Краткое введение в тему и пояснения перед упражнениями (упражнения ниже):
@property, @setter, @deleter — паттерны и подводные камни
#Таблица сравнения, ошибки и примеры из stdlib
#Протокол дескрипторов: как @property работает под капотом
#Упражнения:
@classmethod: альтернативный конструктор
#class Person:
def __init__(self, name, age):
self.name = name
self.age = age
@classmethod
def from_string(cls, s):
# разберите 'Alice,30' и верните cls(...)
pass
p = Person.from_string('Alice,30')
print(p.name) # Alice
print(p.age) # 30
Решение
class Person:
def __init__(self, name, age):
self.name = name
self.age = int(age)
@classmethod
def from_string(cls, s):
name, age = s.split(',')
return cls(name.strip(), int(age.strip()))
p = Person.from_string('Alice,30')
print(p.name) # Alice
print(p.age) # 30
@classmethod: фабрика с учётом подкласса
#class Animal:
def __init__(self, name):
self.name = name
@classmethod
def create(cls, name):
# верните экземпляр cls
pass
def __repr__(self):
return f'{type(self).__name__}({self.name!r})'
class Dog(Animal): pass
class Cat(Animal): pass
print(Dog.create('Rex')) # Dog('Rex')
print(isinstance(Dog.create('Rex'), Dog)) # True
Решение
class Animal:
def __init__(self, name):
self.name = name
@classmethod
def create(cls, name):
return cls(name)
def __repr__(self):
return f'{type(self).__name__}({self.name!r})'
class Dog(Animal): pass
class Cat(Animal): pass
print(Animal.create('Generic')) # Animal('Generic')
print(Dog.create('Rex')) # Dog('Rex')
print(Cat.create('Whiskers')) # Cat('Whiskers')
print(isinstance(Dog.create('Rex'), Dog)) # True
@staticmethod: вспомогательный метод
#class EmailValidator:
@staticmethod
def is_valid(email):
# True если email имеет один '@' и '.' после него
pass
print(EmailValidator.is_valid('[email protected]')) # True
print(EmailValidator.is_valid('no-at-sign')) # False
print(EmailValidator.is_valid('missing-dot@com')) # False
Решение
class EmailValidator:
@staticmethod
def is_valid(email):
parts = email.split('@')
if len(parts) != 2:
return False
return '.' in parts[1]
print(EmailValidator.is_valid('[email protected]')) # True
print(EmailValidator.is_valid('no-at-sign')) # False
print(EmailValidator.is_valid('missing-dot@com')) # False
@property: вычисляемый атрибут только для чтения
#class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
@property
def area(self):
# верните вычисленную площадь
pass
r = Rectangle(4, 5)
print(r.area) # 20
r.width = 10
print(r.area) # 50
Решение
class Rectangle:
def __init__(self, width, height):
self.width = width
self.height = height
@property
def area(self):
return self.width * self.height
r = Rectangle(4, 5)
print(r.area) # 20
r.width = 10
print(r.area) # 50
@property + @setter: атрибут с валидацией
#class Person:
def __init__(self, name, age):
self.name = name
self.age = age # проходит через сеттер
@property
def age(self):
return self._age
@age.setter
def age(self, value):
# валидация: int и >= 0
pass
p = Person('Alice', 30)
print(p.age) # 30
p.age = -1 # ValueError
Решение
class Person:
def __init__(self, name, age):
self.name = name
self.age = age
@property
def age(self):
return self._age
@age.setter
def age(self, value):
if not isinstance(value, int):
raise ValueError(f'age must be int, got {type(value).__name__}')
if value < 0:
raise ValueError('age must be >= 0')
self._age = value
p = Person('Alice', 30)
print(p.age) # 30
try:
p.age = -1
except ValueError as e:
print(e) # age must be >= 0
@property.deleter: очистка при del
#class Session:
def __init__(self, token):
self._token = token
@property
def token(self):
if self._token is None:
raise AttributeError('Session has no active token')
return self._token
@token.deleter
def token(self):
# отзовите: установите None и выведите сообщение
pass
s = Session('abc123')
print(s.token) # abc123
del s.token # Token revoked
print(s.token) # AttributeError
Решение
class Session:
def __init__(self, token):
self._token = token
@property
def token(self):
if self._token is None:
raise AttributeError('Session has no active token')
return self._token
@token.deleter
def token(self):
self._token = None
print('Token revoked')
s = Session('abc123')
print(s.token) # abc123
del s.token # Token revoked
try:
print(s.token)
except AttributeError as e:
print(e)
Класс Temperature: @property с конвертацией единиц
#class Temperature:
def __init__(self, celsius):
self.celsius = celsius
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
# валидация >= -273.15
pass
@property
def fahrenheit(self):
# конвертируйте в Фаренгейт
pass
@classmethod
def from_fahrenheit(cls, f):
# конвертируйте и верните cls(...)
pass
t = Temperature(100)
print(t.celsius) # 100
print(t.fahrenheit) # 212.0
t2 = Temperature.from_fahrenheit(32)
print(t2.celsius) # 0.0
Решение
class Temperature:
ABSOLUTE_ZERO = -273.15
def __init__(self, celsius):
self.celsius = celsius
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value < self.ABSOLUTE_ZERO:
raise ValueError(f'Temperature below absolute zero: {value}')
self._celsius = value
@property
def fahrenheit(self):
return self._celsius * 9 / 5 + 32
@classmethod
def from_fahrenheit(cls, f):
return cls((f - 32) * 5 / 9)
t = Temperature(100)
print(t.celsius) # 100
print(t.fahrenheit) # 212.0
t2 = Temperature.from_fahrenheit(32)
print(t2.celsius) # 0.0
@property: ленивое кэширование
#class Document:
def __init__(self, text):
self.text = text
self._words = None
@property
def words(self):
# вычислите только если не кэшировано
pass
doc = Document('hello world foo bar')
print(doc.words) # ['hello', 'world', 'foo', 'bar'] (вычислено)
print(doc.words) # ['hello', 'world', 'foo', 'bar'] (из кэша)
Решение
class Document:
def __init__(self, text):
self.text = text
self._words = None
@property
def words(self):
if self._words is None:
print('Computing words...')
self._words = self.text.split()
return self._words
doc = Document('hello world foo bar')
print(doc.words) # Computing words... ['hello', 'world', 'foo', 'bar']
print(doc.words) # ['hello', 'world', 'foo', 'bar'] (без 'Computing')
@staticmethod vs @classmethod: когда что использовать
#class MathUtils:
def __init__(self, values):
self.values = values
@staticmethod
def add(a, b):
# чистое вычисление
pass
@classmethod
def zeros(cls, n):
# создайте экземпляр с [0] * n
pass
print(MathUtils.add(2, 3)) # 5
m = MathUtils.zeros(4)
print(m.values) # [0, 0, 0, 0]
Решение
class MathUtils:
def __init__(self, values):
self.values = values
@staticmethod
def add(a, b):
return a + b
@classmethod
def zeros(cls, n):
return cls([0] * n)
print(MathUtils.add(2, 3)) # 5
m = MathUtils.zeros(4)
print(m.values) # [0, 0, 0, 0]
print(m.add(10, 20)) # 30
BankAccount: все четыре декоратора вместе
#class BankAccount:
def __init__(self, owner, balance):
self.owner = owner
self._balance = balance
@classmethod
def open(cls, owner, initial=0):
pass
@staticmethod
def _validate_amount(amount):
pass
@property
def balance(self):
pass
@balance.deleter
def balance(self):
pass
def deposit(self, amount):
self._validate_amount(amount)
self._balance += amount
def withdraw(self, amount):
self._validate_amount(amount)
if amount > self._balance:
raise ValueError('Insufficient funds')
self._balance -= amount
acc = BankAccount.open('Alice', 100)
print(acc.balance) # 100
acc.deposit(50)
acc.withdraw(30)
del acc.balance
Решение
class BankAccount:
def __init__(self, owner, balance):
self.owner = owner
self._balance = balance
@classmethod
def open(cls, owner, initial=0):
if initial < 0:
raise ValueError('Initial balance cannot be negative')
return cls(owner, initial)
@staticmethod
def _validate_amount(amount):
if amount <= 0:
raise ValueError(f'Amount must be positive, got {amount}')
@property
def balance(self):
if self._balance is None:
raise AttributeError('Account is closed')
return self._balance
@balance.deleter
def balance(self):
print(f'Account of {self.owner} closed. Final balance: {self._balance}')
self._balance = None
def deposit(self, amount):
self._validate_amount(amount)
self._balance += amount
def withdraw(self, amount):
self._validate_amount(amount)
if amount > self._balance:
raise ValueError('Insufficient funds')
self._balance -= amount
acc = BankAccount.open('Alice', 100)
print(acc.balance) # 100
acc.deposit(50)
print(acc.balance) # 150
acc.withdraw(30)
print(acc.balance) # 120
del acc.balance