Статья из моего телеграм канала о программировании.
Генераторы коллекций - короткий(относительно цикла for) способ создавать коллекции на основе других коллекций.
Эти генераторы позволяют нам:
Кратко и просто создавать коллекции(при несложной логике).
Экономить время(генераторы более эффективны, чем цикл for).
Подходит для адептов функционального программирования, так как происходит именно генерация новой коллекции, а не изменение существующей.
Сразу хочу упомянуть одну важную вещь - не стоит "прятать" важные бизнес правила в сложные генераторы коллекций:
Не все могут быстро понимать, что же происходит в генераторе. Не все умеют хорошо писать генераторы коллекций. Раньше, выполняя задачки на сайтах по типу CodeWars, я тоже думал "Как круто, что можно много всего запихнуть в одну строку", и даже стремился к этому. Сейчас я так не считаю.
При усложнении бизнес логики велик шанс, что человек вносящий изменения будет стараться сохранить выражение генератор, а не отдать предпочтение переписыванию всего на этапы через for для того, что бы сделать код более явным. В следствие чего выражение генератор может стать ещё сложнее, либо логика может обосноваться рядом в лучшем случае с этим выражением(начнёт расплываться по коду, то что должно выполняться вместе)
# Функциональное выражение генератор взятое из боевого кода,
# которое и по сей день кажется мне идеальным примером,
# когда бизнес логика замешана с попыткой сделать код проще(меньше).
# Попытка, как по мне, неудачна :)
discounts = (
load_result.map(self._collect_discounts)
.rescue(lambda ex: Success([None] * len(self._products)))
.unwrap()
)
Давайте же перейдём к примерам:
Кратко и просто создавать коллекции.
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
"Создание списка через стандартный цикл for"
numbers_squares = []
for number in numbers:
numbers_squares.append(number ** 2)
"Создание списка с помощью генератора списков"
numbers_squares_ = [number ** 2 for number in numbers]
print(numbers_squares) # -> [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
print(numbers_squares_) # -> [1, 4, 9, 16, 25, 36, 49, 64, 81, 100]
И правда, компактно. А что, если логика будет посложнее?
students = {
1: {
"age": 27,
"first_name": "Mark",
"last_name": "Loginov",
"subject_average_score": {
"history": 4.5,
"mathematics": 3.4,
},
},
2: {
"age": 32,
"first_name": "Igor",
"last_name": "Petrov",
"subject_average_score": {
"history": 4.2,
"literature": 5,
},
},
}
students_ = {
id: {
"full_name": f"{student_data['first_name']} {student_data['last_name']}",
"subject": [
subject
for subject in student_data["subject_average_score"].keys()
],
}
for id, student_data in students.items()
}
print(students_)
{
1: {'full_name': 'Mark Loginov', 'subject': ['history', 'mathematics']},
2: {'full_name': 'Igor Petrov', 'subject': ['history', 'literature']},
}
# Имея информацию о студентах, мы можем сделать выжимку и сжать данные до
# размера: что это за студент, и какие уроки он посещает.
numbers = [
[[1, 2, 3], [4, 5, 6], [7, 8, 9]],
[[10, 11, 12], [13, 14, 15], [16, 17, 18], [19, 20, 21]],
[[22, 23, 24], [25, 26, 27]],
]
numbers_split = [
number for numbers_level_two in numbers
for numbers_level_three in numbers_level_two
for number in numbers_level_three
]
print(numbers_split) # -> [1, 2, 3, 4, 5, 6, 7, 8, ..., 27]
# Если нам известна вложенность нашей структуры, то мы можем сделать
# из неё линейную последовательность.
peoples = [
{"name": "", "age": 29},
{"name": "Igor", "age": 27},
{"name": "Petr", "age": 31},
{"name": "Liza", "age": 20},
]
filtered_peoples_names = [
people["name"] if people.get("name") else "Unknown Person"
for people in peoples
if people["age"] < 30
]
print(filtered_peoples_names) # -> ['Unknown Person', 'Igor', 'Liza']
# Также обратите внимание, что if в конце служит для фильтрации данных,
# а if, else в начале для возможности выбора конечного действия над
# выбранным объектом. В этом примере мы убрали из конечной выборки всех
# кому менее 30 лет. Тех, у кого не было внесено имя, установили
# его в "Unknown Person".
Допустим, мы разобрались с базовым синтаксисом генератора списков, но что же там насчёт скорости?
def for_() -> list[int]:
numbers_squares = []
for i in range(100):
numbers_squares.append(i)
return numbers_squares
def list_comprehension() -> list[int]:
return [i for i in range(100)]
# python 3.10
print(min(timeit.repeat(list_comprehension, number=100000))) # -> ~0.1477
print(min(timeit.repeat(for_, number=100000))) # -> ~0.2755
# python 3.12
print(min(timeit.repeat(list_comprehension, number=100000))) # ~0.0841
print(min(timeit.repeat(for_, number=100000))) # ~0.1155
Обратите внимание какая разница в скорости у генерации списка относительно цикла:
python 3.10 ~87%
python 3.12 ~37%
Если у вас возник вопрос, почему так сильно сократилась разница в скорости между версиями python? В python 3.12 сильно увеличилась производительность относительно python 3.10 в подобных случаях:
list_comprehension ~75%
for ~139%
Увеличим объём генерируемых данных в 10 раз и снова проведём замеры:
python 3.10 ~40%
python 3.12 ~18%
list_comprehension ~82%
for ~116%
За счёт чего же появляется прирост в скорости?
Используя цикл нам, приходится на каждой итерации делать __getattribute__ и call метода append.
Создаваемый в цикле список заранее, не знает какое кол-во объектов в нём будет. Поэтому при большом наборе данных, он будет многократно "выниматься" из оперативной памяти, аллоцировать новый увеличенный объём и "вставляться" в новое место. Это достаточно затратная операция, занимающая O(n) времени.
Если вас интересует более глубокий разбор, что же происходит "под капотом", то предлагаю запустить в своём интерпретаторе код подобный этому:
import dis
def for_() -> list[int]:
numbers_squares = []
for _ in range(100):
numbers_squares.append(_)
return numbers_squares
def list_comprehension() -> list[int]:
return [_ for _ in range(100)]
print(dis.dis(for_))
print(dis.dis(list_comprehension))
Вы получите разобранный машинный код на языке assembler(код отражает действия процессора):
В завершение хочется упомянуть, что кроме генераторов списков, есть так же:
Генератор множества
Генератор словарей
Генератор генераторов :)
Работают они все по тому же принципу и тем же правилам.
print({number for number in [1, 2, 1]}) # -> {1, 2}
print({name: value for name, value in zip(["one", "two"], [1, 2])}) # -> {'one': 1, 'two': 2}
print((x for x in range(10))) # -> <generator object <genexpr> at xxx>
Комментарии (11)
longclaps
12.04.2024 17:55+7Кратко и просто создавать коллекции.
numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
Это называется словом "литерал", в данном случае - списочный литерал.
Знание терминологи не обязательно для простых смертных, но весьма желательно для python разработчика с многолетним опытом и держателя профильного телеграм-канала. Объясню почему:
Литерал, как способ создания питон-объекта, существует не для только лишь всех. Хрестоматийный пример: множество из нескольких элементов можно задать литералом, напр
{1, 2, 3}
, но пустое множество создаётся только вызовом конструктораset()
. Причина проста и естественна - для пустого множества не удалось придумать органично выглядящий и однозначно распознаваемый литерал.В древности литералы существовали лишь для самых базовых типов, например чисел - ну как ты число сконструируешь? Т.е. можно конечно конструктором, вызов
int()
порождает0
, но один только ноль - этого мало. В питон литералы сразу завезли для списков и словарей, а множества (которые кстати тоже появились не сразу) изначально создавались только через конструктор,set([1, 2, 3])
, и лишь потом для них придумали литералы, как синтаксический сахар.В завершение хочется упомянуть, что кроме генераторов списков, ...
Есть и другие хорошие новости. Например, хотя для коллекции типа
deque
(очередь) нет литерала, и создавать её экземпляр приходится древним способом,deque([1, 2, 3])
, для создания очереди можно воспользоваться генераторным выражением в качестве аргумента конструктора,deque(i * 2 for i in range(3))
, что выглядит почти так же мило, как списочный генератор.KarmanovichDev Автор
12.04.2024 17:55Спасибо за обратную связь! В статье я не пытался раскрыть, что такое "литерал". Я хотел рассказать, как работают генераторы коллекций. Коллекция в программирование это объект, который содержит в себе другие объекты, и предоставляет открытый интерфейс для доступа к этим данных.
RedEyedAnonymous
12.04.2024 17:55Причина проста и естественна - для пустого множества не удалось
придумать органично выглядящий и однозначно распознаваемый литерал....потому что пустые фигурные скобки заняты словарём. И это несколько удивляет: для списка - квадратные скобки, для кортежа - круглые, а фигурные достались и словарю, и множеству.
KarmanovichDev Автор
12.04.2024 17:55Квадратные скобки используются также при: list[0], str[::], dict["key"].
Круглые скобки используются ещё для генерации генераторов (x for x on y), выделения условий и мат. операций (2 + 2) * 2, средством переноса кода без применения "/", вызов любого callable объекта происходит через ().
Фигурные скобки используются в f-string f"{argument}".
Итог - на всех не хватит разных видов скобок :)
ol_mur
12.04.2024 17:55Вообще-то множество - это словарь, где есть только ключи без значений. Поэтому у него и у словаря - фигурные скобки. Обе коллекции в Python - хэшируемые объекты.
KarmanovichDev Автор
12.04.2024 17:55Множество и словарь это разные типы данных, с разными интерфейсами взаимодействия. Множество не имеет порядка и доступа по ключу.
Также операции и задачи в которых используются эти типы данных, отличаются.
KarmanovichDev Автор
12.04.2024 17:55Эти коллекции не хешируемые, так как являются изменяемыми типами данных.
SwetlanaF
12.04.2024 17:55Спасибо за ценную информацию. Автору тоже спасибо и лайк)). Если нуль-функция порождается как int(), то нужен литерал только для единицы, чтобы записать другую базовую примитивно-рекурсивную функцию следования s(x)= x+1. Тогда любое натуральное число записывается как s...s(int(0))...).
MountainGoat
Я поставил плюс, но догадываюсь, почему минусуют: чтобы понять, что здесь написано, нужно знать, что здесь написано.
KarmanovichDev Автор
Здравствуйте. У меня сложные примеры?