6. Dunder metody a dekorátory tříd

Dunder metody

Speciální metody sloužící k implementaci podpory pro vestavěné funkce Pythonu a jinou rozšiřující funkcionalitu (např. __init__ jakožto konstruktor). Přehled dunder metod je dostupný zde.

String reprezentace

Implementací dunder metod __repr__ a __str__ implementujeme chování funkcí repr() a str()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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 self.__repr__()
1
2
3
4
5
6
7
8
9
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

1
2
3
4
5
__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__.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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 self.__repr__()
    
    def __bool__(self):
        return bool(self.balance)
    
    def __int__(self):
        return int(self.balance)
1
2
3
4
5
6
7
8
9
10
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

1
2
3
__abs__ # chování funkce abs()
__neg__ # chování unárního minus
__pos__ # chování unárního plus

Porovnávání

1
2
3
4
5
6
__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__.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
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 self.__repr__()
    
    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
1
2
3
4
5
6
7
8
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

1
2
3
4
5
6
7
8
9
__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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
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 self.__repr__()
    
    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
1
2
3
4
5
6
7
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__.

1
2
3
4
5
6
7
8
__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.

1
2
3
4
5
6
7
__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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
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 self.__repr__()
    
    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
1
2
3
4
5
6
7
8
9
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

1
2
3
4
5
6
__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.

1
2
3
4
5
6
__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
__contains__ # chování in

Použití implementovaných dunder metod

Implementovanou funkcionalitu můžeme použít rovněž v rámci třídy.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
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 self.__repr__()
    
    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
1
2
3
4
5
6
7
8
9
10
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

Vestavěné funkce

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(), rovněž jsem se setkal s implementace dunder metody __abs__() a následné používání funkce abs().

Dekorátory ve třídách

@property

V jazyce Python nepoužíváme klasické (například Javovské) gettery, settery (tedy metody s názvy get_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. Vlastnost CreditAccount.balance je dostupná pomoci tečkového operátoru.

1
2
3
4
5
6
7
8
9
10
11
12
13
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
1
2
3
4
5
6
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?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# š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
1
2
3
4
5
6
7
8
9
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říklady kdy chceme modifikovat chování přístupu k vlastnosti třídy je nutné použít dekorátor @property.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
# 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
1
2
3
4
5
6
7
8
9
10
11
12
13
14
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

@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.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
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))
1
2
3
4
5
6
7
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ě.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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
1
2
3
4
5
6
7
8
9
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)

Pořadí definic metod

Neexistuje žádný jeden správný způsob v jakém pořadí metody třídy definovat. Populární možnost je následující:

1
2
3
4
5
6
7
8
9
10
class MyClass:
    # Dunder metohods
    
    # @staticmethod a @classmethod

    # @property

    # _private_method(self)

    # public_method(self)

Úkoly

Nevíte si rady? Přečtěte si “Jak pracovat s Github Classroom?”.