prev up next title end end

10. Třídy a instance

  1. Objektově orientované programování
  2. Vytvoření třídy
    1. Minimum minimórum
    2. Konstruktor objektu třídy
  3. Objekty třídy
    1. Datové atributy
    2. Funkce a metody
  4. Příklad: obdélník
  5. Instance
    1. Instance jsou měnitelné
    2. Instance jako výstupní hodnoty
    3. Instance uvnitř f-stringu
  6. Kopírování třídy
  7. Iterátory definované třídou
  8. Dekorátory
    1. Vytvoření dekorátoru
    2. Klauzura s parametry
    3. Dekorátor s parametrem
    4. Dekorátor 'wraps'
    5. Dekorace funkce třídou
    6. Dekorace třídy funkcí
  9. Speciální metody a atributy
  10. Datové třídy
  11. Dědění
  12. Glosář
  13. Cvičení

10.1 Objektově orientované programování

Python je programovací jazyk, který podporuje objektově orientované programování (OOP), jehož princip lze shrnout do mantry Vše v Pythonu je objekt.
Slovem vše je míněno doslova vše: hodnoty, funkce, třídy, metody, dokonce i zdrojový kód, zvaný modul - viz kap. 7.5.

Prvotním generátorem objektů jsou třídy, z nichž jsou všechny objekty odvozeny coby instance. Třídy samotné jsou rovněž odvozené děděním ze základní (bázové) třídy, zvané base.
S řadou vestavěných tříd (str, int, float, ...), označovaných jako vestavěné typy, jsme se setkali již v počátečních kapitolách.

Termíny třída (class) a typ (type) jsou v Python3 v podstatě synonyma. V Python2 to byly dvě různé entity. Je obvyklé označovat vestavěné (built-in) třídy slovem typ.


10.2 Vytvoření třídy

10.2.1   Minimum minimórum

Nejjednodušší deklarace třídy vypadá takto:

class Emanuel:
    '''Nepovinný dokumentační řetězec - docstring '''
    pass

Záhlaví složené z klíčového slova class, názvu třídy a dvojtečky je vše, co jednoduchá třída potřebuje.
Později se dovíme, že třída může dědit ze své nadtřídy, v tom případě by před dvojtečkou byl název supertřídy v závorkách, například class Emanuel(Motýle). Pokud není supertřída uvedena, je supertřídou základní třída base.

Následuje odsazené tělo třídy, které bude obsahovat řadu dalších objektů. V naší demonstraci jsme uvedli příklad dokumentačního řetězce a jedno klíčové slovo pass, které nedělá nic. Pro platnost minimální definice postačí jedno z uvedeného. Definice třídy končí přechodem na (neodsazený) počátek nového řádku.

Dokumentační řetězec (docstring) slouží jako stručná informace o funkci, třídě i modulu; vyvoláme ji příkazem (zde pro třídu Emanuel):

>>> Emanuel.__doc__      
'Nepovinný dokumentační řetězec - docstring'

S naší prostinkou třídou můžeme už provádět řadu regulérních úkonů, například:

Definice třídy se mohou vyskytnout kdekoli v programu, ale obvykle se umisťují poblíž počátku (po příkazech import).

10.2.2   Konstruktor objektu třídy

Konstrukci třídy si dále vysvětlíme na konkretním případě třídy Point, kterou si vytvoříme pro manipulaci s bodem ve dvourozměrném prostoru.

Abychom při každé deklaraci objektu nemuseli vytvářet jeho datové atributy ručně, použijeme speciální metodu __init__, kterou přidáme na začátek třídy a pomocí níž potřebné atributy objektu zavedeme předem. Této metodě by se správně mělo říkat inicializátor ale je také často označována jako konstruktor.

 class Point:
     def __init__(self, x=0, y=0):        # konstruktor
         self.x = x
         self.y = y  

Prvním parametrem definice __init__ je povinné slovo self, které zastupuje dosud neexistující (ale předpokládanou) instanci třídy. Další parametry s počáteční hodnotou zajišťují, že každá nově vytvořená instance bude implicitně obsahovat datové atributy x, y, zde s počáteční nulovou hodnotou.

Polohu bodu v rovině určíme jeho souřadnicemi. V matematickém zápisu bodu se tyto souřadnice, oddělené čárkou, uvádějí v závorce. Například, (0, 0) představuje počátek souřadnic, (x, y) představuje bod ve vzdálenosti x ve vodorovném a y ve svislém směru od počátku.

Vytvoření nové instance připomíná volání funkce:

>>> p = Point()             # vytvoření objektu (instance třídy)

