Method chaining#
Often, you need to perform several operations with data, for example:
In [1]: line = "switchport trunk allowed vlan 10,20,30" In [2]: words = line.split() In [3]: words Out[3]: ['switchport', 'trunk', 'allowed', 'vlan', '10,20,30'] In [4]: vlans_str = words[-1] In [5]: vlans_str Out[5]: '10,20,30' In [6]: vlans = vlans_str.split(",") In [7]: vlans Out[7]: ['10', '20', '30']
line = "switchport trunk allowed vlan 10,20,30" words = line.split() vlans_str = words[-1] vlans = vlans_str.split(",") print(vlans)
In this case, variables are used to store the intermediate result and subsequent methods/actions are performed with the variable. This is a completely normal version of the code, especially at first when it’s hard perceive more complex expressions.
However, in Python, there are often expressions in which actions or methods are applied one after the other in one expression. For example, the previous code could be written like this:
line = "switchport trunk allowed vlan 10,20,30" vlans = line.split()[-1].split(",") print(vlans)
Since there are no expressions in parentheses that would indicate the priority of execution, everything is executed from left to right.
First, line.split() is executed — we get the list, then to the resulting list applies [-1] — we get the last element of the list, the line 10,20,30 . The method split(«,») is applied to this line and as a result we get the list [’10’, ’20’, ’30’] .
The main nuance when writing such chains, the previous method/action should return something what the next method/action is waiting for. And it is imperative that something is returned, otherwise there will be an error.
Explain Python class method chaining
In OOP languages methods are a piece of code that can perform a specific task to produce a desired output. Methods are created inside classes and are invoked or called by objects of the class.
Methods are extremely useful in programming as they provide modularity to code by breaking a complex code into manageable blocks. Methods can function independently of other methods, thus making it easy to check individual functionality within a program.
Method chaining in Python
Methods chaining is a style of programming in which invoking multiple method calls occurs sequentially. It removes the pain of assigning variables at each intermediate step as each call performs action on same object and then returns the object to next call.
Method chaining has two useful benefits −
- It can reduce the length of the overall code as countless variables do not have to be created.
- It can increase the readability of the code since methods are invoked sequentially.
Example
In this simple example of method chaining in Python, different CalculatorFunctions contains multiple methods which need to be invoked by calculator object. Instead of invoking one after the other, all the functions are chained and invoked in a single line.
In order for method chaining to work, every method must return an object of the class, which in this case is self.
class CalculatorFunctions(): def sum(self): print("Sum called") return self def difference(self): print("Difference called") return self def product(self): print("Product called") return self def quotient(self): print("Quotient called") return self if __name__ == "__main__": calculator = CalculatorFunctions() # Chaining all methods of CalculatorFunctions calculator.sum().difference().product().quotient()
Output
Following is an output of the above code −
Sum called Difference called Product called Quotient called
Example
In this example of chaining built-in methods in python, the days are first split by space, then the last element (Saturday,Sunday) is split by comma to output them as separate entities.
days_of_week = "Monday Tuesday Wednesday Thursday Friday Saturday,Sunday" weekend_days = days_of_week.split()[-1].split(',') print(weekend_days)
Output
Following is an output of the above code −
Method Chaining Using Pandas
Pandas is a python package used for solving complex problems in fields of data science and machine learning. The pandas package has many built-in methods that can be chained together to reduce code length.
Example
chaining pandas methods
In this example a csv file is first read and assigned to a data frame. After that different methods of pandas are chained together to manipulate the CSV file.
The .assign() method creates Percentage column, .drop deletes an Gender column, .sort_value sorts the data based on Percentage and .head gives the top three results from the CSV file.
import pandas as pd data_frame = pd.read_csv('E:/Marks.csv', index_col = 0) # Chaining different methods of pandas chained_data_frame = (data_frame.assign(Percentage = (data_frame['Marks']*100)/70) .drop(columns = 'Gender') .sort_values('Percentage', ascending = False) .head(3)) print(chained_data_frame)
Output
Following is an output of the above code −
Age Marks Percentage ID 4 40 68 97.142857 2 20 65 92.857143 3 30 60 85.714286
Method Chaining Using NumPy
NumPy is a python package that provides support for multidimensional array objects and multiple routines to perform extremely fast operations on arrays. Just like pandas methods, NumPy methods can also be chained.
Example
chaining NumPy methods
In this example different NumPy methods are chained to create a 4×4 matrix. The .arange() method creates a matrix from 1 to 32 with step count of 2, .reshape adjusts the matrix into a 4×4 configuration and .clip is used to set minimum element to 9 and maximum to 25 in the matrix.
import numpy as np # Chaining different methods of numpy chained_numpy = np.arange(1, 32, 2).reshape(4, 4).clip(9, 25) print(chained_numpy)
Output
Following is an output of the above code
[[ 9 9 9 9] [ 9 11 13 15] [17 19 21 23] [25 25 25 25]]
Цепочка вызова методов в Python
Каждый программист сталкивался с цепочками вызова методов, но не все задумываются о том, как реализуется данный паттерн. Принцип использования цепочки вызова методов заложен в API множества программных продуктов, например, Django QuerySet, селекторы jQuery, elasticsearch-dsl Query и так далее.
Зачем это нужно? Такой подход повышает читабельность программы. Сравните два примера:
car = Car() car.color('red') car.mark('BMW') car.model('M3')
car = Car().color('red').mark('BMW').model('M3')
Второй пример короче и проще читается.
Цепочка вызова является реализацией текучего интерфейса, вся суть которого заключается в том, что в результате вызова любого метода необходимо вернуть указатель на текущий объект класса, что позволяет вызывать методы по цепочке.
Тут есть нюанс. В классической реализации методы изменяют состояние объекта, на котором вызываются, что приводит к нарушению принципа DRY и не позволяет делать такие вещи:
# задача — создать две красных машины разных моделей car = Car().color('red').mark('BMW') m3 = car.model('m3') m4 = car.model('m4')
В данном случае объект класса Car — изменяемый (mutable), а поэтому id(car) == id(m3) == id(m4) , то есть это один объект.
Данное поведение не является ошибкой, когда действительно нужно изменять состояние объекта.
В противном случае, на каждом шаге необходимо возвращать копию объекта (необходимо учитывать повышенный расход памяти и процессорного времени). Такой подход используется в Django QuerySet :
active_records = Record.objects.active() # QuerySet с базовым состоянием new_records = active_records.new() # Копия предыдущего QuerySet с фильтром по новым записям old_records = active_records.old() # Ещё одна копия QuerySet с фильтром по старым записям
Итак, привожу мою реализацию, с тестами. Скачать исходник вы можете из этого gist:
import copy from collections import namedtuple, Sequence Item = namedtuple('Item', 'name, price') items = [ Item('apple', 10.0), Item('banana', 12.0), Item('orange', 8.0), Item('coconut', 50.0), ] class Query(Sequence): def __init__(self, items): self.items = items def __getitem__(self, i): return self.items[i] def __len__(self): return len(self.items) def _clone(self): return copy.deepcopy(self) def first(self): q = self._clone() return next(iter(q), None) def last(self): q = self._clone() try: return q[-1] except KeyError: return None def values(self, attr): q = self._clone() return [getattr(o, attr) for o in q] def filter(self, cond): """ Filter by condition. Condition must be a function taking one argument (an object), and returning True or False. """ q = self._clone() return Query([o for o in q if cond(o)]) @property def total(self): return sum(self.values('price')) if __name__ == "__main__": q = Query(items) q2 = Query(items) assert id(q.items) != id(q2.items) assert q.first() == items[0] assert q.last() == items[-1] assert q.values('name') == ['apple', 'banana', 'orange', 'coconut'] assert q.values('price') == [10.0, 12.0, 8.0, 50.0] assert q.filter(lambda x: x.price > 30).values('name') == ['coconut'] assert q.filter(lambda x: x.price > 30).total == 50.0 assert q.total == 80.0
Данный пример является лишь демонстрацией и говорит сам за себя. Следует учитывать, что при вызове каждого метода, происходит копирование объекта вместе со всем набором данных. Для данного конкретного случая этот момент можно оптимизировать, но уже в рамках другой статьи 🙂
Вызов методов цепочкой#
Часто с данными надо выполнить несколько операций, например:
In [1]: line = "switchport trunk allowed vlan 10,20,30" In [2]: words = line.split() In [3]: words Out[3]: ['switchport', 'trunk', 'allowed', 'vlan', '10,20,30'] In [4]: vlans_str = words[-1] In [5]: vlans_str Out[5]: '10,20,30' In [6]: vlans = vlans_str.split(",") In [7]: vlans Out[7]: ['10', '20', '30']
line = "switchport trunk allowed vlan 10,20,30" words = line.split() vlans_str = words[-1] vlans = vlans_str.split(",") print(vlans)
В этом случае переменные используются для хранения промежуточного результата и последующие методы/действия выполняются уже с переменной. Это совершенно нормальный вариант кода, особенно поначалу, когда тяжело воспринимать более сложные выражения.
Однако в Python часто встречаются выражения, в которых действия или методы применяются один за другим в одном выражении. Например, предыдущий код можно записать так:
line = "switchport trunk allowed vlan 10,20,30" vlans = line.split()[-1].split(",") print(vlans)
Так как тут нет выражений в скобках, которые бы указывали приоритет выполнения, все выполняется слева направо. Сначала выполняется line.split() — получаем список, затем к полученному списку применяется [-1] — получаем последний элемент списка, строку 10,20,30 . К этой строке применяется метод split(«,») и в итоге получаем список [’10’, ’20’, ’30’] .
Главный нюанс при написании таких цепочек предыдущий метод/действие должен возвращать то, что ждет следующий метод/дествие. И обязательно чтобы что-то возвращалось, иначе будет ошибка.