Python уже давно подвергается критике за медленное выполнение, но он по-прежнему, несомненно, является отличным инструментом для обучения и работы. В результате у нас отношения «любовь/ненависть» к Python.

В этой статье собраны несколько советов, которые помогут ускорить выполнение Python и оптимизировать производительность. Все следующие советы были проверены мной и безопасны для использования.

Все тайминги в этой статье используют метод timeit и используют счетчик выполнения по умолчанию, равный одному миллиону раз.

1. Используйте map() для сопоставления функций

Пример: Преобразует строчные буквы массива строк в прописные.

Тестовые массивы: старый список = [‘жизнь’, ‘есть’, ‘короткий’, ‘я’, ‘выбрать’, ‘питон’].

Способ 1

newlist = []
for word in oldlist:
    newlist.append(word.upper())

Способ 2

list(map(str.upper, oldlist))

Метод 1 занимает 0,5267724000000005 с, метод 2 занимает 0,4146256999999999843 с, улучшение производительности на 21,29%.

2. Используйте set(), чтобы найти пересечение

Пример: Найдите пересечение двух списков.

Тестовые массивы: a = [1,2,3,4,5], b = [2,4,6,8,10].

Способ 1

overlaps = []
for x in a:
    for y in b:
        if x == y:
            overlaps.append(x)

Способ 2

list(set(a) & set(b))

Метод 1 занимает 0,9507264000000006 с, метод 2 занимает 0,614820099999999993 с, повышение производительности на 35,33%.

О синтаксисе set(): |, &, и обозначают наборы слияния, пересечения и различия соответственно.

3. Сортировка с помощью sort() или sorted()

Мы можем сортировать последовательность разными способами, но на самом деле написание собственного алгоритма сортировки более чем стоит затраченных усилий. Потому что встроенные методы sort() или sorted() достаточно хороши и достаточно гибки для выполнения различных функций с использованием ключа параметра. Разница между ними заключается в том, что sort() определяется только в списках, а sorted() — это глобальный метод, действительный для всех итерируемых последовательностей.

Пример. Отсортируйте тот же список с помощью методов quick-rank и sort() соответственно.

Тестовый массив: списки = [2,1,4,3,0].

Способ 1

def quick_sort(lists,i,j):
    if i >= j:
        return list
    pivot = lists[i]
    low = i
    high = j
    while i < j:
        while i < j and lists[j] >= pivot:
            j -= 1
        lists[i]=lists[j]
        while i < j and lists[i] <=pivot:
            i += 1
        lists[j]=lists[i]
    lists[j] = pivot
    quick_sort(lists,low,i-1)
    quick_sort(lists,i+1,high)
    return lists

Способ 2

lists.sort()

Метод 1 занимает 2,4796975000000003 с, метод 2 занимает 0,05551999999999999424 с, повышение производительности на 97,76%.

Как видите, sort() по-прежнему очень силен как эксклюзивный метод сортировки списков. sorted() немного медленнее первого, но он «не привередлив» и работает для всех итерируемых последовательностей.

Расширение: как определить метод key of sort() или sorted()?

Определено лямбда-выражением

#students:(name,score,age)
students = [('john', 'A', 15),('jane', 'B', 12),('dave', 'B', 10)]
students.sort(key = lambda student: student[0]) #Sorted by name
sorted(students, key = lambda student: student[0])

Определяется оператором

import operator

students = [('john', 'A', 15),('jane', 'B', 12),('dave', 'B', 10)]
students.sort(key=operator.itemgetter(0))
sorted(students, key = operator.itemgetter(1, 0)) #Sort by grades first, then sort by names

оператор itemgetter() для обычной сортировки массива, attrgetter() для сортировки массива объектов.

Определяется cmp_to_key(), наиболее гибким

import functools

def cmp(a,b):
    if a[1] != b[1]:
        return -1 if a[1] < b[1] else 1 #Sort by score first in ascending order
    elif a[0] != b[0]:
        return -1 if a[0] < b[0] else 1 #If the scores are the same, they will be sorted by name in ascending order
    else:
        return -1 if a[2] > b[2] else 1 #If the scores and names are the same, sort by age in descending order