Užitečnost konstruktoru si ukážeme ve spojení s další metodou, která určí vzdálenost bodu od počátku souřadnic:

 class Point:
     def __init__(self, x=0, y=0 ):           # konstruktor 
         self.x = x
         self.y = y
     def distance_from_origin(self):          # metoda instance
         return(self.x**2 + self.y**2)**0.5		 

Při vytváření instance musíme zadat hodnotu atributů, deklarovaných v metodě __init__. Pokud jsou tyto atributy deklarovány s počátečními hodnotami, které nám vyhovují - potom nemusíme:

>>> p = Point()
>>> p.x, p.y
(0, 0)
Můžeme zajisté vytvořit objekt s vlastními hodnotami x,y:
>>> g = Point(3,4)
>>> g.x, g.y
(3, 4)
>>> g.distance_from_origin()
5.0
Pro zvídavé:

V popisované deklaraci třídy Point jsou uvedená jména nezávazná. Stejně dobře nám poslouží třída, deklarovaná takto:

 class Point:
     def __init__(nerv, x=0, y=0):        # konstruktor
         nerv.a = x
         nerv.b = y  

V naší 'odchylce' zastupuje slovo 'nerv' jméno budoucí možné instance. Jména 'x', 'y' jsou interní označení pro parametry metody __init__, které se coby atributy instance budou nazývat 'a', 'b'.

>>> p = Point()                            # nerv = p
>>> p.a, p.b                               # x, y = a, b     
(0, 0)

Objekt coby argument funkce

Instanci můžeme zadat jako parametr obvyklým způsobem:

def print_point(obj):
    print('%s, %s' % (obj.x, obj.y))
>>> print_point(g)
3, 4
>>> print_point(p)
0, 0

10.3 Objekty třídy

Kromě dokumentačního řetězce a klíčového slova "pass" může třída obsahovat další dva důležité objekty - datové a funkční.

Datovými objekty (atributy) jsou proměnné, funkčními objekty jsou metody, což jsou funkce, definované uvnitř třídy a volané pro instanci (objekt) tečkovou notací.

10.3.1   Datové atributy

Pro instanci i třídu lze určit atributy i dodatečně (jak jsme již viděli v předběžné ukázce):

>>> p.x = 3              # datový atribut 'x' instance 'p'  
>>> p.y = 4              # datový atribut 'y' instance 'p' 

>>> Point.a = "alef"     # datový atribut 'a' třídy Point

Tato skladba je podobná skladbě pro výběr atributu z modulu, např. math.pi nebo string.ascii_uppercase. Jak moduly, tak i třídy a instance vytvářejí své vlastní jmenné prostory. Přístup k jejich jménům (atributům) je v obou případech stejný - přes tečkovou notaci. V naší ukázce vytváříme atributy tím, že jim přiřazujeme hodnoty.

Ověřme si to:

>>> p.x, p.y
(3,4)
>>> Point.a
'alef'

Tečkovou notaci můžeme použít jako součást jakéhokoliv výrazu, takže následující příkazy jsou legální:

>>> print('%d  %d' % (p.x, p.y))
3 4
>>> print(p.x*p.x + p.y*p.y)
25

Stejně snadné jako vytvoření objektu a deklarace jeho atributů (případně atributů třídy), je jejich smazání příkazem del:

>>> del p.y
>>> del Point.a
>>> del p

10.3.2   Funkce a metody

Metoda je funkce, deklarovaná uvnitř třídy. Pro třídu v Pythonu se rozlišují tři metody: instanční, statická a metoda třídy:

  1. Instanční metoda definuje chování objektu (instance), nikoli třídy. Definice obsahuje parametr self. V odstavci 10.2.2 uvedená speciální metoda __init__ je eminentní metoda instanční.
    class Sample:                     deklarace třídy
        val = 10                      datový atribut třídy
        def myFunc(self):             metoda instance
            print("Value: ", self.val)
    
    >>> obj = Sample()                vytvoření instance
    >>> obj.myFunc()                  volání metody
    Value:  10
    
  2. Metoda třídy definuje chování celé třídy, nikoli objektu. Definice obsahuje parametr cls a je uvedena vestavěným dekorátorem @classmethod.
    Tato metoda má přístup k proměnné třídy a ke statické metodě, nemá přístup k datům instance.
    class Hample:
        ham = 20                       datový atribut třídy
    
        @classmethod                   dekorace
        def myFunc(cls):               metoda třídy
            print("Hodnota proměnné třídy: ", cls.ham)
            cls.otherFunc()            volání staticé metody
    
        @staticmethod                  dekorace
        def otherFunc():               statická metoda
            print("Výstup statické metody.")
    
    >>> Hample.myFunc()                volání metody třídy
    Hodnota proměnné třídy:  20        výstup metody třídy
    Výstup statické metody.            výstup statické metody
    
  3. Statická metoda je obyčejná funkce, deklarovaná uvnitř třídy. Deklarace je uvedena vestavěným dekorátorem @staticmethod (viz ad 2).
    Tato metoda má přístup pouze ke svým vlastním atributům. Obě metody lze volat i přes vytvořenou instanci:
  4. >>> hamp = Hample()
    >>> hamp.myFunc()
    Hodnota proměnné třídy:  20
    Výstup statické metody.
    >>> hamp.otherFunc()
    Výstup statické metody.
    

