Статьи
July 13, 2022

Пишем чистый код на Python с помощью Pipes

Функции map и filter эффективны при работе с итерируемыми объектами. Однако одновременное их использование может сделать код не таким чистым и читаемым, как нам бы того хотелось.

Рассмотрим пример:

arr = [1, 2, 3, 4, 5]

print(list(map(lambda x: x * 2, filter(lambda x: x % 2 == 0, arr))))
# [4, 8]

Почему бы не использовать пайпы |, чтобы применить к итерируемому объекту (в нашем случае — к списку) сразу несколько методов?

from pipe import select, where

arr = [1, 2, 3, 4, 5]

print(list(arr
     | where (lambda x: x % 2 == 0)
     | select (lambda x: x * 2)))
# [4, 8]

Именно это мы и можем сделать с помощью библиотеки Pipe.

Что такое Pipe?

Pipe — это библиотека Python, которая позволяет использовать пайпы (англ. pipe — трубка, канал) для передачи результатов работы одного метода другому.

Для установки Pipe достаточно воспользоваться следующей командой:

pip install pipe

Этот инструмент позволяет сделать код чище и удобнее для восприятия, когда над одним и тем же итерируемым объектом требуется произвести целый ряд операций. Некоторые из часто нужных операций уже реализованы внутри библиотеки, поэтому применение пайпов — процесс не сложный. О самых полезных пайп-методах мы и поговорим в этой статье.

where — фильтруем элементы в итерируемом объекте

Метод where из библиотеки Pipe действует точно так же, как и в SQL. С его помощью мы можем отфильтровать элементы итерируемых объектов, для которых истинно заданное условие.

В приведенном ниже примере с помощью данного метода выводится список четных чисел, так как для них истинно условие x % 2 == 0.

from pipe import where

arr = [1, 2, 3, 4, 5]

print(list(arr | where (lambda x: x % 2 == 0)))
# [2, 4]

select — применяем функцию к итерируемому объекту

Pipe-метод select действует аналогично функции map, применяя к каждому элементу итерируемого объекта заданную функцию.

В приведенном ниже примере метод select используется для умножения каждого элемента в списке на число 2.

from pipe import select

arr = [1, 2, 3, 4, 5]

print(list(arr | select (lambda x: x * 2)))
# [2, 4, 6, 8, 10]

Возможно, вы задались вопросом: «Зачем нужны методы select и where, если они делают то же самое, что и map вместе с filter?».

Дело в том, что Pipe-методы можно вставлять в код один за другим, просто используя символ |. Таким образом, мы избегаем большого количества вложенных скобок и улучшаем читаемость кода.

Разглаживаем итерируемые объекты

chain — разглаживаем один уровень

Когда один итерируемый объект вложен в другой, работать с ними становится проблематично. Можно использовать метод chain, позволяющий раскрыть вложенные итерируемые объекты.

from pipe import chain

nested = [[1, 2, [3]], [4, 5]]

print(list(nested | chain))
# [1, 2, [3], 4, 5]

Полученный в примере список стал содержать меньше вложенных итерируемых объектов, однако не всегда мы можем избавиться от них полностью. В частности, вам не удастся применить метод chain к итерируемому объекту, если он содержит хотя бы один НЕ итерируемый объект.

traverse — рекурсивно разглаживаем итерируемые объекты

Чтобы полностью разгладить все вложения, воспользуемся методом traverse. Для списка nested из приведенного выше примера результат будет выглядеть следующим образом:

from pipe import chain

nested = [[1, 2, [3]], [4, 5]]

print(list(nested | traverse))
# [1, 2, 3, 4, 5]

Теперь попробуем использовать методы select и traverse одновременно, чтобы достать значения из словаря и раскрыть все вложения.

from pipe import select, traverse

fruits = [
          {"name": "яблоко", "price": [20, 50]},
          {"name": "апельсин", "price": 40},
          {"name": "грейпфрут", "price": 50},
]

print(list(fruits
     | select(lambda fruit: fruit["price"])
     | traverse))
# [20, 50, 40, 50]

Группируем элементы в списке

Иногда необходимо распределить элементы по группам в уже существующем списке. И метод groupby может нам в этом помочь.

В качестве примера возьмем простой список чисел и сделаем из него словарь, разделяющий эти числа на чётные и нечётные.

from pipe import groupby, select

arr = [1, 2, 3, 4, 5]

print(list(arr
     | groupby(lambda x: "Even" if x % 2 == 0 else "Odd")
     # [('Even', <itertools._grouper at 0x7fc90fe4cf10>),
     # ('Odd', <itertools._grouper at 0x7fc90fe4c4d0>)]
     | select(lambda x: {x[0]: list(x[1])})))
     # [{'Even': [2, 4]}, {'Odd': [1, 3, 5]}]

А теперь достанем из полученных групп элементы, имеющих значение большее, чем 2.

from pipe import groupby, select, where

arr = [1, 2, 3, 4, 5]

print(list(arr
     | groupby(lambda x: "Even" if x % 2 == 0 else "Odd")
     # [('Even', <itertools._grouper at 0x7fc90fe4cf10>),
     # ('Odd', <itertools._grouper at 0x7fc90fe4c4d0>)]
     | select(lambda x: {x[0]: list(x[1] | where(lambda x: x > 2))})))
     # [{'Even': [4]}, {'Odd': [3, 5]}]

dedup — удаляем дубликаты из списка

from pipe import dedup

arr = [1, 1, 2, 2, 3, 3, 3, 4, 5, 6, 6, 7, 9]

print(list(arr | dedup))
# [1, 2, 3, 4, 5, 6, 7, 9] 

В принципе, метод set делает то же самое, но за одним исключением: dedup является более гибким, т.к. позволяет получить уникальные элементы по любому предварительному преобразованию.

Следующий блок кода позволяет получить два элемента, один из которых меньше 5, а другой — больше или равен 5.

from pipe import dedup

arr = [1, 1, 2, 2, 3, 3, 3, 4, 5, 6, 6, 7, 9]

print(list(arr | dedup(lambda key: key < 5)))
# [1, 5] 

И напоследок используем dedup вместе с select и where.

from pipe import select, where, dedup

data= [
       {"name": "яблоко", "count": 20},
       {"name": "апельсин", "count": 40},
       {"name": "грейпфрут", "count": None},
       {"name": "апельсин", "count": 70},
]

print(list(fruits
     | dedup(key = lambda fruit: fruit["name"])
     | select(lambda fruit: fruit["count"])
     | where(lambda count: isinstance(count, int))))
# [20, 40]

В конечном итоге мы отсеяли элементы с одинаковыми значениями в name и None-значениями в count.

Заключение

Теперь вы знаете, что представляют собой пайпы, и как с их помощью сделать свой код чище. Надеемся, что полученные знания помогут вам превращать даже самые сложные операции в короткие строки кода.

Источник: Towards Data Science

👉🏻Подписывайтесь на PythonTalk в Telegram 👈🏻

👨🏻‍💻Чат PythonTalk в Telegram💬

🍩 Поддержать канал 🫶