students = [('john', 'A', 15),('john', 'A', 14),('jane', 'B', 12),('dave', 'B', 10)]
sorted(students, key = functools.cmp_to_key(cmp))

4. Использование списка

Понимание списка короткое и лаконичное. В небольших фрагментах кода это может не иметь большого значения. Но в больших разработках это может сэкономить время.

Пример: Возведите в квадрат нечетные числа в списке и оставьте четные числа без изменений.

Тестовый массив: старый список = диапазон (10).

Способ 1

newlist = []
for x in oldlist:
    if x % 2 == 1:
        newlist.append(x**2)

Способ 2

[x**2 for x in oldlist if x%2 == 1]

Метод 1 занимает 1,5342976000000021 с, метод 2 занимает 1,4181957999999923 с, повышение производительности на 7,57%.

5. Используйте join() для соединения строк

Большинство людей привыкли использовать + для объединения строк. Но на самом деле этот метод очень неэффективен. Потому что операция + создает новую строку и копирует старую на каждом шаге. Лучше использовать join() для соединения строк. Для других операций со строками также попробуйте использовать встроенные функции, такие как isalpha(), isdigit(), startswith(), endswith() и т. д.

Пример: объединяет элементы списка строк.

Тестовый массив: старый список = [‘жизнь’, ‘есть’, ‘короткий’, ‘я’, ‘выбрать’, ‘питон’].

Способ 1

sentence = ""
for word in oldlist:
    sentence += word

Способ 2

"".join(oldlist)

Метод 1 занимает 0,27489080000000854 с, метод 2 занимает 0,08166570000000206 с, повышение производительности на 70,29%.

Другой очень удобной точкой соединения является то, что он может указать разделитель соединения, например:

oldlist = ['life', 'is', 'short', 'i', 'choose', 'python']
sentence = "//".join(oldlist)
print(sentence)

6. Используйте while 1 вместо while True

Когда точное количество циклов неизвестно, традиционный подход состоит в том, чтобы использовать while True для бесконечного цикла и определять в блоке кода, выполняется ли условие завершения цикла. Хотя ничего страшного в этом нет, в то время как 1 выполняется быстрее, чем в то время как True. Это связано с тем, что это численное преобразование, которое быстрее генерирует выходные данные.

Пример: используйте while 1 и while True для повторения 100 раз соответственно.

Способ 1

i = 0
while True:
    i += 1
    if i > 100:
        break

Способ 2

i = 0
while 1:
    i += 1
    if i > 100:
        break

Метод 1 занимает 3,679268300000004 с, метод 2 занимает 3,60784749999999991 с, повышение производительности на 1,94%.

7. Используйте кеш декоратора

Хранение файлов в кэше помогает быстро восстановить функции. python поддерживает кеш-декоратор, который поддерживает определенный тип кеша в памяти для оптимальной скорости, управляемой программным обеспечением. Мы используем декоратор lru_cache, чтобы обеспечить функциональность кэширования для функций Фибоначчи. При использовании рекурсивных функций Фибоначчи выполняется множество повторяющихся вычислений, например, fibonacci(1), fibonacci(2) запускается много раз. Но после использования lru_cache все повторяющиеся вычисления будут выполняться только один раз, что значительно повысит эффективность выполнения программы.

Пример: Найдите ряд Фибоначчи.

Тестовые данные: Фибоначчи(7)

Способ 1

def fibonacci(n):
    if n == 0:
        return 0
    elif n == 1:
        return 1
    return fibonacci(n - 1) + fibonacci(n-2)

Способ 2

import functools

@functools.lru_cache(maxsize=128)
def fibonacci(n):
    if n == 0:
        return 0
    elif n == 1:
        return 1
    return fibonacci(n - 1) + fibonacci(n-2)

Метод 1 занимает 3,955014900000009 с, метод 2 занимает 0,0507797999999998661 с, повышение производительности на 98,72%.

Кэш следует за аргументами как за ключами, а это означает, что функция, украшенная lru_cache, будет выполняться только один раз, когда аргументы остаются неизменными.
Все аргументы должны быть хэшируемыми, т.е. list нельзя использовать в качестве аргумента функции, декорированной lru_cache.