10.4 Příklad: obdélník

Řekněme, že chceme, aby třída představovala obdélník (rectangle). Otázkou je, jakou informaci musíme poskytnout, abychom určili obdélník? Pro zjednodušení předpokládejme, že je obdélník orientován buďto vodorovně nebo svisle, nikdy není pootočen.

Máme několik možností: můžeme určit střed obdélníka (dvě souřadnice) a jeho velikost (šířku a výšku), nebo můžeme určit jeden z jeho rohů a velikost, nebo můžeme určit polohu dvou protilehlých bodů. Zvolíme si levý horní roh obdélníka a jeho velikost.

class Rectangle:
    def __init__(self, posice, w, h):
        self.corner = posice
	self.width = w
	self.height = h
Parametr posice předpokládá zadání dvojice čísel - entici. Pro její vložení můžeme použít nepojmenovaný objekt již deklarované třídy Point (viz 10.2.2):
>>> bod = Rectangle(Point(), 100, 200)
>>> bod.width, bod.height
(100, 200)
>>> bod.corner.x, bod.corner.y
(0, 0) 

Objektu Point() jsme nezadali žádné argumenty, protože nám zřejmě vyhovují jeho implicitní hodnoty. Kdybychom chtěli mít počátek obdélníka jinde než v bodě (0,0), mohli bychom například zadat:

>>> box = Rectangle(Point(10,50), 100, 200)
>>> box.corner.x, box.corner.y
(10,50)
>>> box.width, box.height
(100,200)

Vzájemné vztahy objektů vidíme na následujícím obrázku: atribut 'corner' objektu 'box' odkazuje na objekt třídy Point s atributy 'x' a 'y'.


10.5 Instance

10.5.1   Instance jsou měnitelné

Změnu instance můžeme provést změnou některého z jejich atributů a to přiřazením. Abychom například změnili velikost obdélníka bez změny jeho polohy, změníme hodnoty width a height:

box.width = box.width + 50
box.height = box.height + 100

Změnu atributů můžeme zobecnit do funkce:

def grow_rectangle(obj, dwidth, dheight):
    obj.width += dwidth
    obj.height += dheight

Můžeme také napsat funkci pro změnu polohy obdélníka:

def move_rectangle(obj, dx, dy):
    obj.corner.x += dx
    obj.corner.y += dy


10.5.2   Instance jako výstupní hodnoty

Instance může být výstupní hodnotou funkce. Například, funkce find_center přijímá instanci třídy Rectangle jako argument a vrací instanci třídy Point, která obsahuje souřadnice středu zadaného obdélníka:

def find_center(obj):
    p = Point() 
    p.x = obj.corner.x + obj.width/2.0
    p.y = obj.corner.y + obj.height/2.0
    return p

Když si definice tříd Point (odst. 10.2.2), Rectangle (10.4) a funkcí find_center (10.5.2), print_point (10.2.2) vložíme do jednoho skriptu, můžeme v konzole Pythonu psát:

>>> box = Rectangle(Point(40,60), 100, 200)
>>> center = find_center(box)
>>> print_point(center)
90.0, 160.0

Je také možné deklarovat instanci s použitím entice,

>>> boot = Rectangle((40,60), 100, 200)

instance obsahuje atribut corner ale hodnoty tohoto atributu nemají jména:

>>> boot.corner
(40,60)
>>> boot.corner.x
AttributeError: 'tuple' object has no attribute 'x'

Pro takto deklarovaný objekt nám samozřejmě nebudou chodit funkce distance_from_origin(), print_point(obj) (odst. 10.2.2) a find_center(obj) (10.5.2), protože všechny používají jména x, y. Bude nám chodit funkce grow_rectangle(obj,dwidth,dheight) (10.5.1).


10.5.3   Instance uvnitř f-stringu

Instanci lze reflektovat i uvnitř f-stringu (viz kap. 6.8.3). Máme-li například definovanou třídu Komik v souboru "F:/codetest/howto/ch-10/katalog.py":

# F:/codetest/howto/ch-10/katalog.py 

