Functools, Itertools, Collections и с чем это всё едят

от автора

Небольшое вступление

Сегодня я хочу поговорить о трёх прекрасных библиотеках Python, которые очень сильно облегчают жизнь в разных сценариях жизни. Возможно не всем они приглянутся, не всем понравятся такие методы или решения, но это мой выбор и не вижу в нём ничего плохого. Моё желание заключается лишь в том, чтобы познакомить с этими библиотеками начинающих, которые только втягиваются в работу с Python.

Хочу сказать, во-первых, это моя вторая статья на данной площадке, я ещё только начинаю писать и, так скажем, это всё пока что ещё проба пера. Во-вторых, я приветствую любое мнение окружающих, но ещё лучше, если это будет конструктивная критика по делу, а не просто так, и да, примеры мои сугубо учебные, придуманные на ровном месте, не обязательно из каких-то реальных проектов и задач, поэтому не стоит придерживаться к ним в комментариях. А теперь к сути дела.

Itertools – полезным итераторам нет конца

Как можно догадаться из названия, itertools – это кладезь полезных итераторов, которым можно найти огромное количество разных вариантов применений. Пройдёмся по всему порядку:

  • itertools.count(start, step) — бесконечная арифметическая прогрессия На вход принимаем до двух аргументов: start и step. Start – начало прогрессии, step – шаг, с которым будет двигаться прогрессия. Вот пример с обращением к подобному итератора, простой и наглядный:

arithmetic_progression = count(2, 5) print(next(arithmetic_progression), end=" ") print(next(arithmetic_progression), end=" ") print(next(arithmetic_progression), end=" ")  # Вывод: 2 7 12 
  • itertools.cycle(iterable) — возвращает по одному значению из последовательности, повторенной бесконечное количество раз, то есть зацикленной. На вход данная функция принимает любое итерируемое значение.

list_of_books_names = ["Война и мир", 'Отцы и дети', 'На дне'] infinity_iteration = cycle(list_of_books_names)  print(next(infinity_iteration)) print(next(infinity_iteration)) print(next(infinity_iteration)) print(next(infinity_iteration)) 
  • itertools.repeat(element, n) — создает итератор, который многократно возвращает один и тот же объект. На вход принимает два значения: какой-нибудь элемент, а также сколько раз его надо повторить. По умолчанию значение n равно бесконечности.

result = list(repeat('Привет', 4))  print(result)  # Вывод: ['Привет', 'Привет', 'Привет', 'Привет'] 
  • itertools.accumulate(iterable) — аккумулирует суммы, но если объяснять проще, то он берёт все итерируемые числа по порядку и складывает их с предыдущим числом, но уже из нового списка. Проще показать на примере:

original_list = [4, 1, 7, 3, 10] new_list = list(accumulate([4, 1, 7, 3, 10]))  print(new_list)  # Вывод: [4, 5, 12, 15, 25] 

Давайте разберём как это работает, первый элемент исходного списка(original_list) суммируется с предыдущим элементом нового списка, но так как такового нет, то остаётся просто четыре. Далее следующий элемент единица суммируется с предыдущим элементом нового списка, то есть с четвёркой и вот уже второй элемент нового списка равный пяти.

  • itertools.chain(iterable) — принимает на вход некоторое количество итерируемых объектов, последовательно берёт из каждого элемента по одному с самого начала и формирует новый итерируемый объект.

bin_number = list(chain([1, 0, 0, 1], [0, 1, 1, 1], [0, 1, 1, 0]))  print(bin_number)  # Вывод: [1, 0, 0, 1, 0, 1, 1, 1, 0, 1, 1, 0] 
  • itertools.combinations(iterable, r) — возвращает все возможные комбинации нужной длины без повторяющихся элементов. На вход принимает итерируемый объект и длину комбинации.

list_of_combinations = list(combinations("ABCD", 2))  print(list_of_combinations)  # Вывод: [('A', 'B'), ('A', 'C'), ('A', 'D'), ('B', 'C'), ('B', 'D'), ('C', 'D')] 
  • itertools.combinations_with_replacement(iterable, r) — возвращает все возможные комбинации нужной длины только уже с повторяющимися элементами.

