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

Сегодня я хочу поговорить о трёх прекрасных библиотеках 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

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

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

Комментарии (2)


  1. evgenyk
    18.08.2025 16:35

    Боюсь показаться старомодным любителем велосипедов, но ИМХО проще написать "for" или "while", чем запоминать эти функции.


    1. DarkScara Автор
      18.08.2025 16:35

      Да я и не утверждаю, что это нужно пихать всюду, конечно, иногда для единичных решений можно обойтись for и while, просто если хотя бы глазами пробежаться по этим библиотекам можно запомнить что-нибудь для себя, когда-нибудь да пригодиться :)