Какво представляват методите dunder/magic на Python и как можем да имплементираме такива методи в нашите собствени обекти (класове)?

Здравейте хора, в тази статия ще разгледаме какво представляват методите dunder/magic на Python и как можем да имплементираме такива в нашите собствени обекти (класове). Тази статия е продължение на моята серия от уроци по Python за обектно ориентирано програмиране. Ако искате да прочетете някои от тях, моля, посетете следните връзки: Въведение, Характеристики, Наследяване срещу композиция.

Магическите или dunder методи на Python са прости специални методи, които се изпълняват върху обекти по време на живота им. Можете лесно да забележите такива методи, защото всички те са предшествани от двойно подчертаване (напр. __init__). Ако се забърквате с тези методи, със сигурност можете, но ако не сте посъветвани, създавайте свои собствени методи. Това е главно защото в бъдеще такъв метод може да бъде дефиниран в стандартната библиотека и той основно ще срине вашия софтуер.

В предишните ни статии вече дефинирахме такива методи. Един от тях е методът __init__, който е метод за инициализация при създаване на обект. изглежда подобно на конструктор от други езици, но методът init всъщност не създава обекта, той просто го инициализира.

class Employee:
    def __init__(self, name, position, salary):
            self.__iter_data = list(locals().items())[1:]
            self.name = name
            self.position = position
            self.salary = salary
            self.__index = 0

В предходния код сме дефинирали клас Employee и сме го инициализирали с __init__. Можете да игнорирате self.__iter_data и self.__index засега (ще ни трябват по-късно за други специални методи). Нека създадем нашия първи обект.

>>> e1 = Employee('John Doe', 'Engineer', 1000)
>>> e2 = Employee('Jane Doe', 'Developer', 1500)
>>> print(e1.name)
'John Doe'
>>> print(e1)
<__main__.Employee object at 0x00000166008C8B50>

Както можете да видите, създадохме обект и извикваме някои от неговите атрибути (напр. име). Може би сте забелязали, че се опитахме да отпечатаме самия обект и това, което имаме, е обектът в паметта. Вероятно сте искали да получите по-подробна и информативна информация за обекта. Със сигурност можем да направим това, като заменим метода __str__. Това, което __str__ всъщност прави, е да изчислява „неформалното“ или добре отпечатано низово представяне на обект. Върнатата стойност трябва да бъде низов обект.

def __str__(self):
    return f'{self.name}:{self.position}:{self.salary}'

Този път, ако трябваше да отпечатаме нашия обект, ще получим напълно различен резултат.

>>> print(e1)
John Doe:Engineer:1000

Вече споменахме тези магически методи в предишна статия. Ами другите? Е, има много от тях и е трудно да разгледаме всеки един от тях в една статия. Поради това ще разгледаме няколко от тях и накрая ще дам някои полезни връзки за допълнително четене.

Много обекти в Python имат дължина. Следният списък например има дължина четири (в него има четири елемента).