list_of_combinations = list(combinations_with_replacement("ABCD", 2))  print(list_of_combinations)  # Вывод: [('A', 'A'), ('A', 'B'), ('A', 'C'), ('B', 'B'), ('B', 'C'), ('C', 'C')] 
  • itertools.compress(iterable, mask) — этот метод позволяет фильтровать итерируемый объект по заданной маске. На вход принимаем сначала итерируемый объект, а потом маску состоящую из 0/1 или из логических значений (True/False).

a = compress("BBABCCA", [1,0,0,1,1,1,0]) print(list(a))   # Вывод: ['B', 'B', 'C', 'C'] 
  • itertools.filterfalse(func, iterable) — возвращает итератор из значений, которые при постановке в функцию вернули значение False.

def is_perfect_power(n): """ Проверяет, является ли число степенью """     if n == 1: return True     if n <= 0: return False     max_exponent = math.floor(math.log2(n)) + 1     for m in range(2, max_exponent + 1):         k = round(n ** (1 / m))         if k ** m == n: return True     return False  no_power_of_nums = filterfalse(is_perfect_power, [1, 4, 9, 81, 16, 99, 100, 110, 101, 43, 25, 15])  # Вывод: [99, 110, 101, 43, 15] 
  • itertools.dropwhile(func, iterable) — работает почти также, как и itertools.filterfalse, но с небольшим отличием. Оно заключается в том, что dropwhile вернёт ту часть итерируемого объекта, которая идёт после элемента, который выдал значения False при использовании в func. Возьму тот же пример:

def is_perfect_power(n): """ Проверяет, является ли число степенью """     if n == 1: return True     if n <= 0: return False     max_exponent = math.floor(math.log2(n)) + 1     for m in range(2, max_exponent + 1):         k = round(n ** (1 / m))         if k ** m == n: return True     return False  no_power_of_nums = dropwhile(is_perfect_power, [1, 4, 9, 81, 16, 99, 100, 110, 15])  # Вывод: [99, 100, 110, 15] 
  • itertools.groupby(iterable, key=None) — эта функция создает итератор, который возвращает последовательность ключей и групп из итерируемой последовательности iterable. Под ключом(key) здесь подразумевается функция. Например:

# Создаём повторяющийся список x = list('AaBbCc' * 3)  # Промежуточный вывод: ['A', 'a', 'B', 'b', 'C', 'c', 'A', 'a', 'B', 'b', 'C', 'c', 'A', 'a', 'B', 'b', 'C', 'c']  x.sort() for key, elements in groupby(x):     print(key, list(elements))  # Итоговый вывод:  A ['A', 'A', 'A'] B ['B', 'B', 'B'] C ['C', 'C', 'C'] a ['a', 'a', 'a'] b ['b', 'b', 'b'] c ['c', 'c', 'c'] 
  • itertools.islice(iterable, start, stop, step=1) — это итератор, состоящий из среза другого итерируемого объекта.

a = islice("Hello, world!", 7, 13) print(list(a))  # Вывод: ['w', 'o', 'r', 'l', 'd', '!'] 

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

a = islice("Hello, world!", 7) print(list(a))  # Вывод: ['H', 'e', 'l', 'l', 'o', ',', ' '] 
  • itertools.permutations(iterable, R=None) — возвращает итератор, состоящий из перестановок размером с параметр R. Пример 1:

a = permutations("ABC") print(list(a))  # Вывод: [('A', 'B', 'C'), ('A', 'C', 'B'), ('B', 'A', 'C'), ('B', 'C', 'A'), ('C', 'A', 'B'), ('C', 'B', 'A')] 

Пример 2:

a = permutations("ABC", 2) print(list(a))  # Вывод: [('A', 'B'), ('A', 'C'), ('B', 'A'), ('B', 'C'), ('C', 'A'), ('C', 'B')] 
  • itertools.product(*iterables, repeat=1) — возвращает итератор, состоящий из перестановок с длиной repeat, но в отличии от permutations здесь итерируемые объекты повторяются. Можно указать несколько итерируемый объектов. Пример 1:

a = product("AC", repeat=3) print(list(a))  # Вывод: [('A', 'A', 'A'), ('A', 'A', 'C'), ('A', 'C', 'A'), ('A', 'C', 'C'), ('C', 'A', 'A'), ('C', 'A', 'C'), ('C', 'C', 'A'), ('C', 'C', 'C')] 

