Небольшое вступление
Сегодня я хочу поговорить о трёх прекрасных библиотеках 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, до этого вывод был не упорядоченным. Методы:
popitem(last=True) — Удаляет последний элемент словаря, но если указать аргумент last = False, то наоборот удалит первый элемент словаря
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 простое:
Примените декоратор @total_ordering к вашему классу.
Обязательно определите в классе метод
__eq__(self, other)
для проверки на равенство.-
Определите хотя бы один из следующих методов:
__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
) следующие атрибуты из исходной функции:
__module__
__name__
__qualname__
(в Python 3.3+)__doc__
__annotations__
(в Python 3+)__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
На этом всё. Я постарался максимально подробно объяснить всё своими словами, используя документации этих библиотек. Хочется верить, что кому-то это было полезно, конечно же данная статья в первую очередь ориентирована больше на начинающих ребят-питонистов, ну, как минимум мне так кажется :)
Благодарю за прочтение!
evgenyk
Боюсь показаться старомодным любителем велосипедов, но ИМХО проще написать "for" или "while", чем запоминать эти функции.
DarkScara Автор
Да я и не утверждаю, что это нужно пихать всюду, конечно, иногда для единичных решений можно обойтись for и while, просто если хотя бы глазами пробежаться по этим библиотекам можно запомнить что-нибудь для себя, когда-нибудь да пригодиться :)