Sytuacja kobiet w IT w 2024 roku
4.07.20226 min
Simon Hawe

Simon HaweDirector of Engineering, Data

Jak pisać świetne klasy w Pythonie

Sprawdź 6 magicznych metod w Pythonie, które ułatwią Twoją codzienną pracę z danymi.

Jak pisać świetne klasy w Pythonie

Python to język stosowany w większości projektów z zakresu data science i uczenia maszynowego. Jednak nie każdy mistrz danych musi być doświadczonym programistą Pythona, który zna i wykorzystuje wszystkie fajne funkcje, jakie oferuje ten język.

Jest to oczywiście zrozumiałe, ale jednocześnie dość niefortunne. Dlaczego? Ponieważ znajomość specyfiki języka pozwala na pisanie mniejszej ilości powtarzalnego kodu, bardziej czytelnego i łatwiejszego w utrzymaniu. Ogólnie rzecz biorąc, jeśli w pełni wykorzystujesz dany język, jakość Twojego kodu będzie lepsza, a co ważniejsze, Ty i Twój kolega będziecie się lepiej bawić, co, jak sądzę, wszyscy lubimy.

Dlatego chcę przyczynić się do poszerzenia Twojej wiedzy o Pythonie, abyś mógł pisać lepszy kod, zaimponować kolegom i koleżankom oraz lepiej się przy tym bawić! W tym artykule chcę omówić w szczególności tzw. metody dunder, specjalne czy też magiczne. Zaciekawieni? Zaczynajmy.

Metody magiczne

Jak można było zauważyć w tytule, będziemy mówić o magicznych metodach Pythona. Można napotkać też takie terminy jak metoda dunder czy metoda specjalna, które odnoszą się do tej samej rzeczy. W tym artykule będę używać terminu metody magiczne. Czym więc są owe magiczne metody?

Podstawowe informacje

Metody magiczne to funkcje, które należą do klasy. Mogą to być zarówno instancje, jak i metody klasy. Można je łatwo zidentyfikować, ponieważ wszystkie zaczynają się i kończą podwójnym podkreśleniem, tzn. wyglądają w ten sposób: __actual_name__. Stąd też pochodzi określenie dunder double underscores (podwójne podkreślenie). Trochę mi zajęło, zanim do tego doszedłem.

Prawdopodobnie najważniejszą rzeczą jest to, że metody magiczne nie są przeznaczone do bezpośredniego wywoływania przez Ciebie! Możesz to oczywiście zrobić i napisać coś w stylu YourClass().__actual_name__(), ale proszę, nie rób tego!

Jak zatem wywoływane są metody magiczne? Poprzez określone akcje, które użytkownik stosuje do swojej klasy lub instancji klasy. Dla przykładu wywołanie str(YourClass()) wywoła magiczną metodę __str__ lub YourClass() + Your Class() wywoła __add__, jeśli ją zaimplementowałeś.

Do czego przydają się metody magiczne? Umożliwiają one pisanie klas, które mogą być używane razem z metodami wbudowanymi w Pythona. Jeśli tak zrobisz, możesz pisać, a powiedziałbym, że nawet na pewno będziesz pisać bardziej czytelny i mniej rozbudowany kod. Mam nadzieję, że zauważyliście to po tej małej zajawce w poprzednim akapicie.

Aby podkreślić przydatność metod magicznych i pokazać, jakie korzyści można odnieść, stosując je w uczeniu maszynowym lub data science, posłużymy się konkretnym przykładem.

Przykład: Niestandardowy zakres datetime

Chciałbym zaprezentować wam przykład, w którym można użyć metod magicznych do napisania czegoś podobnego do wbudowanej funkcji range. W przeciwieństwie do wersji wbudowanej przykład oferuje nieco więcej funkcji, a przede wszystkim tworzy zakresy datetime zamiast zakresów liczbowych.

Oczywiście, można by użyć Pandas lub jakiejś innej biblioteki, ale myślę, że ten przykład ma na celu ułatwienie nam zrozumienia ogólnej koncepcji metod magicznych. Dzięki temu mogę też pokazać niektóre z najbardziej przydatnych metod magicznych, przydatne szczególnie podczas pracy z danymi.

Samo mówienie lub pisanie o kodzie bez zobaczenia prawdziwego i działającego kodu jest nieco nudne, prawda? Przestańmy więc to robić i przyjrzyjmy się rzeczywistej implementacji zakresu datetime

from datetime import datetime, timedelta
from typing import Iterable
from math import ceil


class DateTimeRange:
    def __init__(self, start: datetime, end_:datetime, step:timedelta = timedelta(seconds=1)):
        self._start = start
        self._end = end_
        self._step = step

    def __iter__(self) -> Iterable[datetime]:
        point = self._start
        while point < self._end:
            yield point
            point += self._step

    def __len__(self) -> int:
        return ceil((self._end - self._start) / self._step)

    def __contains__(self, item: datetime) -> bool:
        mod = divmod(item - self._start, self._step)
        return item >= self._start and item < self._end and mod[1] == timedelta(0)

    def __getitem__(self, item: int) -> datetime:
        n_steps = item if item >= 0 else len(self) + item
        return_value = self._start + n_steps * self._step
        if return_value not in self:
            raise IndexError()

        return return_value
   
    def __str__(self):
        return f"Datetime Range [{self._start}, {self._end}) with step {self._step}"

