5. Dunder metody a dekorátory tříd
Obsah
Dunder metody
Dunder metoda (zkráceno z "double underscore method") v Pythonu je speciální metoda, která má dvě podtržítka na začátku a na konci svého názvu, například __init__, __str__ nebo __len__. Tyto metody se také nazývají "magic methods" nebo "special methods", protože poskytují speciální funkce pro práci s objekty a třídami. Přehled dunder metod je dostupný zde.
Dunder metody se volají automaticky při použití určitých operací (např. při volání +, ==, funkce len() nebo přístupu přes index). Implementací těchto metod můžete přizpůsobit chování vlastních objektů pro specifické potřeby. Dunder metody, až na výjimky (__init__() v kombinaci se super()), nevoláme explicitně, proto místo list.__len__() napíšeme raději len(list).
Dunder metody představují elegantní řešení pro implementaci podpory vestavěných funkcí. Většinou je tedy lepší využívat implementace těchto metod pro podporu len(), než implementování vlastní metody object.length(). Protipříkladem je pomyslná třída Vector ve které chceme implementovat výpočet délky vektoru. Pozor, použití len() na instanci třídy Vector není vhodné, funkci len() používáme v kontextu kolekcí pro zjištění počtu prvků. U třídy Vector požadujeme jinou sémantiku. V takovém případě je tedy vhodnější zvolit implementaci metody Vector.length().
String reprezentace
Implementací dunder metod __repr__ a __str__ implementujeme chování funkcí repr() a str()
class CreditAccount:
"""Account with stored credits."""
def __init__(self, owner, initial_credits=0):
"""Creates credit account with given owner and initial credits.
Args:
owner: owner of the account
initial_credits (optional): credit balance. Defaults to 0.
"""
self.owner = owner
self.balance = initial_credits
def __repr__(self):
return f"CreditAccount({self.owner}, {self.balance})"
def __str__(self):
return repr(self)
from credit_account import CreditAccount
credit_account_1 = CreditAccount("Lukas Novak")
credit_account_2 = CreditAccount("Pepa Novak", initial_credits=200)
credit_account_1
print(credit_account_1)
repr(credit_account_1)
Převod na jiné datové typy
__bool__ # chování funkce bool()
__complex__ # chování funkce complex()
__int__ # chování funkce int()
__float__ # chování funkce float()
__hash__ # chování funkce hash()
Následující příklad dále implementuje dunder metody __bool__ a __int__.
class CreditAccount:
"""Account with stored credits."""
def __init__(self, owner, initial_credits=0):
"""Creates credit account with given owner and initial credits.
Args:
owner: owner of the account
initial_credits (optional): credit balance. Defaults to 0.
"""
self.owner = owner
self.balance = initial_credits
def __repr__(self):
return f"CreditAccount({self.owner}, {self.balance})"
def __str__(self):
return repr(self)
def __bool__(self):
return bool(self.balance)
def __int__(self):
return int(self.balance)
from credit_account import CreditAccount
credit_account_1 = CreditAccount("Lukas Novak")
credit_account_2 = CreditAccount("Pepa Novak", initial_credits=200)
assert bool(credit_account_1) == False
assert bool(credit_account_2) == True
assert int(credit_account_1) == 0
Unární číselné operátory
__abs__ # chování funkce abs()
__neg__ # chování unárního minus
__pos__ # chování unárního plus
Porovnávání
__lt__ # chování <
__le__ # chování <=
__eq__ # chování ==
__ne__ # chování !=
__gt__ # chování >
__ge__ # chování >=
Následující příklad dále implementuje dunder metodu __lt__.
class CreditAccount:
"""Account with stored credits."""
def __init__(self, owner, initial_credits=0):
"""Creates credit account with given owner and initial credits.
Args:
owner: owner of the account
initial_credits (optional): credit balance. Defaults to 0.
"""
self.owner = owner
self.balance = initial_credits
def __repr__(self):
return f"CreditAccount({self.owner}, {self.balance})"
def __str__(self):
return repr(self)
def __bool__(self):
return bool(self.balance)
def __int__(self):
return int(self.balance)
def __lt__(self, other):
if isinstance(other, CreditAccount):
return self.balance < other.balance
return NotImplemented
from credit_account import CreditAccount
credit_account_1 = CreditAccount("Lukas Novak")
credit_account_2 = CreditAccount("Pepa Novak", initial_credits=200)
credit_account_1 < credit_account_2
credit_account_2 < credit_account_1
Aritmetické operátory
__add__ # chování +
__sub__ # chování -
__mul__ # chování *
__truediv__ # chování /
__floordiv__ # chování //
__mod__ # chování %
__divmod__ # chování divmod()
__pow__ # chování ** nebo pow()
__round__ # chování round()
Následující příklad dále implementuje dunder metodu __add__. V případě, že pro nějaký typ nejsou dunder metody implementovány, je nutné vracet konstantu NotImplemented.
class CreditAccount:
"""Account with stored credits."""
def __init__(self, owner, initial_credits=0):
"""Creates credit account with given owner and initial credits.
Args:
owner: owner of the account
initial_credits (optional): credit balance. Defaults to 0.
"""
self.owner = owner
self.balance = initial_credits
def __repr__(self):
return f"CreditAccount({self.owner}, {self.balance})"
def __str__(self):
return repr(self)
def __bool__(self):
return bool(self.balance)
def __int__(self):
return int(self.balance)
def __lt__(self, other):
if isinstance(other, CreditAccount):
return self.balance < other.balance
return NotImplemented
def __add__(self, other):
if isinstance(other, CreditAccount):
return self.balance + other.balance
return NotImplemented
from credit_account import CreditAccount
credit_account_1 = CreditAccount("Lukas Novak")
credit_account_2 = CreditAccount("Pepa Novak", initial_credits=200)
credit_account_1 + credit_account_2
Aritmetické operátory je možné implementovat pro "oba směry". V situaci kdy vyhodnocujeme x + y Python hledá implementaci x.__add__ nebo y.__radd__.
__radd__
__rsub__
__rmul__
__rtruediv__
__rfloordiv__
__rmod__
__rdivmod__
__rpow__
Kombinované operátory přiřazení s aritmetickými operacemi
Je běžné, ne však nutné, aby výsledná metoda vracela self.
__iadd__ # chování +=
__isub__ # chování -=
__imul__ # chování *=
__itruediv__ # chování /=
__ifloordiv__ # chování //=
__imod__ # chování %=
__ipow__ # chování **=
Následující příklad dále implementuje dunder metodu __iadd__. V případě, že pro nějaký typ nejsou dunder metody implementovány, je nutné vracet konstantu NotImplemented.
class CreditAccount:
"""Account with stored credits."""
def __init__(self, owner, initial_credits=0):
"""Creates credit account with given owner and initial credits.
Args:
owner: owner of the account
initial_credits (optional): credit balance. Defaults to 0.
"""
self.owner = owner
self.balance = initial_credits
def __repr__(self):
return f"CreditAccount({self.owner}, {self.balance})"
def __str__(self):
return repr(self)
def __bool__(self):
return bool(self.balance)
def __int__(self):
return int(self.balance)
def __lt__(self, other):
if isinstance(other, CreditAccount):
return self.balance < other.balance
return NotImplemented
def __add__(self, other):
if isinstance(other, CreditAccount):
return self.balance + other.balance
return NotImplemented
def __iadd__(self, value):
if isinstance(value, int):
self.balance += value
return self
return NotImplemented
from credit_account import CreditAccount
credit_account_1 = CreditAccount("Lukas Novak")
credit_account_2 = CreditAccount("Pepa Novak", initial_credits=200)
credit_account_1 += 200
credit_account_1 += credit_account_2
Bitové operátory
__invert__ # chování ~
__lshift__ # chování <<
__rshift__ # chování >>
__and__ # chování &
__or__ # chování |
__xor__ # chování ^
Emulace kolekcí
Důležité, dostaneme se k nim později.
__index__ # chování převodu na integer například při slicingu
__len__ # chování len()
__getitem__ # chování x[20]
__setitem__ # chování x[20] = 2
__delitem__ # chování del x[20]
__contains__ # chování in
Použití implementovaných dunder metod
Implementovanou funkcionalitu můžeme použít rovněž v rámci třídy.
class CreditAccount:
"""Account with stored credits."""
def __init__(self, owner, initial_credits=0):
"""Creates credit account with given owner and initial credits.
Args:
owner: owner of the account
initial_credits (optional): credit balance. Defaults to 0.
"""
self.owner = owner
self.balance = initial_credits
def __repr__(self):
return f"CreditAccount({self.owner}, {self.balance})"
def __str__(self):
return repr(self)
def __bool__(self):
return bool(self.balance)
def __int__(self):
return int(self.balance)
def __lt__(self, other):
if isinstance(other, CreditAccount):
return self.balance < other.balance
return NotImplemented
def __add__(self, other):
if isinstance(other, CreditAccount):
return self.balance + other.balance
return NotImplemented
def __iadd__(self, value):
if isinstance(value, int):
self.balance += value
return self
return NotImplemented
def __isub__(self, value):
if isinstance(value, int):
self.balance -= value
return self
return NotImplemented
def transfer_to(self, other, value):
"""Transfer credit into another credit account. Negative balance is allowed.
Args:
other: Target of credit transfer.
value: Amount of credit to be transfered.
"""
self -= value
other += value
from credit_account import CreditAccount
credit_account_1 = CreditAccount("Lukas Novak")
credit_account_2 = CreditAccount("Pepa Novak", initial_credits=200)
credit_account_1.transfer_to(credit_account_2, 200)
credit_account_1
credit_account_2
Příklad: dekorátor pomocí třídy
S využitím dunder metody __call__, která umožní objektu být zavolán jako funkce (takový objekt je tzv. callable), můžeme implementovat dekorátory funkcí jako třídy.
import functools
class CountCalls:
"""
A decorator class that counts and logs the number of times a function is called,
while preserving the original function's documentation and signature.
Args:
start (int): The initial value for the call counter.
"""
def __init__(self, start=0):
self.start = start
def __call__(self, func):
"""
Decorate the given function to count calls.
Args:
func (callable): The function to decorate.
Returns:
callable: The wrapped function with call counting.
"""
self.num_calls = self.start
self.func = func
def wrapper(*args, **kwargs):
self.num_calls += 1
print(f"Call {self.num_calls} of {self.func.__name__}()")
return self.func(*args, **kwargs)
# zachováme identitu původní funkce.
functools.update_wrapper(wrapper, func)
return wrapper
@CountCalls()
def hello():
"""Says 'Hello!'."""
print("Hello!")
hello()
# Call 1 of hello()
# Hello!
hello()
# Call 2 of hello()
# Hello!
print(hello.__name__)
# hello
print(hello.__doc__)
# "Says 'Hello!'"
Dekorátory ve třídách
@property
V jazyce Python nepoužíváme klasické (například Javovské) gettery, settery (tedy metody s názvyget_temperature a set_temperature). To plyne z vlastnosti veřejné dostupnosti všech hodnot objektu (narozdíl od jazyků jako je třeba Java). Vraťme se k jednoduchému příkladu třídy CreditAccount. Atribut CreditAccount.balance je dostupný pomocí tečkového operátoru.
class CreditAccount:
"""Account with stored credits."""
def __init__(self, owner, initial_credits=0):
"""Creates credit account with given owner and initial credits.
Args:
owner: owner of the account
initial_credits (optional): credit balance. Defaults to 0.
"""
self.owner = owner
self.balance = initial_credits
from credit_account import CreditAccount
credit_account = CreditAccount("Lukas Novak")
credit_account.balance
Co můžeme dělat pokud chceme například ověřovat, že balance nemůže být nastavena na zápornou hodnotu?
# špatné, ale bohužel časté řešení
class CreditAccount:
"""Account with stored credits."""
def __init__(self, owner, initial_credits=0):
"""Creates credit account with given owner and initial credits.
Args:
owner: owner of the account
initial_credits (optional): credit balance. Defaults to 0.
"""
self.owner = owner
self.balance = 0
self.set_balance(initial_credits)
def set_balance(self, new_balance):
if new_balance < 0:
raise ValueError("Balance cannot be negative number!")
self.balance = new_balance
from credit_account import CreditAccount
credit_account = CreditAccount("Lukas Novak", 0)
credit_account.set_balance(0)
# negativní hodnotu balance můžeme stále nastavit
credit_account.balance = -100
Pro případy, kdy chceme modifikovat chování přístupu k atributu třídy, je nutné použít dekorátor @property, čímž z něj uděláme vlastnost. Místo atributu balance definujeme pro uchování hodnoty atribut _balance, který začíná podtržítkem, a tedy dle konvence jde o interní atribut, který by uživatel objektu neměl sám měnit.
Pokud definujeme @property bez setteru, vytvoříme tím vlastnost, jejíž hodnotu uživatel nemůže změnit, čímž lze simulovat privátní atribut. Při pokusu o změnu takové vlastnosti totiž nastane AttributeError.
# správně
class CreditAccount:
"""Account with stored credits."""
def __init__(self, owner, initial_credits=0):
"""Creates credit account with given owner and initial credits.
Args:
owner: owner of the account
initial_credits (optional): credit balance. Defaults to 0.
"""
self._owner = owner
self._balance = 0
self.balance = initial_credits
@property
def balance(self):
return self._balance
@balance.setter
def balance(self, new_balance):
"""Sets new value of balance, new value cannot be negative."""
if new_balance < 0:
raise ValueError("Balance cannot be negative number!")
self._balance = new_balance
@balance.deleter
def balance(self):
self._balance = 0
@property
def owner(self):
return self._owner
from credit_account import CreditAccount
credit_account = CreditAccount("Lukas Novak", 0)
credit_account.balance = 10
# volani deleteru
del credit_account.balance
credit_account.balance
# volani setteru
credit_account.balance = -100
# ValueError: Balance cannot be negative number!
# pokus o zmenu vlastnosti bez setteru
credit_account.owner = "Pavel Novák"
# AttributeError: property 'owner' of 'CreditAccount' object has no setter
# bohužel, pořád bude fungovat
credit_account._balance = -100
credit_account._owner = "Pavel Novák"
@classmethod vs @staticmethod
Nejprve se podívejme na dekorátor @classmethod. Tento dekorátor lze použít v situaci kdy je nutné metodám předat odkaz na celou třídu. Demonstrovat jej můžeme na příkladu metod from_*, tedy metod které umí vytvořit instanci třídy různými způsoby.
class CreditAccount:
"""Account with stored credits."""
def __init__(self, owner, initial_credits=0):
"""Creates credit account with given owner and initial credits.
Args:
owner: owner of the account
initial_credits (optional): credit balance. Defaults to 0.
"""
self.owner = owner
self.balance = initial_credits
@classmethod
def from_csv(cls, input_string, separator=","):
"""Creates CreditAccount class from csv string"""
owner, initial_credits = input_string.split(separator)
return cls(owner, int(initial_credits))
from credit_account import CreditAccount
credit_account = CreditAccount.from_csv("Lukas Novak,200")
credit_account.owner
credit_account.balance
Speciální dekorátor @staticmethod naopak nevyžaduje (a nemá) přístup ke své třídě.
class CreditAccount:
"""Account with stored credits."""
def __init__(self, owner, initial_credits=0):
"""Creates credit account with given owner and initial credits.
Args:
owner: owner of the account
initial_credits (optional): credit balance. Defaults to 0.
"""
self.owner = owner
self.balance = initial_credits
@staticmethod
def credit_to_money(credit, exchange_rate):
"""Calculates money value of credits.
Args:
credit: amount of credits
exchange_rate: how many money per one credit
Returns: money value
"""
return credit * exchange_rate
from credit_account import CreditAccount
# dostupné z třídy
CreditAccount.credit_to_money(100, 20)
credit_account = CreditAccount("Lukas Novak", 200)
# dostupné rovněž z objektu
credit_account.credit_to_money(100, 20)
Příklad: dekorátor třídy
Dekorovat můžeme i samotné třídy - tento dekorátor například vytvoří třídu, ke které lze vytvořit jen jednu instanci.
# příklad převzat z: https://realpython.com/
import functools
def singleton(cls):
"""Make a class a Singleton class (only one instance)"""
singleton_instance = None
@functools.wraps(cls)
def wrapper_singleton(*args, **kwargs):
nonlocal singleton_instance
if singleton_instance is None:
singleton_instance = cls(*args, **kwargs)
return singleton_instance
return wrapper_singleton
@singleton
class TheOne:
pass
first_one = TheOne()
another_one = TheOne()
id(first_one)
# 140094218762310
id(another_one)
# 140094218762310
first_one is another_one
# True
@dataclass
Pro rychlé vytvoření třídy lze použít dekorátor tříd @dataclass.
from dataclasses import dataclass
@dataclass
class CreditAccount:
"""Account with stored credits."""
owner: str
balance: int = 0
Dekorátor @dataclass přidá automaticky konstruktor:
def __init__(self, owner: str, balance: int = 0):
self.owner = owner
self.balance = balance
Dekorátor @dataclass je mnohem komplexnější, celkový popis je možné nalézt zde. Pravděpodobně nás zaskočilo, že uvádíme datový typ u jednotlivých atributů. Jedná se o nápovědu typování, o této možnosti v jazyku Python si řekneme v budoucnu.
Pořadí definic metod
Neexistuje žádný jeden správný způsob v jakém pořadí metody třídy definovat, důležitá je spíše konzistence (chceme udržet metody stejného typu u sebe, nebo slučovat metody). Populární možnost je následující:
class MyClass:
# Initialization method __init__ (and __new__, __del__ if needed)
# Representation methods __repr__ and __string__
# Dunder methods
# @staticmethod a @classmethod
# @property
# _private_method(self)
# public_method(self)