Operatory w Pythonie
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__
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
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__
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 amount
i currency
. 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__
4, __sub__
5 czy __mul__
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__
7 czy __le__
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__
9 i __ge__
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__
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.
"str" to skrót od "string".
"repr" to skrót od "representation", czyli jak dany obiekt powinien być reprezentowany.
"eq" to skrót od "equals", czyli "równy".
"add" to skrót od "addition", czyli "dodawanie".
"sub" to skrót od "subtraction", czyli "odejmowanie".
"mul" to skrót od "multiplication", czyli "mnożenie".
"lt" to skrót od "less than", czyli "mniejszy od".
"le" to skrót od "less or equal", czyli "mniejszy lub równy".
"gt" to skrót od "greater than", czyli "większy od".
"ge" to skrót od "greater or equal", czyli "większy lub równy".
"getattr" to skrót od "get attribute", czyli "pobierz atrybut".