8. Уменьшить оператор точки (.) Использование

Оператор точки (.) используется для доступа к атрибутам или методам объекта, что может привести к тому, что программа будет выполнять поиск в словаре с использованием __getattribute__() и __getattr__(), что приведет к ненужным накладным расходам. В частности, обратите внимание, что использование оператора точки должно быть сокращено еще больше в середине цикла и должно обрабатываться вне цикла.
Это вдохновляет нас попробовать использовать from … import … вместо using оператора точки для получить метод, когда вам нужно его использовать. На самом деле не только оператор точки, но и множество других ненужных операций мы пытаемся вынести за пределы цикла для обработки.

Пример: Преобразует строчные буквы массива строк в прописные.

Тестовый массив имеет вид oldlist = [‘жизнь’, ‘есть’, ‘короткий’, ‘я’, ‘выбрать’, ‘питон’].

Способ 1

newlist = []
for word in oldlist:
    newlist.append(str.upper(word))

Способ 2

newlist = []
upper = str.upper
for word in oldlist:
    newlist.append(upper(word))

Метод 1 занимает 0,7235491999999795 с, метод 2 занимает 0,5475435999999831 с, повышение производительности на 24,33%.

9. используйте цикл for вместо цикла while

Когда мы точно знаем, сколько раз выполнять цикл, лучше использовать цикл for, чем цикл while.

Пример: Используйте for и while, чтобы зациклить по 100 раз каждое.

Способ 1

i = 0
while i < 100:
    i += 1

Способ 2

for _ in range(100):
    pass

Метод 1 занимает 3,894683299999997 с, метод 2 занимает 1,0198077999999953 с, повышение производительности на 73,82%.

10. Ускорение вычислений с помощью Numba.jit

Numba может компилировать функции Python в машинный код для выполнения, значительно увеличивая скорость выполнения кода, даже приближаясь к скорости C или FORTRAN. Его можно использовать с Numpy, чтобы значительно повысить эффективность выполнения в циклах for или когда требуется много вычислений.

Пример: Найдите сумму от 1 до 100.

Способ 1

def my_sum(n):
    x = 0
    for i in range(1, n+1):
        x += i
    return x

Способ 2

from numba import jit

@jit(nopython=True) 
def numba_sum(n):
    x = 0
    for i in range(1, n+1):
        x += i
    return x

Метод 1 занимает 3,7199997000000167 с, метод 2 занимает 0,23769430000001535 с, повышение производительности на 93,61%.

Краткое содержание

Из приведенных выше 10 навыков видно, что большинство навыков, вызванных увеличением производительности, по-прежнему значительны, но есть небольшое количество навыков для увеличения меньшего.

Подводя итог, я думаю, что на самом деле это следующие два принципа:

  1. Попробуйте использовать встроенные библиотечные функции

Встроенные библиотечные функции написаны профессиональными разработчиками и многократно протестированы, и многие из них разработаны на языке C в самом низу. Поэтому эти функции вообще очень эффективны (такие как sort(), join() и т. упомяните, что колесо, которое вы строите, может быть хуже. Итак, если функция уже существует в библиотеке функций, просто используйте ее.

2. Попробуйте использовать отличные сторонние библиотеки

Есть много отличных сторонних библиотек, которые могут быть реализованы на C и Fortran внизу, и вы не ошибетесь с такими библиотеками, как вышеупомянутые Numpy и Numba, которые приносят удивительные улучшения. Есть много других подобных библиотек, таких как Cython и PyPy, так что здесь я просто выбрасываю полотенце.

Я продолжу делиться советами по Python в последующих статьях, поэтому, если вам интересны мои статьи, вы можете подписаться на меня. 👉

Дополнительные материалы на PlainEnglish.io. Подпишитесь на нашу бесплатную еженедельную рассылку новостей. Подпишитесь на нас в Twitter, LinkedIn, YouTube и Discord . Заинтересованы в хакинге роста? Ознакомьтесь с разделом Схема.