Generators, Iterables, Callables
The Art of Generators (May 15)
Синтаксически генераторы похожи как на функции, так и на списки и на кортежи. Создать генератор можно через определение функции, которая использует оператор yield. Следующая функция определяет генератор, который выдаёт три числа: 1, 10, 100.
def gen_1_10_100():
yield 1
yield 10
yield 100
Создаём генератор через вызов функции. Генерировать все значения можно через конвертацию генератора в list.
g = gen_1_10_100()
print(f'{type(g)=}')
print(f'{g=}')
print(f'{list(g)=}')
print(f'{list(g)=}')
Output:
type(g)=<class 'generator'>
g=<generator object gen_1_10_100 at 0x7f460e476ac0>
list(g)=[1, 10, 100]
list(g)=[]
Тип объекта g ⏤ это generator. Конвертация в список даёт [1, 10, 100]. Может быть несколько неожиданно, но повторная конвертация выдаёт пустой список [].
Почему?
Ну, генератор выдал все, что было: 1, 10, 100. А дальше более нечего генерировать! Нужно создавать новый генератор!
g = gen_1_10_100()
print(f'{list(g)=}')
print(f'{list(g)=}')
Output:
list(g)=[1, 10, 100]
list(g)=[]
Генератор можно передавать как параметр в функции, где ожидается получить последовательность значений. Проще, где работает список, может (но не обязательно!) сработать и генератор.
g = gen_1_10_100()
print(f'{sum(g)=}')
g = gen_1_10_100()
print(f'{sum(1.01**x for x in g)=}')
Output:
sum(g)=111
sum(1.01**x for x in g)=4.819435954832733
Генератор можно создать и через generator comprehension. Похоже на list comprehension, но в круглых скобках (…).
g = (x for x in [1, 10, 100])
print(f'{type(g)=}')
print(f'{g=}')
print(f'{list(g)=}')
Output:
type(g)=<class 'generator'>
g=<generator object <genexpr> at 0x7f7033d93ac0>
list(g)=[1, 10, 100]
И такой генератор можно передать (не всегда!) в функции, где ожидается список:
g = (x for x in [1, 10, 100])
print(f'{sum(g)=}')
print(f'{type(x for x in [1, 10, 100])=}')
print(f'{sum(x for x in [1, 10, 100])=}')
Output:
sum(g)=111
type(x for x in [1, 10, 100])=<class 'generator'>
sum(x for x in [1, 10, 100])=111
Ещё больше generator comprehension:
g = (x for x in [1, 10, 100])
exp_g = (1.01**x for x in g)
print(f'{type(exp_g)=}')
print(f'{sum(exp_g)=}')
Output:
type(exp_g)=<class 'generator'>
sum(exp_g)=4.819435954832733
И ещё:
exp_g = (1.01**x for x in gen_1_10_100())
print(f'{sum(exp_g)=}')
exp_g = (1.01**x for x in (10**i for i in range(3)))
print(f'{sum(exp_g)=}')
Output:
sum(exp_g)=4.819435954832733
sum(exp_g)=4.819435954832733
Кстати, объект range(3) похож на генератор, но не является им. Списки, кортежи, генератор и range ⏤ это iterable объекты. Поэтому и похожи!
print(f'{type(range(3))=}')
Output:
type(range(3))=<class 'range'>
Code in https://onlinegdb.com/uV9ygOawD
Iterables ⏤ Part 1 (May 15)
Из итерируемых объектов можно создать итератор, который позволяет пробежать по компонентам объекта в цикле for. Лучше понять это на примерах. Берём список: [1, 10, 100]. Поскольку список являются iterable, из него можно создать итератор:
it = iter([1, 10, 100])
Итератор неявно создаётся и используется в цикле for:
for elem in [1, 10, 100]:
print(elem)
На самом деле, нечасто приходится работать с итераторами напрямую.
Понимать какой объект является iterable, может быть полезно, поскольку с такими объектами хорошо работает цикл for ⏤ как оператор,
так и короткая форма (list/tuple/generator comprehension).
Один способ узнать, является ли объект iterable ⏤ это выяснить, есть ли у него метод __iter__
.
Этот метод используется итератором. Проверить наличие метода можно через hasattr:
print(f"{hasattr([1, 10, 100], '__iter__')=}")
Output:
hasattr([1, 10, 100], '__iter__')=True
Иногда такой проверки будет недостаточно: метод вроде как есть, а его вызов завершается аварийно. Поэтому лучше создать итератор явно и ловить возможную аварийную ситуацию:
some_obj = [1, 10, 100]
try:
iter(some_obj)
print(f'{some_obj}: iterable')
except TypeError:
print(f'{some_obj}: not iterable')
Output:
[1, 10, 100]: iterable
Теперь напишем вспомогательную функцию print_iterable_or_not, которая для каждого объекта из данной таблицы выведет на экран полезную информацию (тип объекта, если объект iterable, какие элементы находятся в объекте):
def print_iterable_or_not(tab):
for obj in tab:
print(f'object: {obj}')
print(f"type: {type(eval(obj)).__name__}")
try:
iter(eval(obj))
iterable = True
except TypeError:
iterable = False
print(f"iterable: {iterable}")
values = list(eval(obj)) if iterable else [eval(obj)]
print(f'value(s): {", ".join(str(x).strip() for x in values)}')
print(f'size: {len(values)}')
print()
Выглядит громоздко, посмотрим как её использовать. Создаём таблицу из разных значений: списка, кортежа, множества, range и генератора. Всё это примеры iterable объектов и их можно легко использовать в цикле for. Функция print_iterable_or_not должна определить их всех как iterable.
print_iterable_or_not((
'[0, 1, 2]',
'(0, 1, 2)',
'{0, 1, 2}',
'range(3)',
'(x for x in range(3))',
))
Output:
object: [0, 1, 2]
type: list
iterable: True
value(s): 0, 1, 2
size: 3
object: (0, 1, 2)
type: tuple
iterable: True
value(s): 0, 1, 2
size: 3
object: {0, 1, 2}
type: set
iterable: True
value(s): 0, 1, 2
size: 3
object: range(3)
type: range
iterable: True
value(s): 0, 1, 2
size: 3
object: (x for x in range(3))
type: generator
iterable: True
value(s): 0, 1, 2
size: 3
Для каждого объекта из таблицы, функция вывела информацию о типе (list, tuple, set, range, generator), подтвердила, что все они iterable (True), вывела на печать элементы этих объектов (0, 1, 2) и размер (3 элемента). Генераторы можно создавать комбинацией из функции и yield. Другой способ ⏤ это использовать generator comprehension. Проверим, если любые генераторы являются iterable.
def gen_1_10_100():
yield 1
yield 10
yield 100
print_iterable_or_not((
'gen_1_10_100()',
'(x for x in [1, 10, 100])',
'(x for x in gen_1_10_100())',
'(10**i for i in range(3))',
))
Output:
object: gen_1_10_100()
type: generator
iterable: True
value(s): 1, 10, 100
size: 3
object: (x for x in [1, 10, 100])
type: generator
iterable: True
value(s): 1, 10, 100
size: 3
object: (x for x in gen_1_10_100())
type: generator
iterable: True
value(s): 1, 10, 100
size: 3
object: (10**i for i in range(3))
type: generator
iterable: True
value(s): 1, 10, 100
size: 3
Так и вышло: все генераторы являются итерируемыми объектами (они кстати ещё являются и итераторами!).
Code: https://onlinegdb.com/hoEB0NXUG
Iterables ⏤ Part 2 (May 15)
Идём дальше: строки (str) и словари (dict) ⏤ итерируемые. Строка итерируется по буквам, а словарь по своим ключам. Более интересные примеры: классы-генераторы и файлы. Эти типы тоже итерируемые!
class Gen_1_10_100:
def __iter__(self):
yield 1
yield 10
yield 100
with open('hello.txt', 'w') as f:
print('Hello', file=f)
print('World', file=f)
print_iterable_or_not((
'"hello"',
'{0:"a", 1:"b", 2:"c"}',
'Gen_1_10_100()',
"open('hello.txt', 'r')",
))
Output:
object: "hello"
type: str
iterable: True
value(s): h, e, l, l, o
size: 5
object: {0:"a", 1:"b", 2:"c"}
type: dict
iterable: True
value(s): 0, 1, 2
size: 3
object: Gen_1_10_100()
type: Gen_1_10_100
iterable: True
value(s): 1, 10, 100
size: 3
object: open('hello.txt', 'r')
type: TextIOWrapper
iterable: True
value(s): Hello, World
size: 2
Строку можно представить как кортеж из букв, соответственно итерация по строке ⏤ работает так же как итерация по кортежу букв.
Словари содержат ассоциации между ключами и значениями. По умолчанию итерация по словарю пробегает по ключам. Чтобы пробежать по значениям, нужно воспользоваться методом: dict.values() или dict.items().
Класс-генератор работает примерно также, как и функция-генератор, но может содержать состояние. То есть, может генерировать разные последовательности значений. Файл, открытый на чтение ⏤ это iterable, а значит чтение из файла можно производить с помощью цикла for.
Многие стандартные функции/классы генерируют последовательности, то есть они являются iterable. В модуле itertools можно найти много функций, которые являются iterable. Очень полезный модуль.
Многие стандартные функции/классы генерируют последовательности, то есть они являются iterable. В модуле itertools можно найти много функций, которые являются iterable. Очень полезный модуль.
import itertools
print_iterable_or_not((
'enumerate(["a", "b", "c"])',
'reversed([0, 1, 2])',
'zip(range(3), ["a","b","c"])',
'itertools.product(range(3), repeat=2)',
'itertools.permutations(range(3))',
))
Следующие две таблицы также содержат итерируемые объекты: пустые контейнеры и sole.
print_iterable_or_not((
'[]',
'tuple()',
'set()',
'range(0)',
'(x for x in [])',
'{}',
'""',
))
print_iterable_or_not((
'[7]',
'(7,)',
'{7}',
'(x for x in [7])',
'range(7,8)',
'{7:"abc"}',
'"7"',
))
Code: https://onlinegdb.com/o6581Ooqw
Iterables ⏤ Part 3 (May 15)
Настало время показать примеры объектов, которые не являются итерируемыми. В первую очередь это числа (целые, вещественные), булевские значения (True, False), и конечно None.
Никаких сюрпризов: по объектам типов int, float, bool, NoneType итерироваться в циле for не получится.
print_iterable_or_not((
'5',
'5.5',
'True',
'None',
))
Output:
object: 5
type: int
iterable: False
value(s): 5
size: 1
object: 5.5
type: float
iterable: False
value(s): 5.5
size: 1
object: True
type: bool
iterable: False
value(s): True
size: 1
object: None
type: NoneType
iterable: False
value(s): None
size: 1
Функции, стандартные или определённые программистом, а также типы, классы и модули тоже не являются iterable.
print_iterable_or_not((
'len',
'gen_1_10_100',
'str',
'Gen_1_10_100',
'itertools',
))
Output:
object: len
type: builtin_function_or_method
iterable: False
value(s): <built-in function len>
size: 1
object: gen_1_10_100
type: function
iterable: False
value(s): <function gen_1_10_100 at 0x10ae35a60>
size: 1
object: str
type: type
iterable: False
value(s): <class 'str'>
size: 1
object: Gen_1_10_100
type: type
iterable: False
value(s): <class '__main__.Gen_1_10_100'>
size: 1
object: itertools
type: module
iterable: False
value(s): <module 'itertools' (built-in)>
size: 1
Code: https://onlinegdb.com/wq9VInL3Z
Callables (Aug 18)
В Python объект может быть Callable, то есть, его можно вызвать как обычную функцию.
Стандартная функция callable проверяет, является ли данный объект “вызываемым”.
Очевидно, что любая функция, метод, в том числе внутренние и lambda — являются Callable. Напишем простую программу и протестируем разные объекты на это свойство.
import operator
import typing
import functools
def add(a: int, b: int) -> int: return a + b
def mk_power_add(k: int) -> typing.Callable[[int, int], int]:
def power_k_add(a: int, b: int): return a**k + b**k
return power_k_add
Функция add а также результат вызова mk_power_add(1) — эти примеры функций, которые просто суммируют два аргумента
То есть это примеры Callable, a значит callable(add) и callable(mk_power_add(1) вернёт True.
Также callable(lambda a, b: a+b)
тоже вернёт True.
Далее приведём примеры статической функции (функция класса) и метода:
class A:
@staticmethod
def add(a: int, b: int) -> int: return a + b
class Power:
def __init__(self, k: int): self.k = k
def add(self, a: int, b: int) -> int: return a**self.k + b**self.k
Обе функции A.add
и Power(1).add
суммируют два аргумента и являются Callable.
В следующем примере объекты двух классов являются Callable,
поскольку у них реализован специальный скрытый метод __call__
:
class Add:
def __init__(self):
self.__name__ = f'{type(self).__name__}'
def __call__(self, a: int, b: int) -> int: return a + b
class MkPowerAdd:
def __init__(self, k: int):
self.k = k
self.__name__ = f'{type(self).__name__}({self.k})'
def __call__(self, a: int, b: int) -> int: return a**self.k + b**self.k
Вызовы callable(Add())
и callable(MkPowerAdd(1))
вернут True.
В следующем примере берём функцию с тремя аргументами, в которой фиксируем один аргумент, получим частичную функцию с двумя аргументами. Тоже Callable:
def power_add(a: int, b: int, k: int): return a**k + b**k
Вызов callable(functools.partial(power_add, k=1))
вернёт True.
Библиотечные функции int.__add__
, operator.add — тоже Callable.
Движемся вперёд: для удобства зафиксируем степень k=1 и определим таблицу объектов для проверки на Callable:
k = 1
power1_add = functools.partial(power_add, k=k)
power1_add.__name__ = 'power1_add'
tab = (
int.__add__,
operator.add,
lambda a, b: a + b,
add,
mk_power_add(k),
A.add,
Power(1).add,
MkPowerAdd(k),
power1_add,
)
Каждый объект по сути является аналогом операции +. Выполним вызов на аргументах a=12 и b=5 и проверим результат:
a, b = 12, 5
for func in tab:
print(f'{callable(func)=}, {func.__name__}, {func(12, 5)=}')
Получим табличку:
callable(func)=True, func=<lambda>, func(12, 5)=17
callable(func)=True, func=add, func(12, 5)=17
callable(func)=True, func=power_k_add, func(12, 5)=17
callable(func)=True, func=add, func(12, 5)=17
callable(func)=True, func=add, func(12, 5)=17
callable(func)=True, func=Add, func(12, 5)=17
callable(func)=True, func=MkPowerAdd(1), func(12, 5)=17
callable(func)=True, func=power1_add, func(12, 5)=17
The code is https://onlinegdb.com/TH9ry84dL