Пример 2:

a = product("AB", "12", repeat=2) print(list(a))  # Вывод: [('A', '1', 'A', '1'), ('A', '1', 'A', '2'), ('A', '1', 'B', '1'), ('A', '1', 'B', '2'), ('A', '2', 'A', '1'), ('A', '2', 'A', '2'), ('A', '2', 'B', '1'), ('A', '2', 'B', '2'), ('B', '1', 'A', '1'), ('B', '1', 'A', '2'), ('B', '1', 'B', '1'), ('B', '1', 'B', '2'), ('B', '2', 'A', '1'), ('B', '2', 'A', '2'), ('B', '2', 'B', '1'), ('B', '2', 'B', '2')] 
  • itertools.starmap(func, iterable) — применяет функцию к каждому элементу итерируемого объекта

data = [(2, 5), (3, 2), (10, 3)]  result = starmap(pow, data)  print(list(result))  # Вывод: [32, 9, 1000] 
  • itertools.takewhile(func, iterable) — тоже самое, что и dropwhile, только наоборот. Возвращает итератор из бывшего итерируемого объекта, пока элементы возвращают True из функции. Пример всё тот же:

def is_perfect_power(n): """ Проверяет, является ли число степенью """     if n == 1: return True     if n <= 0: return False     max_exponent = math.floor(math.log2(n)) + 1     for m in range(2, max_exponent + 1):         k = round(n ** (1 / m))         if k ** m == n: return True     return False  power_of_nums = takewhile(is_perfect_power, [1, 4, 9, 81, 16, 99, 100, 110, 101, 43, 25, 15])  # Вывод: [1, 4, 9, 81, 16] 
  • itertools.tee(iterable, n=2) — возвращает кортеж из n итераторов.

a = tee("AB", 3) print([list(x) for x in a])  # Вывод: [['A', 'B'], ['A', 'B'], ['A', 'B']] 
  • itertools.zip_longest(*iterables, fillvalue=None) — работает примерно как встроенная функция zip, но берёт самый длинный итератор, объединяет с самым коротким попарно, а оставшиеся элементы без пары дополняет символом указанном в параметре fillvalue.

a = zip_longest('ABCDE', '01', fillvalue='*') print(list(a))  # Вывод: [('A', '0'), ('B', '1'), ('C', '*'), ('D', '*'), ('E', '*')] 

С библиотекой itertools на этом всё. Как по мне весьма полезная библиотека, сам ей пользуюсь периодически, хоть и не всеми возможными функциями, лишь некоторыми, но всё равно использую, а пока писал эту статью узнал большое количество новых для себя функций и возможно буду применять их в своей работе.

Collections — предоставляет новые типы данных на основе уже имеющихся

  • collections.Counter — особый вид словаря, который может посчитать количество элементов в итерируемом объекте.

data = ["Aabb", "AAbb", "AaBB", "Aabb", "Aabb", "AAbb", "AaBb"]  result = Counter(data)  # Вывод: Counter({'Aabb': 3, 'AAbb': 2, 'AaBB': 1, 'AaBb': 1}) 

У Counter есть парочку своих особых методов:
1. elements() — возвращает все элементы в алфавитном порядке.
2. most_common(n) — возвращает n самых часто встречающихся элементов в словаре Counter, выводит всё в порядке убывания. Если n не указать, то вернёт все элементы.
3. subtract(iterable or mapping) — позволяет вычитать из Counter либо же словари такого же типа Counter, либо обычные словари.
Также словари типа Counter поддерживают обычное сложение, вычитание, объединение(&) и пересечение(|).

  • collections.deque(iterable, maxlen) — создаёт очередь из итерируемого объекта, где максимальная длина задаётся переменной maxlen. Очереди похожи на списки, но элементы добавляются справкой стороны очереди, либо с левой, то же самое и с удалением элементов.
    Очереди поддерживают большинство методов списков, такие как: append, clear, count, pop, extend, remove, reverse. Также к обычным методам добавляются всё те же, только, которые производят действия с левой стороны очереди: appendleft, extendleft, popleft. Помимо этого есть ещё один новый метод – rotate(n). Данный метод переносит n элементов из начала очереди в конец. Если указать n отрицательным, то переносит наоборот из конца в начало.

  • collections.defaultdict(type) — тип данных, почти ничем не отличающийся от обычного словаря (dict), за исключением того, что он не выдаёт ошибку типа KeyError, так как при попытке обратиться

