Как уменьшить использование памяти и ускорить работу кода на Python с помощью генераторов
- пятница, 29 ноября 2019 г. в 00:28:32
Всем привет. Сегодня хотим поделиться одним полезным переводом, подготовленным в преддверии запуска курса «Web-разработчик на Python». Писать код эффективный по времени и по памяти на Python особенно важно, когда занимаешься созданием Web-приложения, модели машинного обучения или занимаешься тестированием.
Когда я начал изучать генераторы в Python, я понятия не имел насколько они важны. Однако они постоянно помогали мне при написании функций на протяжении всего моего путешествия по машинному обучению.
Функции-генераторы позволяют объявить функцию, которая будет вести себя как итератор. Они позволяют программистам создавать быстрые, простые и чистые итераторы. Итератор – это объект, который может быть повторен (зациклен). Он используется для того, чтобы абстрагировать контейнер данных и заставить его вести себя как итерируемый объект. Например, примером итерируемого объекта могут быть строки, списки и словари.
Генератор выглядит как функция, но использует вместо return ключевое слово yield. Давайте рассмотрим пример, чтобы стало понятнее.
def generate_numbers():
n = 0
while n < 3:
yield n
n += 1
Это функция-генератор. Когда вы ее вызываете, она возвращает объект-генератор.
>>> numbers = generate_numbers()
>>> type(numbers)
<class 'generator'>
Важно обратить внимание на то, как состояние инкапсулируется в теле функции генератора. Вы можете итерироваться по одному, используя встроенную функцию next():
>>> next_number = generate_numbers()
>>> next(next_number)
0
>>> next(next_number)
1
>>> next(next_number)
2
StopIteration – это встроенный тип исключения, которое возникает автоматически, как только генератор перестает возвращать результат. Это сигнал остановки для цикла for.
Его основная задача – управлять потоком функции генератора так, чтобы это было похоже на оператор return. При вызове функции генератора или использовании выражения генератора он возвращает специальный итератор, который называется генератором. Чтобы использовать генератор, присвойте его какой-либо переменной. При вызове специальных методов в генераторе, таких как next(), код функции будет выполняться до yield.
При попадании в инструкцию yield, программа приостанавливает выполнение функции и возвращает полученное значение объекту, который инициировал выполнение. (Тогда как return прекращает выполнение функции полностью.) Когда работа функции приостанавливается, ее состояние сохраняется.
Теперь, когда мы познакомились с генераторами в Python, давайте сравним обычный подход с подходом, в котором используются генераторы, в вопросах памяти и времени, которые тратятся на выполнение кода.
Предположим, нам нужно пройтись по большому списку чисел (например, 100000000) и сохранить квадраты всех чисел, которые нужно хранить отдельно в другом списке.
import memory_profiler
import time
def check_even(numbers):
even = []
for num in numbers:
if num % 2 == 0:
even.append(num*num)
return even
if __name__ == '__main__':
m1 = memory_profiler.memory_usage()
t1 = time.clock()
cubes = check_even(range(100000000))
t2 = time.clock()
m2 = memory_profiler.memory_usage()
time_diff = t2 - t1
mem_diff = m2[0] - m1[0]
print(f"It took {time_diff} Secs and {mem_diff} Mb to execute this method")
После запуска кода выше, мы получим следующее:
It took 21.876470000000005 Secs and 1929.703125 Mb to execute this method
С использованием генераторов
import memory_profiler
import time
def check_even(numbers):
for num in numbers:
if num % 2 == 0:
yield num * num
if __name__ == '__main__':
m1 = memory_profiler.memory_usage()
t1 = time.clock()
cubes = check_even(range(100000000))
t2 = time.clock()
m2 = memory_profiler.memory_usage()
time_diff = t2 - t1
mem_diff = m2[0] - m1[0]
print(f"It took {time_diff} Secs and {mem_diff} Mb to execute this method")
После запуска кода выше, мы получим следующее:
It took 2.9999999995311555e-05 Secs and 0.02656277 Mb to execute this method
Как мы видим, время выполнения и затраченная память значительно сократились. Генераторы работают по принципу, известному как «ленивые вычисления». Это значит, что они могут экономить ресурсы процессора, памяти и других вычислительных ресурсов.
Надеюсь, в этой статье я смог показать, как генераторы в Python можно использовать для экономии таких ресурсов как память и время. Это преимущество появляется из-за того, что генераторы не сохраняют все результаты в памяти, а вычисляют их на лету, а память используется только в случае, если мы запрашиваем результат вычислений. Также генераторы позволяют абстрагировать большое количество шаблонного кода, который нужен для написания итераторов, поэтому они также помогают сократить количество кода.