article banner (priority)

Operatory w Pythonie

Cześć! To jest fragment książki Python od podstaw, która ma pomóc w nauce programowania od zera. Znajdziesz ją na Allegro, w Empikach i w księgarniach internetowych.

W rozdziale Podstawowe wartości poznaliśmy podstawowe operatory, takie jak operatory matematyczne (+, -, *, ...) czy operatory porównania (>, ==, >=, ...) oraz zobaczyliśmy ich użycie dla podstawowych wartości. Co jednak z klasami, które sami zdefiniujemy? Czy takie klasy również mogą używać operatorów? Odpowiedź brzmi "tak". Konieczne jest jednak określenie, jak te operatory powinny się zachowywać. Do tego Python używa metod o specjalnych nazwach, takich jak __eq__ czy __gt__. Gdy są one zdefiniowane, to pewne dodatkowe operacje są dla danej klasy dozwolone.

W tym rozdziale poznamy najistotniejsze z metod o nazwach specjalnych. Część z nich, na przykład __add__ czy __gt__, pozwala zdefiniować nowy operator dla klasy. Inne, na przykład __str__ czy __eq__, określa zachowanie funkcji lub struktur używanych przez język Python. Wszystko stanie się jasne z przykładami. Te wszystkie metody specjalne są powszechnie wykorzystywane w projektach i bibliotekach, oraz w dużym stopniu stanowią o wygodzie używania języka Python.

__str__

Kiedy przekazaliśmy stringa lub liczbę jako argument print, to zobaczyliśmy piękny wynik. Gdy jednak stworzyliśmy własną klasę, to wypisany tekst nie jest zbyt pomocny.

print(1) # 1 print("AAA") # AAA class User: def __init__(self, name): self.name = name user = User("Alojzy") print(user) # <__main__.User object at 0x1109c6b20>

Od czego to zależy? Od metody __str__[^204_1]. Jest to specjalna metoda wykorzystywana przez język Python, gdy chcemy zamienić obiekt na string. Domyślnie wyświetla ona pełną nazwę obiektu (wraz z umiejscowieniem) i jego adres pamięci (to nas nie powinno interesować). Możemy jednak zamienić ją tak, by zwracała bardziej przydatne informacje o obiekcie. Aby to zrobić, powinniśmy zdefiniować metodę __str__ i zwrócić z niej wartość string, która ma reprezentować obiekt.

class User: def __init__(self, name): self.name = name def __str__(self): return f"User(name={self.name})" user = User("Alojzy") print(user) # User(name=Alojzy)

Jak to działa? Funkcja print używa konstruktora str, który to używa metody __str__ naszego obiektu do zamienienia go na string. Tak więc ta metoda ma specjalne znaczenie. Nie należy jej jednak używać bezpośrednio. Zamiast wywołać metodę __str__, lepiej użyć konstruktora str lub użyć f-stringa.

print("Użytkownik: " + user.__str__()) # Użytkownik: User(name=Alojzy) print("Użytkownik: " + str(user)) # Użytkownik: User(name=Alojzy) print(f"Użytkownik: {user}") # Użytkownik: User(name=Alojzy)

Podobnych metod o specjalnych znaczeniach jest w języku Python wiele więcej. Wszystkie one są używane przez różne wbudowane funkcje lub funkcjonalności języka.

__repr__

Kiedy wyświetlamy listę, to wypisuje ona zawarte w niej elementy.

l = [1, "A", True] print(l) # [1, 'A', True]

Dzieje się tak dzięki temu, że lista ma metodę __str__, a ta zamienia każdy z przechowywanych obiektów na string. Tutaj jednak warto zauważyć, że sposób przedstawienia tych elementów jest nieco inny, niż gdy używaliśmy str. Na przykład dla stringa, str zwraca wyłącznie jego zawartość, a gdy wyświetlany jest string w liście, jest on otoczony cudzysłowami. Dzieje się tak dlatego, że używana jest "oficjalna" reprezentacja obiektu jako string, którą pobieramy przy użyciu funkcji repr[^204_2], a określamy przy użyciu metody __repr__.

print(repr("A")) # 'A' print(str("A")) # A print("A".__repr__()) # 'A' print("A".__str__()) # A

Dobrym przykładem różnicy między dwoma metodami może być klasa reprezentująca imię i nazwisko. Reprezentacją __str__ mogłoby być po prostu imię i nazwisko. Reprezentacją __repr__ mogłaby być pełna informacja o nazwie klasy i jej wartościach.

class FullName: def __init__(self, name, surname): self.name = name self.surname = surname def __str__(self): return f"{self.name} {self.surname}" def __repr__(self): return "FullName("+\ f"name={repr(self.name)}, "+\ f"surname={repr(self.surname)})" player = FullName("Alojzy", "Moskała") print(player) # Alojzy Moskała print(str(player)) # Alojzy Moskała print(repr(player)) # FullName(name='Alojzy', surname='Moskała')

W powyższym przykładzie podzieliłem stringa na kilka linii, ze względu na ograniczoną szerokość kodu w książce. Operacje podzielone na kilka linii albo muszą być otoczone nawiasem, albo na końcu linii powinny się znajdować znaki \. Jest to potrzebne, by interpreter traktował kolejną linię tak, jakby była częścią poprzedniej.

Kiedy definiujemy obiekty, najczęściej chcemy by __repr__ działała identycznie co __str__, co możemy uzyskać poprzez wyrażenie __repr__ = __str__.

class Position: def __init__(self, x, y): self.x = x self.y = y def __str__(self): return f"({self.x}, {self.y})" __repr__ = __str__ # lub # def __repr__(self): # return self.__str__() position = Position(10, 20) print(position) # (10, 20) print(repr(position)) # (10, 20)

__eq__

Kolejną istotną metodą jest __eq__[^204_3], która decyduje czy dwa obiekty są sobie równe. Jest więc wykorzystywana, gdy porównujemy dwa obiekty przy pomocy == lub !=. Jeśli nie zdefiniujemy tej metody, to dwa różne obiekty tej klasy nigdy nie będą sobie równe. Dla klas bez zdefiniowanej metody __eq__, operator == zwraca True tylko wtedy, gdy po obu stronach jest dokładnie ten sam obiekt (tak jak operator is). Dlatego w poniższym przykładzie user1 == user1 zwraca True, ale już user1 == user2 zwraca False, mimo iż atrybuty obu obiektów są identyczne.

class User: def __init__(self, name): self.name = name user1 = User("Alek") user2 = User("Alek") user3 = User("Bolek") print(user1 == user1) # True print(user1 == user2) # False print(user1 == user3) # False print(user1 is user1) # True print(user1 is user2) # False print(user1 is user3) # False print(user1 != user1) # False print(user1 != user2) # True print(user1 != user3) # True

Gdy definiujemy __eq__, powinna ona zawierać parametry self oraz other reprezentujące porównywane obiekty. Powinna też zwracać wartość logiczną odpowiadającą na pytanie, czy obiekty są sobie równe (True) czy też nie (False). Typowo zaczynamy od sprawdzenia, czy other jest tego samego typu, a następnie porównujemy jego właściwości. Do sprawdzenia typu używamy funkcji isinstance. Jako pierwszy argument używamy obiektu, którego typ chcemy sprawdzić, a jako drugi używamy nazwy klasy.

class A: pass class B: pass a = A() print(isinstance(a, A)) # True print(isinstance(a, B)) # False b = B() print(isinstance(b, A)) # False print(isinstance(b, B)) # True

Kiedy implementujemy metodę __eq__, wystarczy sprawdzić typ parametru other. Następnie kolejno porównujemy wartości istotnych dla nas atrybutów.

class User: def __init__(self, name): self.name = name def __eq__(self, other): return ( isinstance(other, User) and other.name == self.name ) user1 = User("Alek") user2 = User("Alek") user3 = User("Bolek") print(user1 == user1) # True print(user1 == user2) # True print(user1 == user3) # False print(user1 != user1) # False print(user1 != user2) # False print(user1 != user3) # True
# Operator `is` nie zmienia zachowania print(user1 is user1) # True print(user1 is user2) # False print(user1 is user3) # False

Oto jak mogłaby wyglądać klasa Position razem z zaimplementowaną metodą __eq__:

class Position: def __init__(self, x, y): self.x = x self.y = y def __eq__(self, other): return ( isinstance(other, Position) and self.x == other.x and self.y == other.y ) def __str__(self): return f"({self.x}, {self.y})" __repr__ = __str__ position1 = Position(10, 20) position2 = Position(10, 20) position3 = Position(1, 2) print(position1 == position2) # True print(position1 == position3) # False

W realnych projektach, gdy definiujemy __eq__, powinniśmy zdefiniować także __hash__. Objaśnienie tej metody wybiega jednak poza zakres tej książki.

Ćwiczenie: __str__, __repr__ i __eq__

Utwórz klasę Money z atrybutami amountcurrency. Ich wartości powinny być określane w konstruktorze. Dwa obiekty powinny być sobie równe, gdy wartości obu pól są takie same. Zamiana na stringa powinna zwracać kwotę i walutę oddzielone spacją. Oficjalna reprezentacja obiektu powinna ujawniać jego nazwę oraz atrybuty amount i currency.

money1 = Money(10.0, "PLN") money2 = Money(10.0, "PLN") money3 = Money(20.0, "PLN") money4 = Money(10.0, "EUR") print(money1 == money1) # True print(money1 == money2) # True print(money1 == money3) # False print(money1 == money4) # False print(money1) # 10.0 PLN print(money2) # 10.0 PLN print(money3) # 20.0 PLN print(money4) # 10.0 EUR print(repr(money1)) # Money(amount=10.0, currency='PLN') print(repr(money3)) # Money(amount=20.0, currency='PLN') print(repr(money4)) # Money(amount=10.0, currency='EUR')

Jednocześnie porównanie z inną klasą, nawet mającą takie same atrybuty, powinno zwracać False.