defdict = defaultdict(list)  defdict[1].append("Ivan")  print(defdict)  # Вывод:  
  • collections.OrderedDict — ещё одна разновидность словарь, которая в первую очередь отличается тем, что запоминает порядок ключей, записанных в этот словарь. Конечно, классический dict это тоже делает, но только начиная с версии Python 3.7, до этого вывод был не упорядоченным. Методы:

  1. popitem(last=True) — Удаляет последний элемент словаря, но если указать аргумент last = False, то наоборот удалит первый элемент словаря

  2. move_to_end(key, last=True) — перемещает существующий ключ в конец словаря, но если указать аргумент last = False, то в начало словаря.

  • collections.namedtuple() — это своего рода класс, состоящий только из полей данных, без методов, и который ведёт себя как кортеж, то есть не изменяется.

Man = namedtuple('Ivan', ["Grade", "Surname"])  man1 = Man(5, "Pupkin") print(man1) print(man1.Grade)  # Вывод: Ivan(Grade=5, Surname='Pupkin') 5 

Functools — это своего рода сборник функций более высокого уровня, пригодится конечно не всегда, но есть пару полезных моментов

Начнём с моего любимого:

  • @lru_cache(maxsize=128, typed=False) — это декоратор, который кэширует данные функции. Такое может пригодиться, чтобы сэкономить время при очень дорогих вычислениях, если вы вызываете функцию с одними и теми же аргументами. Параметр maxsize отвечает за максимальный размер кэша, если установить значение None, то кэш будет возрастать бесконечно. Параметр typed отвечает за кэширование аргументов функции в том случаи, если у них отличаются типы. Например, если у вас функция запускается с аргументом 3(int) и 3.0(float), то при значении аргумента typed=True, эти случаи будут кэшироваться отдельно, так как у них разные типы данных.

@lru_cache(maxsize=None) def fib(n):      if n < 2: return n      return fib(n-1) + fib(n-2)  print([fib(n) for n in range(10)]) 

Самый наглядный и распространённый пример это числа Фибоначчи. Если хотите убедиться, то попробуйте запустить этот же алгоритм, но без декоратора и с большими числами (100, 500, 1000…), увидите, что это будет слишком длительные вычисления.
Для этого декоратора также существуют две дополнительные функции:
1. cache_info() — возвращающая namedtuple, отображающий: попадания в кэш, промахи, максимальный размер и текущий размер кэша.
2. cache_clear() — очищает данные кэша.

  • functools.cmp_to_key(func) — Используется с пользовательскими инструментами, которые в качестве ключа сортировки принимают подобные функции: sorted(), max, min, itertools.groupby(о которой как раз-таки говорили ранее), heapq.nlargest, heapq.nsmallest . Эта функция в основном используется в качестве инструмента перехода для программ, конвертируемых из Python 2. Сейчас местами это конечно не очень актуально, но как по мне знать такое полезно. Пояснение: В версии Python 2.4 или более ранних версиях, как таковой функции sorted() не было, а обычный list.sort() не принимал аргументов с ключевыми словами для сортировки. Вместо этого, все версии Pyton 2.X поддерживали параметр cmp для обработки пользовательских функций сравнения.

В Pyton 3.0 параметр cmp был удален для устранения конфликта между «богатыми сравнениями» и «магическим» методом __cmp__().

В Python 2 можно было писать:

numbers = [5, 2, 8, 1, 3] sorted_numbers = sorted(numbers, cmp=lambda a, b: b - a)  # Вывод: [8, 5, 3, 2, 1] 

В Python 3 подобное реализовывалось бы так:

def compare(a, b):     if a < b:         return 1     elif a == b:         return 0     else:         return -1  numbers = [5, 2, 8, 1, 3] sorted_numbers = sorted(numbers, key=cmp_to_key(compare))  # Вывод: [8, 5, 3, 2, 1] 

