article banner (priority)

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 repr2, 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 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__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__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.

1:

"str" to skrót od "string".

2:

"repr" to skrót od "representation", czyli jak dany obiekt powinien być reprezentowany.

3:

"eq" to skrót od "equals", czyli "równy".

4:

"add" to skrót od "addition", czyli "dodawanie".

5:

"sub" to skrót od "subtraction", czyli "odejmowanie".

6:

"mul" to skrót od "multiplication", czyli "mnożenie".

7:

"lt" to skrót od "less than", czyli "mniejszy od".

8:

"le" to skrót od "less or equal", czyli "mniejszy lub równy".

9:

"gt" to skrót od "greater than", czyli "większy od".

10:

"ge" to skrót od "greater or equal", czyli "większy lub równy".

11:

"getattr" to skrót od "get attribute", czyli "pobierz atrybut".