class Komik:
    def __init__(self, first_name, last_name, age):
        self.first_name = first_name
        self.last_name = last_name
        self.age = age

    def __str__(self):
        return f"{self.first_name} {self.last_name}, věk {self.age}."

    def __repr__(self):
        return f"{self.first_name} {self.last_name}, věk {self.age}. Překvapení!" 

můžeme ji importovat do interaktivní konzoly(1), vytvořit instanci a tu posléze aktivovat v f-stringu:

>>> import katalog      # importujeme deklaraci třídy  
>>> komik = katalog.Komik("Jan", "Plíšek", "74")  # instance třídy
>>> f"{komik}"         (případně  f"{komik!s}")
'Jan Plíšek, věk 74.'
>>> f"{komik!r}"        # viz 6.10.3 Formátování literálem (f-stringem)
'Jan Plíšek, věk 74. Překvapení!'  # viz soubor katalog.py(2)
Poznámka 1

Zmíněnou "interaktivní konzolu" si vytvoříme tak, že se v systémové konzole (cmd, cmder, yori, terminal, bash) nacédujeme do složky "F:/codetest/howto/ch-10", kde si otevřeme konzolu Pythonu příkazem "python". Zde zadáváme výše uvedené příkazy.

Poznámka 2

Dodatek "Překvapení!" jsme při uplatnění konverze !r ve výměnném poli získali proto, že jsme si jej nadefinovali v metodě __repr__(self).


10.6 Kopírování třídy

Při kopírování třídy či objektu třídy (instance) použijeme vždy metodu copy nebo deepcopy z modulu copy.
Pro objekty tříd, které neobsahují vnořené objekty, postačí tak zvaná mělká kopie (shallow copy), která vytvoří nezávislou kopii nevnořených objektů. Příkladem takového objektu je třída Point z odst. 10.2.2:

>>> import copy
 
>>> p1 = Point(3,4)
>>> p2 = copy.copy(p1) 
>>> p1 is p2
False                              # mají různá ID
>>> p1.x == p2.x and p1.y == p2.y
True                               # mají stejné souřadnice

Příkladem vnořeného objektu je atribut corner třídy Rectangle , jejíž dva nevnořené objekty jsou atributy x, y - viz odst. 10.4.

Vytvoříme-li obdélník b1 a jeho kopii b2 funkcí copy,

>>> b1 = Rectangle(Point(), 100, 200)
>>> b2 = copy.copy(b1)
vytvoří se nezávislé kopie pouze atributů with a height, zatímco atribut corner je společný pro obě prezentace b1 a b2:

Nezávislou hlubokou kopii (deep copy) i vnořených objektů vytvoříme metodou deepcopy:

>>> b2 = copy.deepcopy(b1)

10.7 Iterátory definované třídou

Iterátorem v tomto případě je instance třídy s definovanými metodami __iter__()  a next(). Takto definovaný iterátor líný výpočet neprovádí.

Jako příklad si uvedeme skript, který si poté pustíme v konzole:

class Count: 
    def __init__(self, m):
        self.m = m-1
        self.n = 0
    def __iter__(self):
        return self
    def __next__(self):
        if self.n <= self.m:
            cur, self.n = self.n, self.n + 1
	    return cur
        else:
            raise StopIteration() 
>>> cnt = Count(5)       # instance třídy a zároveň iterátor
>>> next(cnt)            # první iterace
0 
>>> for i in cnt: print(i, end=" ") 
1 2 3 4                  # opakovaná iterace pro zbytek členů
>>> for in cnt: print(i, end=" ") 
...                      # zdvořilé mlčení, iterátor je prázdný

Alternativa funkce squares(start, stop) ve tvaru uživatelského iterátoru může mít tento tvar:

class Squares(object): 
    def __init__(self, start, stop):
        self.start = start
        self.stop = stop
    def __iter__(self):
        return self
    def __next__(self):
        if self.start >= self.stop:
            raise StopIteration
        current = self.start * self.start
        self.start += 1
        return current 
>>> sqr = Squares(4,8)     # instance třídy a zároveň iterátor
>>> 
>>> for i in sqr: print(i, end=" ") 
16 25 36 49                # dychtivá (eager) iterace
>>> for in sqr: print(i, end=" ") 
...                        # zdvořilé mlčení, iterátor je prázdný

10.8 Dekorátory

10.8.1   Vytvoření dekorátoru

Dekorátor je funkce vyššího řádu, která jako argument přijímá dekorovanou funkci a vrací svou vnitřní funkci (klauzuru), s oblibou nazývanou wrapper().

Účelem dekorátoru je změna chování dekorované funkce bez její modifikace. Úlohou klauzury je popsat tuto modifikaci.

Mechanizmus dekorátorů vychází z toho, že funkce může být zadána jako argument, definována uvnitř jiné funkce, vrácena jinou funkcí a být přiřazena k proměnné.