В целом данная функция нужна для сортировок со сложными условиями, например, если нужно сравнивать объекты по нескольким полям с разной логикой, а также данная функция необходима при портировании кода с Python 2 на Python 3.

  • @functools.total_ordering — это декоратор, который помогает создавать объекты с «богатым сравнениям», то есть, если вы хотите создать класс, который можно будет сравнивать обычными знаками сравнения (<, >, =, <=, >=, !=), то вам пришлось бы вручную определить все шесть соответствующих «магических» методов: __lt__, __le__, __gt__, __ge__, __eq__, __ne__. Но этот декоратор делает это за вас. Основное правило для использования @total_ordering простое:

  1. Примените декоратор @total_ordering к вашему классу.

  2. Обязательно определите в классе метод __eq__(self, other) для проверки на равенство.

  3. Определите хотя бы один из следующих методов:

    • __lt__(self, other) для операции «меньше» (<)

    • __le__(self, other) для операции «меньше или равно» (<=)

    • __gt__(self, other) для операции «больше» (>)

    • __ge__(self, other) для операции «больше или равно» (>=) Этого достаточно. Декоратор проанализирует существующие методы и доопределит все остальные, используя логические связи между ними. Например, если вы определили __eq__ и __lt__, то декоратор создаст __gt__, __le__ и __ge__ на их основе .

  • functools.partial(func, *args, **kwargs) — это функция, которая позволяет создавать новые функции путем «замораживания» или фиксации некоторых аргументов существующей функции . Этот механизм называется частичным применением (partial application). Проще говоря, partial берет функцию и часть ее аргументов, а на выходе дает новую, более простую функцию, которая при вызове уже будет знать об этих «замороженных» аргументах. Например:

base_two = partial(int, base=2) # Функция конвертирует строку с записью двоичного кода в десятичное число типа int  print(basetwo('11001011'))  # Вывод: 203 
  • functools.reduce(function, iterable) — эта функция которая работает по принципу сворачивания всего итерируемого объекта в одно значения. То есть, оно берёт первые два значения итерируемого объекта, применяет к ним функцию и получает новое одно значения, далее берёт третье значение из итерируемого объекта и к нему и ранее полученному значения применяет функцию и так далее, пока не получит одно единственное значение.

result = reduce(lambda a, b: a**2+b**2, [1,6, 8, 0, 2])  print(result)  # Вывод: 4216817073125 
  • functools.update_wrapper(wrapper, wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES) — используется для корректного копирования метаданных (например, имени, документации, аннотаций) из оригинальной функции в её обёртку (декоратор или другую функцию-заменитель). Когда вы создаёте декоратор или любую обёртку вокруг функции, Python по умолчанию теряет метаданные оригинальной функции (например, __name____doc____module____annotations__).

Функция обновляет у обёртки (wrapper) следующие атрибуты из исходной функции:

  1. __module__

  2. __name__

  3. __qualname__ (в Python 3.3+)

  4. __doc__

  5. __annotations__ (в Python 3+)

  6. __dict__ (дополнительные атрибуты)

Например:

def my_decorator(func):     def wrapper(*args, **kwargs):         result = func(*args, **kwargs)         return result     update_wrapper(wrapper, func)  # Копируем метаданные из func в wrapper     return wrapper  @my_decorator def greet(name):     print(f"Привет, {name}!")  print(greet.__name__)  # 'greet' (корректно!) print(greet.__doc__)   # 'Приветствует пользователя.' (корректно!) 
  • @wraps(wrapped, assigned=WRAPPER_ASSIGNMENTS, updated=WRAPPER_UPDATES) — тоже самое, что и функция functools.update_wrapper, описанная выше. Это своего рода «синтаксически сахар» для упрощения жизни.

def my_decorator(func):     @wraps(func)  # Автоматически вызывает update_wrapper     def wrapper(*args, **kwargs):         result = func(*args, **kwargs)         return result     return wrapper 

На этом всё. Я постарался максимально подробно объяснить всё своими словами, используя документации этих библиотек. Хочется верить, что кому-то это было полезно, конечно же данная статья в первую очередь ориентирована больше на начинающих ребят-питонистов, ну, как минимум мне так кажется 🙂

Благодарю за прочтение!


ссылка на оригинал статьи https://habr.com/ru/articles/938242/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *