Хочешь уверенно проходить IT-интервью?

Мы понимаем, как сложно подготовиться: стресс, алгоритмы, вопросы, от которых голова идёт кругом. Но с AI тренажёром всё гораздо проще.
💡 Почему Т1 тренажёр — это мастхэв?
- Получишь настоящую обратную связь: где затык, что подтянуть и как стать лучше
- Научишься не только решать задачи, но и объяснять своё решение так, чтобы интервьюер сказал: "Вау!".
- Освоишь все этапы собеседования, от вопросов по алгоритмам до диалога о твоих целях.
Зачем листать миллион туториалов? Просто зайди в Т1 тренажёр, потренируйся и уверенно удиви интервьюеров. Мы не обещаем лёгкой прогулки, но обещаем, что будешь готов!
Реклама. ООО «Смарт Гико», ИНН 7743264341. Erid 2VtzqwP8vqy
«Питон – медленный». Наверняка вы не раз сталкивались с этим утверждением, особенно от людей, пришедших в Python из C, C++ или Java. Во многих случаях это верно. Циклы или сортировка массивов, списков или словарей иногда действительно работают медленно. В конце концов, главная миссия Python – сделать программирование приятным и легким. Ради лаконичности и удобочитаемости пришлось отчасти принести в жертву производительность.
Тем не менее, в последние годы предпринято немало усилий для решения проблемы. Теперь мы можем эффективно обрабатывать большие наборы данных с помощью NumPy, SciPy, Pandas и numba, поскольку все эти библиотеки написаны на C/C++. Еще один интересный проект – PyPy ускоряет код Python в 4.4 раза по сравнению с CPython (оригинальная реализация Python).
Недостаток PyPy – нет поддержки некоторых популярных модулей, например, Matplotlib, SciPy.
Но ускорить Python можно и без внешних библиотек. В наших силах разогнать его с помощью полезных трюков, используемых в повседневной практике кодинга.
1. Стандартные функции