class FakeMoney: def __init__(self, amount, currency): self.amount = amount self.currency = currency money = Money(10, "PLN") fakeMoney = FakeMoney(10, "PLN") print(money == fakeMoney) # False

Odpowiedzi na końcu książki.

Operacje matematyczne i porównania

Python wspiera wiele operatorów, a nasze klasy mogą z nich skorzystać dzięki nadpisywaniu różnych metod specjalnych. Te możliwości są wykorzystywane przez wielu twórców pakietów. Doskonałym przykładem jest Pandas: pakiet powszechnie wykorzystywany przez osoby pracujące z danymi (analityków, inżynierów danych, statystyków). Zobaczymy go w akcji w rozdziale Analiza danych w części czwartej. Aby jednak dać Ci pewien pogląd na temat tego, jakie są możliwości, przedstawię kilka przykładów nadpisania różnych operatorów.

Jeśli chcemy umożliwić wykonywanie operacji matematycznych między obiektami, powinniśmy zdefiniować funkcje takie jak __add__[^204_4], __sub__[^204_5] czy __mul__[^204_6].

class Position: def __init__(self, x, y): self.x = x self.y = y def __str__(self): return f"({self.x}, {self.y})" def __add__(self, other): return Position(self.x + other.y, self.y + other.y) def __sub__(self, other): return Position(self.x - other.y, self.y - other.y) def __mul__(self, other): return Position(self.x * other, self.y * other) p1 = Position(1.0, 2.0) p2 = Position(3.0, 4.0) p3 = p1 + p2 print(p3) # (5.0, 6.0) p4 = p1 * 3 print(p4) # (3.0, 6.0)

Jeśli chcemy umożliwić porównywanie obiektów przy pomocy operatorów >, <, >= i <=, to powinniśmy zdefiniować metody __lt__[^204_7] czy __le__[^204_8].

class Position: def __init__(self, x, y): self.x = x self.y = y def __str__(self): return f"({self.x}, {self.y})" def __lt__(self, other): self_mag = (self.x ** 2) + (self.y ** 2) other_mag = (other.x ** 2) + (other.y ** 2) return self_mag < other_mag def __le__(self, other): self_mag = (self.x ** 2) + (self.y ** 2) other_mag = (other.x ** 2) + (other.y ** 2) return self_mag <= other_mag p1 = Position(1.0, 2.0) p2 = Position(3.0, 4.0) print(p1 > p2) # False print(p1 < p2) # True print(p1 < p1) # False print(p1 >= p2) # False print(p1 <= p2) # True print(p1 <= p1) # True

Zamiast nich moglibyśmy też nadpisać __gt__[^204_9] i __ge__[^204_10]. Metoda __gt__ powinna zwracać odwrotną wartość do __le__ (bo a > b powinno zwracać tę samą wartość co not (a <= b)), a metoda __ge__ wartość odwrotną do __lt__ (bo a >= b powinno zwracać tę samą wartość co not (a < b)).

Na koniec chciałbym przedstawić metodę powszechnie używaną przez wiele pakietów służących do odczytywania i zarządzania danymi. __getattr__[^204_11] pozwala nam zadecydować, co powinno się wydarzyć, gdy spróbujemy odczytać atrybut, który nie istnieje. W poniższym przypadku klasa Echo nie zawiera żadnych atrybutów, ale gdy pytamy o Aaa, Ooo i Hejaa, odpowiada tekstem "Echo: " oraz nazwą atrybutu (parametr item zawiera nazwę atrybutu).

class Echo: def __getattr__(self, item): return f"Echo: {item}" echo = Echo() print(echo.Aaa) # Echo: Aaa print(echo.Ooo) # Echo: Ooo print(echo.Hejaa) # Echo: Hejaa

Zakończenie

Jak widać, Python ma bardzo ciekawą funkcjonalność, pozwalającą nadawać obiektom specjalne zachowanie. Pozwala to na wykonywanie przeróżnych operacji na obiektach, co daje niesamowite możliwości twórcom pakietów. Jeszcze się o tym przekonamy, gdy już będziemy wykorzystywać różne pakiety.

[^204_1]: "str" to skrót od "string". [^204_2]: "repr" to skrót od "representation", czyli jak dany obiekt powinien być reprezentowany. [^204_3]: "eq" to skrót od "equals", czyli "równy". [^204_4]: "add" to skrót od "addition", czyli "dodawanie". [^204_5]: "sub" to skrót od "subtraction", czyli "odejmowanie". [^204_6]: "mul" to skrót od "multiplication", czyli "mnożenie". [^204_7]: "lt" to skrót od "less than", czyli "mniejszy od". [^204_8]: "le" to skrót od "less or equal", czyli "mniejszy lub równy". [^204_9]: "gt" to skrót od "greater than", czyli "większy od". [^204_10]: "ge" to skrót od "greater or equal", czyli "większy lub równy". [^204_11]: "getattr" to skrót od "get attribute", czyli "pobierz atrybut".