Metody specjalne

Metody z podwójnymi podkreślnikami, dzięki którym obiekty działają z operatorami ==, <, +, in oraz z funkcją len().

Na tej stronie

Przykład

#
Python
class Score:
    def __init__(self, student, points):
        self.student = student
        self.points = points

    def __eq__(self, other):
        if not isinstance(other, Score):
            return NotImplemented
        return self.points == other.points

    def __lt__(self, other):
        if not isinstance(other, Score):
            return NotImplemented
        return self.points < other.points

    def __repr__(self):
        return f"Score({self.student!r}, {self.points})"

a = Score("Ania", 92)
b = Score("Kuba", 78)
c = Score("Ola", 92)

print(a == c, a != b)
print(b < a, a > b)
print(sorted([a, b, c]))
print(max([a, b, c]))
Wynikzapisany wynik, możesz go sprawdzić
True True
True True
[Score('Kuba', 78), Score('Ania', 92), Score('Ola', 92)]
Score('Ania', 92)

Definicja i zastosowanie

#

Metody specjalne, nazywane też magicznymi albo „dunder” (od double underscore), to metody o nazwach otoczonych podwójnymi podkreślnikami. Zwykle nie wywołuje się ich bezpośrednio: robi to Python, gdy użyjesz operatora albo funkcji wbudowanej. Zapis a == b zamienia się w a.__eq__(b), a + b w a.__add__(b), a len(a) w a.__len__().

Dzięki nim własne obiekty zachowują się jak typy wbudowane: wyniki można porównywać i sortować, punkty dodawać operatorem +, a kurs traktować jak kolekcję lekcji z len() i in. Takie przeciążanie operatorów ma sens, gdy znaczenie operacji jest oczywiste. Dodawanie punktów jest czytelne, dodawanie dwóch uczniów raczej nie.

Gdy metoda nie obsługuje typu drugiego argumentu, powinna zwrócić specjalną wartość NotImplemented, a nie zgłaszać wyjątek. Python spróbuje wtedy operacji odwrotnej po stronie drugiego obiektu (np. __radd__), a jeśli i ta zawiedzie, zgłosi czytelny TypeError.

Uwaga na równość i hashowanie: klasa, która definiuje __eq__, ale nie __hash__, nie może już być kluczem słownika ani elementem zbioru. Brakujące porównania <=, > i >= na podstawie __eq__ i __lt__ uzupełni dekorator functools.total_ordering.

Składnia

#
Składnia
class Nazwa:
    def __eq__(self, other): ...
    def __lt__(self, other): ...
    def __add__(self, other): ...
    def __len__(self): ...
    def __contains__(self, item): ...

Najczęstsze metody specjalne

#
  • __eq__()

    a == b

    Równość. Bez tej metody obiekt jest równy tylko samemu sobie. Operator != automatycznie zwraca odwrotny wynik.
  • __lt__()

    a < b

    Porównanie „mniejszy niż”. Wystarcza do sortowania przez sorted() oraz do min() i max(), a a > b Python obsłuży jako b < a.
  • __add__()

    a + b

    Dodawanie. Powinno zwracać nowy obiekt, a dla nieobsługiwanego typu wartość NotImplemented.
  • __radd__()

    b + a

    Dodawanie, gdy obiekt stoi po prawej stronie, a lewy argument nie umie go dodać. Dzięki niemu działa sum(), które zaczyna od 0.
  • __len__()

    len(a)

    Długość. Musi zwracać nieujemną liczbę całkowitą. Obiekt o długości 0 jest fałszywy w warunkach.
  • __contains__()

    x in a

    Sprawdzenie, czy element należy do obiektu. Bez tej metody in przeszuka elementy, jeśli po obiekcie da się iterować.
  • __hash__()

    hash(a)

    Hash, potrzebny w zbiorach i kluczach słownika. Musi opierać się na tych samych polach co __eq__.

Więcej przykładów

#
Dodawanie obiektów i sum()
Python
class Points:
    def __init__(self, value):
        self.value = value

    def __add__(self, other):
        if isinstance(other, Points):
            return Points(self.value + other.value)
        if isinstance(other, int):
            return Points(self.value + other)
        return NotImplemented

    def __radd__(self, other):
        return self + other

    def __repr__(self):
        return f"Points({self.value})"

print(Points(20) + Points(15))
print(5 + Points(20))
print(sum([Points(10), Points(25), Points(5)]))

try:
    Points(10) + "dużo"
except TypeError as error:
    print(error)
Wynikzapisany wynik, możesz go sprawdzić
Points(35)
Points(25)
Points(40)
unsupported operand type(s) for +: 'Points' and 'str'
Własna kolekcja: __len__ i __contains__
Python
class Course:
    def __init__(self, title, lessons):
        self.title = title
        self.lessons = list(lessons)

    def __len__(self):
        return len(self.lessons)

    def __contains__(self, topic):
        return topic.lower() in (lesson.lower() for lesson in self.lessons)

python = Course("Python", ["Zmienne", "Pętle", "Funkcje"])
soon = Course("Wkrótce", [])

print(len(python))
print("pętle" in python, "klasy" in python)
print(bool(python), bool(soon))
Wynikzapisany wynik, możesz go sprawdzić
3
True False
True False
Obiekty w zbiorze: __eq__ razem z __hash__
Python
class Badge:
    def __init__(self, name):
        self.name = name

    def __eq__(self, other):
        return isinstance(other, Badge) and self.name == other.name

    def __hash__(self):
        return hash(self.name)

earned = {Badge("Start"), Badge("Seria 7"), Badge("Start")}
print(len(earned))
print(Badge("Seria 7") in earned)
Wynikzapisany wynik, możesz go sprawdzić
2
True

Bez metody __hash__ utworzenie zbioru zgłosiłoby TypeError, bo klasa z własnym __eq__ traci domyślny hash.

Dobre praktyki

#
  • Metody porównań i działań zwracają NotImplemented dla nieobsługiwanych typów, zamiast zgłaszać wyjątek. Python spróbuje wtedy operacji po stronie drugiego obiektu.
  • Operatory arytmetyczne powinny tworzyć nowy obiekt. Zmiana self wewnątrz __add__ zaskoczy każdego, kto użyje +.
  • Komplet porównań dostaniesz dekoratorem functools.total_ordering albo przez @dataclass(order=True).

Powiązane hasła

#

Widzisz błąd albo brakuje przykładu? Napisz do nas.