Dekorátorová funkce:
def decorator(func):   # dekorátor pro dekorovanou funkci 
    def wrapper():                     
        print("Před spuštěním ovečky")
        func()         # interní volání dekorované funkce
        print("Po spuštění ovečky")
    return wrapper
	
Dekorovaná funkce: 
@decorator                                 # jedna varianta                     
def decorated_function():              
    print("Ovečka se pěkně pase.")

#ovečka = decorator(decorated_function)    # druhá varianta

# invokace s dekorátorem:  decorated_function()
# invokace s přiřazením:  ovečka()

Nezbytné propojení dekorátoru a dekorované funkce se provádí dvojím způsobem:

  1. dekorací názvu dekorované funkce - což je v podstatě přiřazení, provedené interně interpretem Pythonu:
    # decorated_function = decorator(decorated_function): 
    @decorator    
    def decorated_function():      
        print("Ovečka se pěkně pase.")
    
    >>> decorated_function()    
    Před spuštěním ovečky
    Ovečka se pěkně pase.
    Po spuštění ovečky
    
  2. deklarací vztahu pod textem obou funkcí. Důležitou částí uvedeného přiřazení je formulace pravé strany. Levou stranu lze pojmenovat jakkoli:
    decorated_function = decorator(decorated_function)
    nebo třeba:
    ovečka = decorator(decorated_function)
    

    Invokací decorated_function() nebo ovečka() se volá dekorátorová funkce, jejímž argumentem je dekorovaná funkce:

    >>> ovečka()
    Před spuštěním ovečky
    Ovečka se pěkně pase.
    Po spuštění ovečky
    

Pro pochopení popisovaného mechanizmu je vhodné připomenout, že při volání dekorované funkce se nejprve provádí tělo dekorátorové funkce.

Existujícím dekorátorem lze dekorovat jakoukoli vhodnou funkci i metodu třídy.

@decorator
def beran():      
    print("Berany, berany duc!")
>>> beran()
Před spuštěním ovečky
Berany, berany duc!
Po spuštění ovečky

Rovněž je možné jednu funkci dekorovat více dekorátory:

def decorator(ovce):              # dekorátorová funkce           
    def wrapper():                       
        print("Před spuštěním ovečky")
        ovce()                           
        print("Po spuštění ovečky")
    return wrapper
		
def lines(ovce):                  # dekorátorová funkce
    def wrapper():
        print("- " * 12)
        ovce()
        print("- " * 12)
    return wrapper  		
		
@decorator
@lines
def beran():                      # dekorovaná funkce     
    print("Berany, berany, duc!")

Zpracování kódu začíná prvním dekorátorem nad názvem dekorované funkce:

>>> beran()
Před spuštěním ovečky
- - - - - - - - - - - - 
Berany, berany, duc!
- - - - - - - - - - - - 
Po spuštění ovečky

10.8.2   Klauzura s parametry

Jako ukázku si uvedeme dekorátor, který zabrání dělení nulou. Povšimněte si shodných parametrů u klauzury, odkazu na ovečku i u vlastní ovečky (dekorované funkce).

def smart_divide(func):
   def wrapper(a,b):            # klauzura s parametry 
      if b == 0:
         print("Nulou nelze dělit!")
         return                 # uplatní se jen při b == 0
      return func(a,b)          # nezbytný odkaz na ovečku
   return wrapper

@smart_divide
def divide(a,b):                # dekorovaná funkce
    return a/b
>>> divide(5,7)
0.7142857142857143
>>> divide(5,0)
Nulou nelze dělit!

Při práci s argumenty je vhodné používat sběrné parametry *args, *kwargs. Parametry vnitřní funkce (klauzury) wrapper musí být kompatibilní s parametry invokované funkce:

def logging(func):
   def wrapper(*args, **kwargs):               
      print('Příchozí argumenty', args, kwargs)
      output = func(*args, **kwargs)
      print("Výstup", output)
   return wrapper

@logging
def sumace(a, b, c, vasil=0):
  return a+b+c+vasil
>>> sumace(1, 2, 3, vasil=5)             # viz Poznámka
Příchozí argumenty (1, 2, 3) {'vasil': 5}
Výstup 11
>>> sumace(1, 2, 3)
Příchozí argumenty (1, 2, 3) {}
Výstup 6

Poznámka:
Při volání funkce je nutné zachovat počet pozičních argumentů a počet a název klíčového argumentu. Klíčový argument může chybět, poziční argumenty nikoliv. Vyzkoušejte si to.

10.8.3   Dekorátor s parametrem