В Python много работающих очень быстро реализованных на C встроенных функций. Они покрывают большинство тривиальных вычислительных операций (abs
, min
, max
, len
, sum
). Хороший разработчик должен их знать, чтобы в подходящем месте не изобретать неуклюжие велосипеды, а брать надёжное стандартное решение. Возьмём в качестве примеров встроенные функции set()
и sum()
. Сравним их работу с кастомными реализациями того же функционала.
Пример для set()
:
import random
random.seed(666)
a_long_list = [random.randint(0, 50) for i in range(1000000)]
# 1. Кастомная реализация set
%%time
unique = []
for n in a_long_list:
if n not in unique:
unique.append(n)
# Вывод в консоли:
# CPU times: user 316 ms, sys: 1.36 ms, total: 317 ms
# Wall time: 317 ms
# 2. Встроенная функция set
%%time
unique = list(set(a_long_list))
# Вывод в консоли:
# CPU times: user 8.74 ms, sys: 110 μs, total: 8.85 ms
# Wall time: 8.79 ms
Пример для sum()
:
# 1. Кастомная реализация sum
%%time
sum_value = 0
for n in a_long_list:
sum_value += n
print(sum_value)
# Вывод в консоли:
# 25023368
# CPU times: user 9.91 ms, sys: 2.2 ms, total: 101 ms
# Wall time: 100 ms
# 2. Встроенная функция sum
%%time
sum_value = sum(a_long_list)
print(sum_value)
# Вывод в консоли:
# 25023368
# CPU times: user 4.74 ms, sys: 277 μs, total: 5.02 ms
# Wall time: 4.79 ms
Стандартные варианты в 36 (set
) и 20 (sum
) раз быстрее, чем функции, написанные самим разработчиком.
2. sort() vs sorted()
Если нам просто нужен отсортированный список, при этом неважно, что будет с оригиналом, sort()
будет работать немного быстрее, чем sorted()
. Это справедливо для базовой сортировки:
# 1. Дефолтная сортировка с использованием sorted()
%%time
sorted(a_long_list)
# Вывод в консоли:
# CPU times: user 12 ms, sys: 2.51 ms, total: 14.5 ms
# Wall time: 14.2 ms
# 2. Дефолтная сортировка с использованием sort()
%%time
a_long_list.sort()
# Вывод в консоли:
# CPU times: user 8.52 ms, sys: 82 μs, total: 8.6 ms
# Wall time: 10 ms
Справедливо и для сортировки с использованием ключа – параметра key
, который определяет сортировочную функцию:
# 1. Сортировка с ключом с использованием sorted()
%%time
str_list1 = "Although both functions can sort list, there are small differences".split()
result = sorted(str_list1, key=str.lower)
print(result)
# Вывод в консоли:
# ['Although', 'are', 'both', 'can', 'differences', 'functions', 'list,', 'small',
'sort', 'there']
# CPU times: user 29 μs, sys: 0 ns, total: 29 μs
# Wall time: 32.9 μs
# 2. Сортировка с ключом с использованием sort()
%%time
str_list2 = "Although both functions can sort list, there are small differences".split()
str_list2.sort(key=str.lower)
print(str_list2)
# Вывод в консоли:
# ['Although', 'are', 'both', 'can', 'differences', 'functions', 'list,', 'small',
'sort', 'there']
# CPU times: user 26 μs, sys: 0 ns, total: 26 μs
# Wall time: 29.8 μs
# 3. Сортировка с ключом (лямбда) с использованием sorted()
%%time
str_list1 = "Although both functions can sort list, there are small differences".split()
result = sorted(str_list1, key=lambda str: len(str))
print(result)
# Вывод в консоли:
# ['can', 'are', 'both', 'sort', 'list,', 'there', 'small', 'Although', 'functions', 'differences']
# CPU times: user 61 μs, sys: 3 μs, total: 64 μs
# Wall time: 59.8 μs
# 4. Сортировка с ключом (лямбда) с использованием sort()
%%time
str_list2 = "Although both functions can sort list, there are small differences".split()
str_list2.sort(key=lambda str: len(str))
print(str_list2)
# Вывод в консоли:
# ['can', 'are', 'both', 'sort', 'list,', 'there', 'small', 'Although', 'functions', 'differences']
# CPU times: user 36 μs, sys: 0 ns, total: 36 μs
# Wall time: 38.9 μs
Так происходит потому, что метод sort()
изменяет список прямо на месте, в то время как sorted()
создает новый отсортированный список, сохраняя исходный нетронутым. Другими словами, порядок значений внутри a_long_list
фактически уже изменился.
Однако функция sorted()
более универсальна. Она может работать с любой итерируемой структурой. Поэтому, если нужно отсортировать, например, словарь (по ключам или по значениям), придется использовать sorted()
:
a_dict = {'A': 1, 'B': 3, 'C': 2, 'D': 4, 'E': 5}
# 1. Дефолтная сортировка по ключам
%%time
result = sorted(a_dict)
print(result)
# Вывод в консоли:
# ['A', 'B', 'C', 'D', 'E']
# CPU times: user 4 μs, sys: 0 ns, total: 4 μs
# Wall time: 6.91 μs
# 2. Cортировка по значениям, результат в виде списка кортежей
%%time
result = sorted(a_dict.items(), key=lambda item: item[1])
print(result)
# Вывод в консоли:
# [('A', 1), ('C', 2), ('B', 3), ('D', 4), ('E', 5)]
# CPU times: user 7 μs, sys: 0 ns, total: 7 μs
# Wall time: 8.82 μs
# 3. Сортировка по значениям, результат в виде словаря
%%time
result = {key: value for key, value in sorted(a_dict.items(), key=lambda item: item[1])}
print(result)
# Вывод в консоли:
# {'A': 1, 'C': 2, 'B': 3, 'D': 4, 'E': 5}
# CPU times: user 8 μs, sys: 0 ns, total: 8 μs
# Wall time: 11.2 μs
3. Литералы вместо функций
Когда нужен пустой словарь или список, вместо dict()
или list()
, можно напрямую вызвать {}
и []
(для пустого множества все еще нужна функция set()
). Этот прием не обязательно ускорит ваш код, но сделает его более "pythonic".
# 1. Создание пустого словаря с помощью dict()
%%time
sorted_dict1 = dict()
for key, value in sorted(a_dict.items(), key=lambda item:item[1]):
sorted_dict1[key] = value
# Вывод в консоли:
# CPU times: user 10 μs, sys: 0 ns, total: 10 μs
# Wall time: 12.2 μs
# 2. Создание пустого словаря с помощью литерала словаря
%%time
sorted_dict2 = {}
for key, value in sorted(a_dict.items(), key=lambda item:item[1]):
sorted_dict2[key] = value
# Вывод в консоли:
# CPU times: user 9 μs, sys: 0 ns, total: 9 μs
# Wall time: 11 μs
# 3. Создание пустого списка с помощью list()
%%time
list()
# Вывод в консоли:
# CPU times: user 3 μs, sys: 0 ns, total: 3 μs
# Wall time: 3.81 μs
# 4. Создание пустого списка с помощью литерала списка
%%time
[]
# Вывод в консоли:
# CPU times: user 2 μs, sys: 0 ns, total: 2 μs
# Wall time: 3.1 μs
4. Генераторы списков
Обычно, когда требуется создать новый список из старого на основе определенных условий, мы используем цикл for
– итерируем все значения и сохраняем нужные в новом списке.
Например, отберём все чётные числа из списка another_long_list
:
even_num = []
for number in another_long_list:
if number % 2 == 0:
even_num.append(number)
Но есть более лаконичный и элегантный способ сделать то же самое. Код цикла for
можно сократить до одной-единственной строки с помощью генератора списка, выиграв при этом в скорости почти в два раза:
import random
random.seed(666)
another_long_list = [random.randint(0,500) for i in range(1000000)]
# 1. Создание нового списка с помощью цикла for
%%time
even_num = []
for number in another_long_list:
if number % 2 == 0:
even_num.append(number)
# Вывод в консоли:
# CPU times: user 113 ms, sys: 3.55 ms, total: 117 ms
# Wall time: 117 ms
# 2. Создание нового списка с помощью генератора списка
%%time
even_num = [number for number in another_long_list if number % 2 == 0]
# Вывод в консоли:
# CPU times: user 56.6 ms, sys: 3.73 ms, total: 60.3 ms
# Wall time: 64.8 ms
Сочетая это правило с Правилом #3 (использование литералов), мы легко можем превратить список в словарь или множество, просто изменив скобки:
a_dict = {'A': 1, 'B': 3, 'C': 2, 'D': 4, 'E': 5}
sorted_dict3 = {key: value for key, value
in sorted(a_dict.items(), key=lambda item: item[1])}
print(sorted_dict3)
# Вывод в консоли:
# {'A': 1, 'C': 2, 'B': 3, 'D': 4, 'E': 5}
Разберёмся в коде:
- Выражение
sorted(a_dict.items(), key=lambda item: item[1])
возвращает список кортежей[('A', 1), ('C', 2), ('B', 3), ('D', 4), ('E', 5)]
. - Далее мы распаковываем кортежи и присваиваем первый элемент каждого кортежа в переменную
key
, а второй – в переменнуюvalue
. - Наконец, сохраняем каждую пару
key
-value
в словаре.
5. enumerate() для значения и индекса
Иногда при переборе списка нужны и значения, и их индексы. Чтобы вдвое ускорить код используйте enumerate()
для превращения списка в пары индекс-значение:
import random
random.seed(666)
a_short_list = [random.randint(0,500) for i in range(5)]
# 1. Получение индексов с помощью использования длины списка
%%time
for i in range(len(a_short_list)):
print(f'number {i} is {a_short_list[i]}')
# Вывод в консоли:
# number 0 is 233
# number 1 is 462
# number 2 is 193
# number 3 is 222
# number 4 is 145
# CPU times: user 189 μs, sys: 123 μs, total: 312 μs
# Wall time: 214 μs
# 2. Получение индексов с помощью enumerate()
for i, number in enumerate(a_short_list):
print(f'number {i} is {number}')
# Вывод в консоли:
# number 0 is 233
# number 1 is 462
# number 2 is 193
# number 3 is 222
# number 4 is 145
# CPU times: user 72 μs, sys: 15 μs, total: 87 μs
# Wall time: 90.1 μs
6. zip() для перебора нескольких списков
В некоторых случаях приходится перебирать более одного списка. Для ускорения операции рекомендуется использовать функцию zip()
, которая преобразует их в общий итератор кортежей:
list1 = ['a', 'b', 'c', 'd', 'e']
list2 = ['1', '2', '3', '4', '5']
pairs_list = [pair for pair in zip(list1, list2)]
print(pairs_list)
# Вывод в консоли:
[('a', '1'), ('b', '2'), ('c', '3'), ('d', '4'), ('e', '5')]
Обратите внимание, списки должны быть одинаковой длины, так как функция zip()
останавливается, когда заканчивается более короткий список.
И наоборот, чтобы получить доступ к элементам каждого кортежа, мы можем распаковать список кортежей, добавив звездочку (*
) и используя множественное присваивание:
# 1. Распаковка списка кортежей с помощью zip()
%%time
letters1, numbers1 = zip(*pairs_list)
print(letters1, numbers1)
# Вывод в консоли:
('a', 'b', 'c', 'd', 'e') ('1', '2', '3', '4', '5')
# CPU times: user 5 μs, sys: 1e+03 ns, total: 6 μs
# Wall time: 6.91 μs
# 2. Распаковка списка кортежей простым перебором
letters2 = [pair[0] for pair in pairs_list]
numbers2 = [pair[1] for pair in pairs_list]
print(letters2, numbers2)
# Вывод в консоли:
['a', 'b', 'c', 'd', 'e'] ['1', '2', '3', '4', '5']
# CPU times: user 5 μs, sys: 1e+03 ns, total: 6 μs
# Wall time: 7.87 μs
7. Комбинация set() и in
Если нужно проверить, содержит ли список некоторое значение, можно написать такую неуклюжую функцию:
import random
random.seed(666)
another_long_list = [random.randint(0,500) for i in range(1000000)]
def check_membership(n):
for element in another_long_list:
if element == n:
return True
return False
Однако есть более характерный для Python способ сделать это – использовать оператор in
:
# 1. Проверка наличия значения в списке перебором элементов
%%time
check_membership(900)
# Вывод в консоль
# CPU times: user 29.7 ms, sys: 847 μs, total: 30.5 ms
# Wall time: 30.2 ms
# 2. Проверка наличия значения в списке с помощью in
900 in another_long_list
# Вывод в консоль
# CPU times: user 10.2 ms, sys: 79 μs, total: 10.3 ms
# Wall time: 10.3 ms
Повысить эффективность можно предварительным удалением из списка дубликатов с помощью set
. Таким образом, мы сократим количество элементов для проверки. Кроме того, оператор in
очень быстро работает с множествами.
# Убираем дубликаты
check_list = set(another_long_list)
# Вывод в консоль
# CPU times: user 19.8 ms, sys: 204 μs, total: 20 ms
# Wall time: 20 ms
# Проверяем наличие значения в списке
900 in check_list
# Вывод в консоль
# CPU times: user 2 μs, sys: 0 ns, total: 2 μs
# Wall time: 5.25 μs
Преобразование списка в множество заняло 20 мс. Но это одноразовые затраты. Зато сама проверка заняла 5 мкс – то есть в 2 тыс. раз меньше, что становится важным при частых обращениях.
8. Проверка на True
Практически в любой программе необходимо проверять, являются ли переменные/списки/словари/... пустыми. На этих проверках тоже можно немножко сэкономить.
Не следует явно указывать == True
или is True
в условии if
, достаточно указать имя проверяемой переменной. Это экономит ресурсы, которые использует «магическая» функция __eq__
для сравнения значений.
string_returned_from_function = 'Hello World'
# 1. Явная проверка на равенство
%%time
if string_returned_from_function == True:
pass
# Вывод в консоль
# CPU times: user 3 μs, sys: 0 ns, total: 3 μs
# Wall time: 5.01 μs
# 2. Явная проверка с использованием оператора is
%%time
if string_returned_from_function is True:
pass
# Вывод в консоль
# CPU times: user 2 μs, sys: 1 ns, total: 3 μs
# Wall time: 4.05 μs
# 3. Неявное равенство
%%time
if string_returned_from_function:
pass
# Вывод в консоль
# CPU times: user 3 μs, sys: 0 ns, total: 3 μs
# Wall time: 4.05 μs
Аналогично можно проверять обратное условие, добавив оператор not
:
if not string_returned_from_function:
pass
9. Подсчет уникальных значений с Counter()
Если нам необходимо подсчитать количество уникальных значений в списке, можно, например, создать словарь, в котором ключи – это значения списка, а значения – счетчик встречаемости.
%%time
num_counts = {}
for num in a_long_list:
if num in num_counts:
num_counts[num] += 1
else:
num_counts[num] = 1
# Вывод в консоль
# CPU times: user 448 ms, sys: 1.77 ms, total: 450 ms
# Wall time: 450 ms
Однако более эффективный способ для решения этой задачи – использование Counter()
из модуля collections. Весь код при этом уместится в одной строчке:
%%time
num_counts2 = Counter(a_long_list)
# Вывод в консоль
# CPU times: user 40.7 ms, sys: 329 μs, total: 41 ms
# Wall time: 41.2 ms
Этот фрагмент будет работать примерно в 10 раз быстрее, чем предыдущий.
У Counter
также есть удобный метод most_common
, позволяющий получить самые часто встречающиеся значения:
for number, count in num_counts2.most_common(10):
print(number, count)
# Вывод в консоль
29 19831
47 19811
7 19800
36 19794
14 19761
39 19748
32 19747
16 19737
34 19729
33 19729
Одним словом, collections
– это замечательный модуль, который должен быть в базовом наборе инструментов любого Python-разработчика. Не поленитесь прочитать наше руководство по применению модуля.
10. Цикл for внутри функции
Представьте, что вы создаете функцию, которую нужно повторить некоторое количество раз. Очевидный способ решения этой задачи – помещение функции внутрь цикла for
.
def compute_cubic1(number):
return number**3
%%time
new_list_cubic1 = [compute_cubic1(number) for number in a_long_list]
# Вывод в консоль
# CPU times: user 335 ms, sys: 14.3 ms, total: 349 ms
# Wall time: 354 ms
Однако правильнее будет перевернуть конструкцию – и поместить цикл внутрь функции.
def compute_cubic2():
return [number**3 for number in a_long_list]
%%time
new_list_cubic2 = compute_cubic2()
# Вывод в консоль
# CPU times: user 261 ms, sys: 15.7 ms, total: 277 ms
# Wall time: 277 ms
В данном примере для миллиона итераций (длина a_long_list
) мы сэкономили около 22% времени.
Будем рады, если вы поделитесь в комментариях своими подходами к ускорению кода в Python. Вот ещё несколько статей, которые могут вас заинтересовать:
Комментарии