>>> data = [1,2,3,4]
>>> len(data
4

Какво ще кажете за нашия обект? Имаме изключение TypeError, което ни казва, че нашият обект няма дефиниран метод len().

>>> len(e1)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: object of type 'Employee' has no len()

Под капака се извиква магическият метод __len__, за да върне дължината на обект. Тъй като нямаме дефиниран такъв метод, нашето извикване на len към клас Employee завършва с изключение. Нека променим това.

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

В нашия пример извикването на len към нашия обект ще върне броя на атрибутите, които нашият обект има. Ако разгледаме метода __init__, имаме три атрибута: име, позиция и заплата. __iter_data в нашия случай се предполага, че е променлива, която нашият клас използва само вътрешно. Тъй като Python няма частни променливи като други езици, ние използваме две долни черти преди името, за да покажем на колегите разработчици, че това трябва да се използва само в рамките на класа.

>>> print(len(e1))
3

Нека сега да разгледаме някои аритметични операции като събиране и изваждане. Python дефинира dunder методи и за тези операции и може би сте познали техните имена — __add__ и __sub__. В момента не можем да добавяме или изваждаме нашия обект и ще получим следните грешки, ако се опитаме да го направим.

>>> e1 + e2 
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'Employee' and 'Employee'

Python е достатъчно умен, за да ни каже, че нашият клас не поддържа операнд плюс. Можем лесно да дефинираме __add__ или __sub__ методи, но какво точно трябва да добавят или изваждат. За нашия клас основно бихме искали да добавим/извадим техните заплати, както следва.

def __add__(self, other):
    return self.salary + other.salary
def __sub__(self, other):
    return self.salary - other.salary

Ако се опитаме да съберем или извадим двамата служители, ще получим следния резултат.

>>> e1 + e2
2500
>>> e2 - e1
500

Преминаваме към следващата двойка интересни магически методи, които са __iter__ и __next__. Тези методи се използват главно, когато искаме да повторим конкретен обект. Например можем лесно да итерираме списък като този:

>>> data = [1,2,3,4,5]
>>> for item in data:
...     print(item)
1
2
3
4
5

Опитът да направим това за нашия клас ще завърши с грешка.

>>> for i in e1:
...     print(i) 
... 
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'Employee' object is not iterable

За да поправим това, нашият обект трябва да има двата магически метода, споменати по-горе. Има много опции за избор и основно зависи от вас да разберете какъв тип данни искате да обходите. Например, можем да направим следното.

def __iter__(self):    # Make object iterable
    return self
def __next__(self):
    try:
        val = self.__iter_data[self.__index]
        self.__index += 1
        return val
    except IndexError:
        raise StopIteration

Първо, трябва да направим нашия обект итерируем, като използваме метода __iter__. Това връща самия обект (нов обект на итератор). Второ, трябва да преминем през някои данни. В нашия случай ние сме дефинирали __iter_data в началото, което ще бъде много полезно в нашия случай. Както при всеки итератор, когато го изчерпим, трябва да повдигнем изключението StopIteration. Това може да се види, ако се върнем към нашия пример със списък и управляваме повторенията ръчно с __next__.

>>> data = [1,2] 
>>> idata = iter(data)
<list_iterator object at 0x0000029179FE8EB0>
>>> idata.__next__()
1
>>> idata.__next__()
2
>>> idata.__next__()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
StopIteration

Нашият персонализиран обект на служител ще има същото поведение, ако го изпълним ръчно.

# Driving throuhg for loop
>>> for data in e1:
...     print(data)    
... 
('name', 'John Doe')
('position', 'Engineer')
('salary', 1000)
# Driving it manually
>>> e1.__next__()
('name', 'John Doe')
>>> e1.__next__()
('position', 'Engineer')
>>> e1.__next__()
('salary', 1000)
>>> e1.__next__()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "main.py", line 34, in __next__
    raise StopIteration
StopIteration

Следващият интересен метод на dunder, който ще разгледаме, е __contains__. Методът прилага оператори за тест за членство. Трябва да върне true, ако item е в self, false в противен случай. За картографски обекти това трябва да вземе предвид ключовете на картографирането, а не стойностите или двойките ключ-елемент.

def __contains__(self, item):
    if item in str(self):
        return True
    return False

Това, което нашият метод ще направи, е да провери дали има ключови думи в низовото представяне на нашия обект. Ако са, ще върне True. Това може да бъде удобно, ако искаме бързо да проверим дали определена ключова дума съществува. Например.

>>> 'Engineer' in e1 
True
>>> 'Engineer' in e2
False

Последният магически метод, който ще спомена, е методът __call__. Много е полезно, ако искаме да извършим някаква операция върху обекта, но не искаме да извикваме изрично „външен“ метод върху него.

def call(self):
    print('Calling call method on object')
def __call__(self):
    print('Calling call() method on object')
>>> e1.call()
Calling call method on object
>>> e1()
Calling call() method on object

Заключение

В тази статия видяхме какви са методите dunder/magic в Python и как можем да ги използваме, за да персонализираме поведението на обекта, който създаваме. Някои интересни четива могат да бъдат намерени „тук“ и „тук“. Както винаги се надявам, че това е било информативно за вас и бих искал да ви благодаря, че ме прочетохте.

Повече съдържание в plainenglish.io. Регистрирайте се за нашиябезплатен седмичен бюлетин. Получете изключителен достъп до възможности за писане и съвети в нашата общност Discord.