Vlastní dekorátorová funkce (dekorátor) může mít pouze jeden parametr, jímž je nepřímý odkaz na dekorovanou funkci. Případný další parametr se připojuje k nové vnější funkci (kvazi dekorátor). Propojení přiřazením se v tomto případě vytváří mezi dekorovanou funkcí a kvazidekorátorem (z vnější dekorátorové funkce se stala vnitřní funkce).

Příklad plně vybaveného (kvazi) dekorátoru, jehož účelem je poskytnout argument dekorované funkci

def anonce(n):                       # kvazi dekorátor
    def decorator(func)              # dekorátor s klauzurou
        def wrapper(*args, **kwargs):        # klauzura
            result = func(*args, **kwargs)   # volání ovečky
            return result + n**n             # úprava ovečky
        return wrapper
    return decorator
 
@anonce(2)                 # argument je součástí dekorace
def add(x, y):             # dekorovaná funkce - říkejme ji ovečka
    return x + y
>>> add(5,7)
16                         # není to chyba: 5+7 + 2**2 = 16

Zkrácená verze dekorátoru s parametrem

Dekorátorovou funkci tvoří pouze kvazidekorátor a dekorátor:

def call(*argv, **kwargs):             # kvazidekorátor 
    def dekor(func):           # dekorátor coby klauzura  
        return func(*argv, **kwargs)       
    return dekor 
  
@call(5,7)                     # dekorace s argumenty
def add(x, y):                 # dekorovaná fce 
    return x + y 

10.8.4   Dekorátor 'wraps'

Systémový dekorátor @wraps umožňuje přístup k introspektivním atributům funkce. Tento dekorátor importujeme z modulu functools. Atribut .num_calls vrací počet volání funkce:

from functools import wraps

