Blog

Python, Regular, Senior

Funkcja type() – kiedy class to za mało

06 Lis 2018

Dzisiaj na warsztat trafia jedna z funkcji wbudowanych – funkcja type(). Zwykle przyjmuje ona jeden argument i zwraca typ przekazanego jej argumentu.

class type(object)

Taką jej postać poznajemy w trakcie kursów pythona, takiej używamy przy debugowaniu. Warto na szybko przypomnieć sobie jej użycie z pomocą intepretera pythona:

>>> val = 10
>>> val2 = „Hello World”

>>> type(val)

<class ‚int’>

>>> type(val2)

<class ‚str’>

Jak widać nie ma tutaj żadnej większej niespodzianki. Zastanawialiście się kiedyś jaki jest typ zwracany? To również możemy sprawdzić funkcją type().

>>> type(type(val))

<class ‚type’>

O ile do tej pory wyniki były dosyć oczywiste, to teraz funkcja zwróciła nam tajemniczy typ type. To tzw. metaklasa. Podobny efekt uzyskamy jeśli sprawdzimy typy wbudowane.

>>> type(int)

<class ‚type’>

>>> type(float)

<class ‚type’>

A także klasy przez nas utworzone.

>>> class Foo:
…     pass

>>> type(Foo())
<class ‚__main__.Foo’>
>>> type(Foo)
<class ‚type’>

Czym są metaklasy? Pokrótce na potrzeby tego artykułu wystarczy nam wyjaśnienie, że metaklasy są tym dla klas, czym klasy są dla instancji.

funkcja type() diagram1: http://yuml.me/diagram/scruffy/class/edit/[type{bg:steelblue}]^--[type], [type]^--[class], [class{bg:wheat}]^--[instance{bg:green}]

https://yuml.me – polecam

Czyli zarówno Foo, jak i int czy float są instancjami metaklasy type.

Jak nie korzystać z funkcji type()?

Chociaż każdy programista pisze kod najlepiej jak potrafi, to każdy z nas – prędzej lub później – napotka tego typu makaronowy koszmar. Oczywiście napisany przez kogoś innego, kto dawno już nie pracuje w naszej organizacji :).

Funkcja type() jest tutaj wykorzystywana, by na podstawie typu przekazanego jej argumentu podjąć jakieś decyzje. Tylko gdzie właściwie czai się ten koszmar? Można powołać się na obszerną listę artykułów, które dokładnie takie wykorzystanie tej funkcji podają jako przykład pythonowego antypatternu. Ale nie musimy szukać tak daleko. Dokumentacja samej funkcji to bez wątpienia najlepszy argument.

[box type=”info”] With one argument, return the type of an object. The return value is a type object. The isinstance() built-in function is recommended for testing the type of an object.

https://docs.python.org/3.7/library/functions.html#type

[/box]

Według dokumentacji lepsze będzie tutaj wykorzystanie funkcji isinstance(). Przy takich porównaniach, kiedy chcemy by wyrażenie było prawdziwe dla klasy bazowej, to niemalże zawsze chcemy osiągnąć to samo również przy klasie pochodnej. Funkcja type() niestety nie daje nam żadnych mechanizmów wsparcia dla polimorfizmu, tymczasem isinstance() – jak najbardziej. Porównanie tych dwóch funkcji można zobaczyć na poniższym przykładzie.

Czy teraz jest już dobrze? Jeden rabin powie tak, a inny powie nie. W rzeczywistości nie spotyka się zbyt wielu przypadków, gdy przepływ sterowania powinien zależeć od przekazanego typu. Tak napisany kod z pewnością wzbudza wątpliwości.

Artykuł mógłby się zakończyć w tym miejscu, gdyby nie druga postać funkcji, z którą tak naprawdę styczność miało już niewielu.

Funkcja type() – kiedy class to za mało

Do naszej funkcji można przekazać również trzy parametry:

class type(name, bases, dict)

Na wyjściu otrzymamy wówczas nową instancję metaklasy type, czyli nową klasę! Klasę o nazwie name, klasach bazowych bases oraz słowniku dict. Możemy więc dowolnie kształtować jej metody czy zmienne. Daje nam to możliwości dedykowane słowu kluczowemu class, jednakże możliwe do wykorzystania w runtime. Ale jak to wygląda w praktyce?

Co tutaj się właściwie stało? W runtime utworzyliśmy nową klasę Cat na bazie istniejącej już Animal, zaś funkcja my_init została przekazana jako __init__ nowej klasy. Ponadto przekonaliśmy się, że tak utworzona klasa nie odstaje funkcjonalnie od swoich odpowiedników tworzonych za pomocą class. Czy jednak potrafimy sobie wyobrazić jakieś praktyczne zastosowanie dla tej funkcjonalności? Bez wątpienia możemy się tym pobawić na akademickich przykładach, jednakże czy w praktyce może zaistnieć sytuacja, gdy nie wiemy jakie klasy będą nam potrzebne?

Tak, to się zdarza. Chociażby możemy zechcieć przedstawić bazę danych, tak by każda tabela miała swój odpowiednik jako klasa. Python nic nie wie o tym jakie klasy będą potrzebne, zanim nie otworzy bazy danych. Ze względu na ograniczenia repl.it – poniższy przykład oparty jest o „bazę danych” w jsonie.

Jak widać możemy wylistować całe zbiory instancji klas, nawet jeśli nie wiedzieliśmy o nich w trakcie pisania kodu. Takie wykorzystanie type() pozwala na bardzo elastyczne podejście do wielu problemów, o czym należy pamiętać gdy współpracujemy z zewnętrznymi źródłami danych.

A wiedza na ten temat bez wątpienia przydaje się na rozmowach :).

Dawid Trendota

Software Engineer po przejściach. Na swojej drodze miał okazję zmierzyć się zarówno z rozwiązaniami wbudowanymi, aplikacjami bazodanowymi jak i kernelem Linuksa. Prywatnie użytkownik i fan Gentoo Linux. W wolnym czasie programuje w C#/Unity.

@Dawid Trendota
__init__(self) a klasa bazowa

Angielski (4)
C++ (7)
Junior (12)
Python (3)
Regular (6)
ReverseEngineering (1)
Rozmowa (5)
Różne (4)
Senior (1)
Tips&Tricks (8)
angielski c++ junior python regular rozmowa rozmowa kwalifikacyjna