10 практик коду, що прискорюють виконання програм на Python

  • 1 октября, 09:17
  • 4610
  • 1

«Python - повільний». Напевно ви не раз стикалися з цим твердженням, особливо від людей, які прийшли в Python з C, C ++ або Java. У багатьох випадках це вірно. Цикли або сортування масивів, списків або словників іноді дійсно працюють повільно. Зрештою, головна місія Python - зробити програмування приємним і легким. Заради лаконічності і легкості читання довелося частково принести в жертву продуктивність.

Проте, в останні роки зроблено чимало зусиль для вирішення проблеми. Тепер ми можемо ефективно обробляти великі набори даних за допомогою NumPy, SciPy, Pandas і numba, оскільки всі ці бібліотеки написані на C / C ++. Ще один цікавий проект - PyPy прискорює код Python в 4.4 рази в порівнянні з CPython (оригінальна реалізація Python).

Недолік PyPy - немає підтримки деяких популярних модулів, наприклад, Matplotlib, SciPy.

10 практик коду, що прискорюють виконання програм на Python

Але прискорити Python можна і без зовнішніх бібліотек. У наших силах розігнати його за допомогою корисних трюків, які використовуються в повсякденній практиці кодингу.

1. Стандартні функції

10 практик коду, що прискорюють виконання програм на Python

Список вбудованих функцій Python 3

В 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. Cортування по значенням, результат в вигляді словника
%%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(), можна безпосередньо викликати { } і [ ] . Цей прийом не обов'язково прискорить ваш код, але зробить його більш " 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}

Розберемося в коді:

  1. Вираз sorted (a_dict.items(), key=lambda item: item[1]) повертає список кортежів [('A', 1), ('C', 2), ('B', 3), ('D', 4), ('E', 5)].
  2. Далі ми розпаковуємо кортежі і присвоюємо перший елемент кожного кортежу в змінну key, а другий - в змінну value.
  3. Нарешті, зберігаємо кожну пару 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% часу.

Джерело перекладу


1 комментарий
Сортировка:
Добавить комментарий
Brendokhil
Brendokhil 2020, 2 октября, 17:37
0

с такими статьями идите на Хабр там  разную копипасту любят ,   зачем это полотно тут нужно?