def counter(func): 
    @wraps(func)         # dekorace importovaným dekorátorem                     
    def wrapper(*args, **kwargs):          
        wrapper.num_calls += 1                # počítadlo
        print(f"Call {wrapper.num_calls} of   
  	             {func.__name__!r}:")     # viz Pozn.
        return func(*args, **kwargs)
    wrapper.num_calls = 0
    return wrapper

@counter                 # dekorace vlastní funkcí
def add(x, y):
    "Aplikován dekorátor 'wraps'"
    print(x + y)

Poznámka: Písmeno f uvádí takzv. f-string - viz kap. 6.8.3. Písmeno !r označuje druh konverze - viz kap. 6.8.2 (Konverze argumentu).

>>> add(5,7)
Call 1 of 'add':
12
>>> add(5,7)
Call 2 of 'add':
12

>>> add.__name__    ==> 'add'
>>> add.__doc__     ==> "Aplikován dekorátor 'wraps'"
>>> add.__module__  ==> '__main__'  

Pro výše uvedené vlastnosti je doporučeníhodné používat importovaný dekorátor @wraps v hojné míře.

10.8.5   Dekorace funkce třídou

Při dekorování funkce třídou zastupuje klauzuru wrapper speciální metoda __call__ :

class Pohoda:
    def __init__(self, fun):             # konstruktor
        self.fun = fun 
    def __call__(self, *args, **kwargs): 
        # Sem může přijít vhodný kód  
        self.fun(*args, **kwargs)         # volání 'ovečky' 
        # Sem rovněž může přijít vhodný kód 
              
@Pohoda                                   # dekorace třídou
def pozdrav(name, message ='Nazdar'):     # ovečka 
    print("{} {}".format(message, name)) 
>>> pozdrav("Pavle!")
Nazdar Pavle!

Uvedená ukázka je poněkud schematická, protože výstup "Nazdar Pavle!" bychom zajistili i bez dekorátoru. Napravíme to další ukázkou, ve které nám dekorátor doplní výstup z dekorované třídy:

class Decorator:
   def __init__(self, x):
        self.x = x
   def __call__(self, a):
        print('Druhá mocnina', a, 'je', self.x(a))
                    # výraz self.x(a) je invokace ovečky
@Decorator
def square(a):
   return a*a
>>> square(5)
Druhá mocnina 5 je 25

10.8.6   Dekorace třídy funkcí

import functools
import time

def timer(func):
    "Vytiskne délku výpočtu dekorované funkce či třídy"
    @functools.wraps(func)                 
    def wrapper(*args, **kwargs):
        start = time.perf_counter()  
        val = func(*args, **kwargs)
        run_time = time.perf_counter() - start  
        print(f"Finished {func.__name__!r} in {run_time:.4f} secs")
        return val
    return wrapper

@timer
class Calculator:
    def __init__(self, num):
        self.num = num
        import time
        time.sleep(2)

    def double_and_add(self):
        "Vrátí sumaci stejně modifikovaných členů řady"
        res = sum([i * 2 for i in range(self.num)])
        print("Result : {}".format(res))
>>> c = Calculator(100)
Finished 'Calculator' in 2.0111 secs
>>> c.double_and_add()
Result : 9900

Dekorátor @timer můžeme použít i k dekoraci funkce.


10.9 Speciální metody a atributy

Pro zvídavé

Speciální (také magické) jsou všechny předdefinované metody a atributy tříd, jejichž název je ohraničen dvěma podtržítky, jako například výše popsaná metoda __init__. Jejich alternativní označení v angličtině je dunders (double underscores).

Výpis předdefinovaných metod a atributů pro určitý objekt získáme příkazem dir(), například pro slovník (list):

>>> dir([1,2,3])
['__add__', '__class__', '__contains__', '__delattr__', '__delitem__',
'__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__',
 '__getitem__', '__gt__', '__hash__', '__iadd__', '__imul__',
'__init__', '__init_subclass__', '__iter__', '__le__', '__len__',
 '__lt__',  '__mul__', '__ne__', '__new__', '__reduce__',  '__reduce_ex__',
 '__repr__', '__reversed__',  '__rmul__', '__setattr__', '__setitem__',
 '__sizeof__', '__str__', '__subclasshook__',
'append', 'clear', 'copy', 'count', 'extend', 'index', 'insert',
 'pop', 'remove', 'reverse', 'sort']

Speciální metody jsou interně evokované při explicitním volání příslušných operátorů a vestavěných funkcí. Například porovnání "ariel" == "alias" je realizováno speciální metodou __eq__:

>>> "ariel".__eq__("alias")
False

Speciální metodě můžeme zadat specifické chování. Například sečíst souřadnice dvou bodů (provést vektorový součet):

class Point:
    def __init__(self, x=0, y=0):
        self.x = x
        self.y = y

    def __add__(self, other):    # speciální metoda (instanční)
        return self.x + other.x, self.y + other.y		
>>> p1 = Point(1, 2)
>>> p2 = Point(3, 4)
>>> p1 + p2                       # interně  p1.__add__(p2)
(4, 6)

Specielními atributy jsou datové objekty __doc__, __name__, __dict__, ... . Atribut __doc__obsahuje informaci o příslušném objektu; například o prázdném seznamu se dozvíme:

>>> [].__doc__
Built-in mutable sequence.

If no argument is given, the constructor creates a new empty list.
The argument must be an iterable if specified. 

Dobrý přehled specielních metod lze nalézt na stránce tutoriálu Ponořme se do Pythonu 3


10.10 Datové třídy

Datová třída (dataclass) je speciální třída, vhodná pro ukládání datových objektů, která je vytvořená s pomocí dekorátoru @dataclass, importovaného z modulu dataclasses.

Tento dekorátor automaticky generuje všechny potřebné dunder metody __{init, repr, eq, lt, gt, le, ge, hash, ...}__(), podporující zjednodušené zadávání datových atributů (neboli polí či fields). Deklarace pole má skladbu:   name:   type   [ = value ].

from dataclasses import dataclass

@dataclass
class Kontakt:
    name: str                    # jméno a typ pole                     
    email: str                   # jméno a typ pole
    phone: str = "00 00 00 00"   # dtto plus hodnota pole 

Vytvoření instance třídy může mít následující formu:

>>> contact = Kontakt("test", "test@test.com", '72 63 08 56')
>>> contact
Kontakt(name='test', email='test@test.com', phone='72 63 08 56')

Implicitní hodnotu lze při invokaci vynechat:

>>> contact = Kontakt(name="test1",email="test1@test.com")
>>> contact
Kontakt(name='test1', email='test1@test.com', phone='00 00 00 00')

Výpis vytvořených metod a funkcí pro instanci contact získáme příkazem:

>>> print(dir(contact))             

Výpis polí instance contact ve formě slovníku získáme příkazem:

>>> print(contact.__dict__)
{'name': 'test', 'email': 'test@test.com', 
'phone': '72 63 08 56'}

Implicitní hodnotu argumentu lze vytvořit rovněž importovanou metodou:

from dataclasses import dataclass
from datetime import datetime

@dataclass
class Kontakt1:
    name: str                                             
    email: str                       
    time: datetime.date = datetime.now().date()   
>>> contact = Kontakt1("name = Pavel","email = best@test.cz")
>>> print(contact)
Kontakt1(name='Pavel', email='best@test.cz', 
        time=datetime.date(2024, 1, 7))

Implicitní 'dunder' metody lze ručně upravovat jejich zneplatněním při deklaraci dekorátoru a opětovnou deklarací metody v těle třídy:

from dataclasses import dataclass
import time

@dataclass(repr = False)
class Kontakt2:
    name: str                                             
    email: str
    seconds = time.time()              # pomocný argument             
    local_time = time.ctime(seconds)

    def __repr__(self):
        return (f"{self.name}, {self.email},
		{self.local_time}")
>>> contact = Kontakt2(name = "Pavel", email = "best@test.cz")
>>> print(contact)
Pavel, best@test.cz, Tue Jan  9 17:16:16 2024

10.11 Dědění

Mechanizmus, zvaný dědění (inheritance) se používá při odvození nové třídy z třídy stávající. Odvozená třída (sub class, potomek) přebírá atributy a metody třídy výchozí, neboli bázové (super class, rodič či předek).

Skladba pro deklaraci subtřídy je jednoduchá:

class NázevOdvozenéTřídy(NázevVýchozíTřídy):
    '''Nepovinný dokumentační řetězec - docstring '''
    pass

Kromě naznačeného dědění po jediném předkovi (simple heritance) existují další možnosti:

Instruktivní ukázka s použitím speciální metody __init__:

class Savci:
    vid = "Pozdrav Pánbůh"
    def __init__(self, druh):
        hid = "pane Randák"
        print(druh, 'je teplokrevný savec.')
		        
class Pes(Savci):
    def __init__(self):
        super().__init__('Pes')       Varianta: Savci.__init__(self, 'Pes')
	print('Pes je přítel člověka.')

Speciální metoda __init__ třídy Pes potlačí tutéž metodu třídy Savci. Toto chování však zruší následně definovaná metoda __init__ s objektem super() (nebo Savci), čímž umožňuje použití atributů děděné třídy.

>>> pajda = Pes()
Pes je teplokrevný savec.
Pes je přítel člověka.

Zadání: Vypusťte v definici třídy Pes řádek "super().__init__('Pes')" a a zkuste pro objekt pajda volat proměnné vid a hid.

Obsahuje-li zdrojová i děděná třída stejný atribut, platí pro děděnou třídu vlastní definice tohoto atributu:

class A:
    pes = "Dingo" 
    def zobraz(self):
        print('Jsem třída A.')

class B(A):
    pes = "Voříšek" 
    def zobraz(self):
        print('Jsem třída B.')
>>> obj = B()
>>> obj.pes
'Voříšek'
>>> obj.zobraz()
Jsem třída B.

10.12 Glosář

třída (class)
Základní typ v Pythonu. Třídu lze považovat za prototyp objektů, určující jejich vlastnosti a chování.
instance
Konkrétní objekt určité třídy.
objekt (object)
Všechno v Pythonu je objekt - coby instance třídy.
konstruktor (constructor)
Metoda __init__, kterou Python automaticky použije při tvorbě nové instance.
mělká rovnost (shallow equality)
Rovnost odkazů, nebo dvou odkazů, které ukazují na stejný objekt.
hluboká rovnost (deep equality)
Rovnost hodnot, nebo dvou odkazů, které ukazují na objekty se stejnými hodnotami.
mělká kopie (shallow copy)
Kopie objektu včetně odkazů na vnořené objekty provedená příkazem copy.copy(item); vnořené objekty se nezkopírují, pouze odkazy.
hluboká kopie (deep copy)
Kopie objektu včetně jeho vnořených objektů provedená příkazem copy.deepcopy(item)
dunders (double underscores)
Alternativní označení dvojitých podtržítek (__), používaných při zápisu 'magických' metod a atributů tříd (viz 12.9).

10.13 Cvičení

  1. Do třídy Rectangle vložte metodu area, která vrací plochu obdélníka
    r = Rectangle(Point(0,0), 50,100)
    # test: r.area() --> 5000
    
  2. Do třídy Rectangle vložte metodu perimeter, která vrací obvod obdélníka
    r = Rectangle(Point(0,0), 50,100)
    # test: r.perimeter() --> 300
    
  3. Do třídy Rectangle vložte metodu flip, která zamění jeho šířku za výšku a obráceně
  4. Ke třídě Point (viz 12.2.2) přidejte metodu sklon, která vrátí úhel sklonu spojnice bodu x,y s počátkem 0,0 v radiánech. Její evokace může vypadat takto:

    >>> Point(4,10).sklon()
    Sklon v radiánech:  1.1902899496825317
    

    Ošetřte případ, kdy je spojnice kolmá k ose x (ψ = π/2).

  5. Upravte skript třídy Punktum:

    class Punktum:
        def __init__(self, x, y): 
            self.x = x
            self.y = y
    	def __repr__(self):
            return f"Souřadnice bodu: x = {self.x}," \
    		                        f"y = {self.y}"	
    

    tak aby její volání mělo následující výstup

    >>> Punktum(5,9)
    Souřadnice bodu: x = 5, y = 9
    

pre up next title end end