PythonJiří Znamenáček„Abstract Base Classes“2016-04-14
Abstract Base Classes (zkráceně a velmi často ABC) je speciální druh tříd (a také skupina specifických tříd ve standardní knihovně), které se vyznačují mimo jiné následujícím:
lze na nich definovat tzv. abstraktní atributy, které třídy, jež jsou jejich potomky, musí implementovat, jinak při pokusu o jejich instanciaci obdržíme výjimku;
Jejich nejprůhlednějším použitím bude asi bod dvě – díky němu totiž ABCs umožňují přesunout kontrolu úplnosti API již do chvíle instanciace a nikoli až do chvíle pokusu o zavolání nenaimplementovaného atributu. (Příklad na dalším slajdu toto snad osvětlí.) Ovšem to není ten hlavní důvod, proč byly do jazyka zavedeny.
PS: Z jiných jazyků konceptu ABC odpovídají pojmy interface nebo trate. Podědit z ABC pak znamená přijmout (a implementovat) daný kontrakt.
..můžeme na potomcích vesele volat poděděné metody:
>>> p = Potomek()
>>> hasattr(p, 'metoda1')
0: True
>>> p.metoda1()
Ahoj!
>>> hasattr(p, 'metoda2')
1: True
>>> p.metoda2()
Traceback (most recent call last):
File "<pyshell#18>", line 1, in <module>
p.metoda2()
File "<pyshell#10>", line 7, in metoda2
raise NotImplementedError()
NotImplementedError
Potomek sice podědil po Rodiči dvě metody, ale naimplementoval pouze jednu a my se to nedozvíme, dokud se ji nepokusíme zavolat. To není moc příjemné.
Ba co víc, můžeme si pochopitelně instanciovat přímo Rodiče, jako by se nechumelilo, byť samozřejmě nemá chudák co dělat, protože právě to byl koneckonců i jeho účel:
>>> class Rodič:
... def metoda1(self):
... raise NotImplementedError()
... def metoda2(self):
... raise NotImplementedError()
>>> r = Rodič()
>>> r.metoda1()
Traceback (most recent call last):
File "<pyshell#20>", line 1, in <module>
r.metoda1()
File "<pyshell#10>", line 4, in metoda1
raise NotImplementedError()
NotImplementedError
>>> r = Rodič()
Traceback (most recent call last):
File "<pyshell#23>", line 1, in <module>
r = Rodič()
TypeError: Can't instantiate abstract class Rodič with abstract methods metoda1, metoda2
..ale dokonce ani potomka, pokud bude v API chybět implementace vyžadovaných metod:
>>> class Potomek(Rodič):
...
... def metoda1(self):
... print('Ahoj!')
>>> p = Potomek()
Traceback (most recent call last):
File "<pyshell#25>", line 1, in <module>
p = Potomek()
TypeError: Can't instantiate abstract class Potomek with abstract methods metoda2
Samozřejmě by to celé nemělo moc smysl, kdyby to nespolupracovalo s ostatními třídními dekorátory, ale (naštěstí) spolupracuje. Jen by dekorátor @abstractmethod měl být vždy poslední (nebo vlastně první), aby bezpečně tušil, která zrovna bije:
class C(metaclass=ABCMeta):
# klasické třídní metody
@abstractmethod
def my_abstract_method(self, ...):
...
@classmethod
@abstractmethod
def my_abstract_classmethod(cls, ...):
...
@staticmethod
@abstractmethod
def my_abstract_staticmethod(...):
...
Přímo podle dokumentace.
Tato spolupráce samozřejmě platí i pro deskriptory:
class C(metaclass=ABCMeta):
# deskriptor klasicky
@abstractmethod
def _get_x(self):
...
@abstractmethod
def _set_x(self, val):
...
x = property(_get_x, _set_x)
# deskriptor pomocí dekorátorů
@property
@abstractmethod
def my_abstract_property(self):
...
@my_abstract_property.setter
@abstractmethod
def my_abstract_property(self, val):
...
Přímo podle dokumentace.
ABCs můžete využít ještě jedním způsobem – v případě potřeby je můžete zaregistrovat jako předka pro libovolnou jinou třídu. Tím se sice dostanou do jejích předků z hlediska introspekčních funkcí issubclass() a isinstance(), ale příslušné abstraktní metody nejsou ani dostupné, ani vyžadované:
Podobnou funkcionalitu na úrovni třídy bez potřeby registrace zajistí i metoda __subclasshook__(), ale to už se podívejte do dokumentace.
Kdy se to hodí? Třeba když váš objekt splňuje API nějaké ABC, ale přitom sám není napsán v Python'u (což je mimo jiné případ většiny základních pythoních typů).
Ostatně stačí se podívat do modulu _collections_abc.py ve standardní knihovně.
ABCs tedy pomáhají vyrábět API, kterého se všichni musí držet – „zpola upečená“ implementace se totiž prozradí hned při prvním pokusu o instanciaci dané třídy.
Tuto vlastnost využijete nejspíše při práci na velkém projektu a v týmu, případně když váš program budou používat jiní jako knihovnu nebo když budete třeba vyrábět typy dynamicky za běhu programu.
Ale možná ještě zajímavější je, že Python poskytuje doslova armádu předpřipravených (abstraktních) tříd, které takto na abstraktní úrovni definují atributy, které musí splňovat objekty, které se mají chovat jako užitečné (vestavěné) typy.
Tak třeba v modulu collections.abc se mimo jiné schovává předpis pro objekty, které se mají chovat jako funkce – ty totiž musí implementovat metodu __call__() a ta je nepřekvapivě na (abstraktní) třídě Callable definována jako abstraktní.
PS: Vtipné je, že když čtete PEP 3119, tak zjistíte, že právě tohle byl důvod, proč se ABCs v Python'u vůbec zavedly. Že je ve výsledku můžete používat i pro asi trochu jiné účely, je už jen vedlejší efekt ^_~