# Usage
my_range = DateTimeRange(datetime(2021,1,1), datetime(2021,12,1), timedelta(days=12))
print(my_range)
assert len(my_range) == len(list(my_range))
my_range[-2] in my_range
my_range[2] + timedelta(seconds=12) in my_range
for r in my_range:
    do_something(r)


Okey, całkiem niezły kawałek kodu. Jeśli nieco się wzdrygnąłeś, nie przejmuj się, wytrzymaj ze mną, rzucimy na to lepsze światło. Ogólnie rzecz biorąc, wdrożyłem sześć różnych metod magicznych, które wyjaśniam poniżej.


__init__

Pierwszą z nich i prawdopodobnie najbardziej znaną jest metoda __init__.  Jak z pewnością wiesz, metoda ta służy głównie do inicjalizowania atrybutów instancji klasy. W tym miejscu ustawiamy początek i koniec zakresu klasy oraz wielkość kroku. Jest to podobne do tego, co robimy podczas tworzenia wbudowanej funkcji range.


__iter__

Kolejną metodą jest metoda __iter__. Jest to prawdopodobnie najważniejsza metoda, ponieważ generuje ona wszystkie elementy należące do zakresu datetime. Funkcja ta jest tak zwaną funkcją generatora , która tworzy jeden element na raz, przekazuje go do wywołującego i pozwala mu go przetworzyć.

Robi to do momentu osiągnięcia końca zakresu. Funkcję generatora można łatwo zidentyfikować, widząc słowo kluczowe yield. Ta instrukcja wstrzymuje działanie funkcji, zapisując wszystkie jej stany, a następnie kontynuuje je przy kolejnych wywołaniach. Umożliwia to korzystanie z jednego elementu na raz i pracę z nim bez konieczności przechowywania wszystkich elementów w pamięci.

Nieumieszczanie wszystkiego w pamięci staje się bardzo przydatne, kiedy albo każdy element zajmuje dużo miejsca w pamięci, albo kiedy jest ich bardzo dużo. Na przykład spróbuj wykonać list(DateTimeRange(datetime(1900,1,1), datetime(2000,1,1)) albo raczej tego nie rób, ponieważ w ten sposób powstanie lista zawierająca 3184617600 pozycji datetime. Za dużo, przepraszam. Jednak używając generatora, można łatwo przetwarzać te elementy jeden po drugim.

Teraz widać, że nie jest to lista ani krotka. Jednakże, aby pracować z klasą DateTimeRange podobnie jak z listą lub krotką, dodałem trzy kolejne magiczne metody, a mianowicie __len__, __contains__ i __getitem__


__len__                   

Dzięki metodzie __len__ możesz dowiedzieć się, ile elementów wchodzi w skład Twojego zakresu, wywołując len(my_range). Może to być bardzo pomocne na przykład podczas iteracji po wszystkich elementach i gdy chcemy wiedzieć, ile elementów spośród wszystkich dostępnych już przetworzyliśmy. Może Ci też powiedzieć: „Hej, będę musiał przetworzyć dość dużo danych, więc napij się w tym czasie kawy”.


__contains__

Dzięki __contains__ możesz sprawdzić, czy element jest częścią twojego zakresu, używając wbudowanej składni element in my_range. Fajną rzeczą w podanej implementacji jest to, że jest to czysta matematyka, i nie trzeba porównywać danego elementu ze wszystkimi elementami z zakresu.

Oznacza to, że sprawdzanie, czy dany element należy do zakresu, jest operacją wykonywaną w stałym czasie i nie zależy od rozmiaru instancji zakresu. Również w tym przypadku może to być przydatne przy dużych zakresach, z którymi często mamy do czynienia podczas pracy z danymi.


__getitem__

Dzięki __getitem__ możesz użyć składni indeksowania do pobierania wpisów z obiektów. Można więc na przykład uzyskać ostatni element naszego zakresu, pisząc my_range[-1]. Muszę przyznać, że jest to prawdopodobnie najmniej przydatna metoda dla tego konkretnego przykładu. Jednak ogólnie rzecz biorąc, używanie __getitem__ pozwala na pisanie bardzo czystych i czytelnych interfejsów.


__str__

Szóstą i ostatnią magiczną metodą, którą dodałem, jest __str__. Metoda ta umożliwia konwersję instancji klasy na string. Staje się to bardzo przydatne przy wywołaniu print (my_range), ponieważ print musi przekształcić instancję w string i dlatego używa metody __str__

Zakończenie

W tym artykule przedstawiłem podstawowe pojęcia związane z metodami magicznymi w Pythonie. Przedstawiłem tutaj pierwszy przykład wykorzystania niektórych z tych metod, które moim zdaniem mogą być przydatne podczas pracy z danymi. Oczywiście jest ich o wiele więcej, na przykład do tworzenia menedżer kontekstu lub ulepszania klas. Polecam Wam dalsze eksplorowanie tej przestrzeni i ciągłe pogłębianie swojej wiedzy w tym zakresie!

Liczę na to, że jesteś już dobrze przygotowany do tworzenia magicznych interfejsów, a może także do pochwalenia się nimi swoim przyjaciołom i kolegom.

Dzięki za przeczytanie tego artykułu! 


Oryginał tekstu w języku angielskim przeczytasz tutaj.

<p>Loading...</p>