Python уже с давних лет имеет титул медленного языка. Несмотря на это, его повсеместно используют по поводу и без. Его популярность растет - в информационной безопасности, в анализе данных, машинном обучении и даже написании оконных менеджеров (тайловый оконный менеджер QTile, написанный целиком и полностью на python).
Но каковы все-таки причины медленности python?
Причина первая: — в GIL (Global Interpreter Lock, глобальная блокировка интерпретатора).
Причина вторая состоит в том, что Python — это интерпретируемый, а не компилируемый язык.
Причина третья — в динамической типизации.
Вкратце, GIL запрещает одним переменным пользоваться несколькими потоками процесса. Тут и проявляется действие «глобальной блокировки интерпретатора», которая тщательно контролирует выполнение потоков. Интерпретатор может выполнять лишь одну операцию за раз, независимо от того, как много потоков имеется в программе.
Еще, что делает Python более медленным, по сравнению с Java, C# это отсутствие в питоне JIT-компиляции (Just In Time compilation, компиляция «на лету» или «точно в срок»), которая требует наличия промежуточного языка для того, чтобы позволить осуществлять разбиение кода на фрагменты (кадры). Системы AOT-компиляции (Ahead Of Time compilation, компиляция перед исполнением) спроектированы так, чтобы обеспечить полную работоспособность кода до того, как начнётся взаимодействие этого кода с системой.
Проверка и конверсия типов — операции тяжёлые. Каждый раз, когда выполняется обращение к переменной, её чтение или запись, производится проверка типа.
Язык, обладающей подобной гибкостью, сложно оптимизировать. Причина, по которой другие языки настолько быстрее Python, заключается в том, что они идут на те или иные компромиссы, выбирая между гибкостью и производительностью.
Проект Cython объединяет Python и статическую типизацию, что приводит к 84-кратному росту производительности в сравнении с применением обычного Python. Обратите внимание на этот проект, если вам нужна скорость.
Причиной невысокой производительности Python является его динамическая природа и универсальность. Его можно использовать как инструмент для решения разнообразнейших задач. Для достижения тех же целей можно попытаться поискать более производительные, лучше оптимизированные инструменты. Возможно, найти их удастся, возможно — нет.
Приложения, написанные на Python, можно оптимизировать, используя возможности по асинхронному выполнению кода, инструменты профилирования, и — правильно подбирая интерпретатор. Так, для оптимизации скорости работы приложений, время запуска которых неважно, а производительность которых может выиграть от использования JIT-компилятора, рассмотрите возможность использования PyPy. Если вам нужна максимальная производительность и вы готовы к ограничениям статической типизации — взгляните на Cython.
Способ ускорения №1: Аннотации
Да, аннотации не так сильно увеличивают скорость выполнения и запуска вашего кода, но все равно, это вам сильно поможет.
Так что такое аннотации? Аннотации типов - это особый способ отметки типов переменной. Как мы знаем, в питоне динамическая типизация, что означает, что всем переменным и функциям автоматически устанавливается тип, в зависимости от вывода функции или типа переменной (строка, число, число с плавающей запятой, None).
Аннотации типов просто считываются интерпретатором Python и никак более не обрабатываются, но доступны для использования из стороннего кода и в первую очередь рассчитаны для использования статическими анализаторами.
Как сделать аннотацию типов для переменных? Легко: мы создаем переменную, и сразу после названия, не ставя лишних символов, вставляем знак : (знак двоеточия) и тип переменной - int, float, str, bool или другие.
Если вы хотите создать функцию, с аннотацией, какой тип она выводит, а также переменные какого типа принимает, то вам нужно всего лишь:
def return_hello(name: str, age: int) -> str:
return f'Hi, {name} ({age} years)'
# данная функция принимает строковую переменную name и числовую age
# возвращает строковое знчение
def print_hello(name: str, age: int) -> None:
print(f'Hi, {name} ({age} years)')
# данная функция возвращает ничего (None), поэтому аннотация типов None
Если вы создаете свой класс:
class Book:
title: str
author: str
def __init__(self, title: str, author: str) -> None:
self.title = title
self.author = author
b: Book = Book(title='Властелин Колец', author='Толкин')
Или вы хотите сделать для переменной возможность вписания в нее нескольких типов?
from typing import Union
def hundreds(x: Union[int, float]) -> int:
return (int(x) // 100) % 10
hundreds(100.0)
hundreds(100)
А если переменная это список, словарь или кортежи?
from typing import Tuple, List, Dict
# Кортежи
price_container: Tuple[int] = (1,)
price_with_title: Tuple[int, str] = (1, "hello")
prices: Tuple[int, ...] = (1, 2)
prices = (1, )
something: Tuple = (1, 2, "hello")
# Словари
book_authors: Dict[str, str] = {"Fahrenheit 451": "Bradbury"}
# Списки
titles: List[str] = ["hello", "world"]
items: List = ["hello", 1]
Интересно, что при записи значения другого типа в переменную (int в str например) - не выведет ошибки (как минимум в последнем релизе Python 3.11.5).
Магия
Python создает новый объект таким образом, что под него выделяется очень много информации, о которой мы даже не догадываемся. Надо понимать, что python создает объект __dict__ внутри класса для того, чтобы можно было добавлять новые атрибуты и удалять уже имеющиеся без особых усилий и последствий.
Если мы уже знаем, какие атрибуты должны быть у нас. В python’e есть магический атрибут __slots__, который позволяет отказаться от __dict__. Отказ от __dict__ приведет к тому, что для новых классов не будет создаваться словарь со всеми атрибутами и хранимым в них данными, по итогу объем занимаемой памяти должен будет уменьшиться.
class Coder:
__slots__ = ['name', 'age']
def __init__(self, name: str, age: int) -> None:
self.name = name
self.age = age
Но у такого подхода есть и минусы, например, если вы захотите добавить новый атрибут, вы получите ошибку.
PyPy
PyPy использует JIT компилятор. Скачайте его с этого сайта. И запустите ваш код через него (PyPy поддерживает не все доступные python-модули) - pypy3 <название>.py
LRU кеширование
Кэширование – один из подходов, который при правильном использовании значительно ускоряет работу и снижает нагрузку на вычислительные ресурсы. В модуле стандартной библиотеки Python functools реализован декоратор @lru_cache
, дающий возможность кэшировать вывод функций, используя стратегию Least Recently Used (LRU, «вытеснение давно неиспользуемых»). Это простой, но мощный метод, который позволяет использовать в коде возможности кэширования.
Кэширование – это метод оптимизации хранения данных, при котором операции с данными производятся эффективнее, чем в их источнике.
Представим, что мы создаем приложение для чтения новостей, которое агрегирует новости из различных источников. Пользователь перемещается по списку, приложение загружает статьи и отображает их на экране.
Как поступит программа, если читатель решит сравнить пару статей и станет многократно между ними перемещаться? Без кэширования приложению придется каждый раз получать одно и то же содержимое. В этом случае неэффективно используется и система пользователя, и сервер со статьями, на котором создается дополнительная нагрузка.
Лучшим подходом после получения статьи было бы хранить контент локально. Когда пользователь в следующий раз откроет статью, приложение сможет открыть контент из сохраненной копии, вместо того, чтобы заново загружать материал из источника. В информатике этот метод называется кэшированием.
Но кеширование занимает место и замедляет код.
from functools import lru_cache
@lru_cache
def steps_to(stair):
if stair == 1:
return 1
elif stair == 2:
return 2
elif stair == 3:
return 4
else:
return (steps_to(stair - 3)
+ steps_to(stair - 2)
+ steps_to(stair - 1))
# Вывод
>>> steps_to(30)
>>> 53798080
Время: 82.7 ns ± 3.4 ns per loop (mean ± std. dev. of 7 runs, 10000000 loops each)
Подключив декоратор @lru_cache
, мы сохраняем каждый вызов и ответ в памяти для последующего доступа, если они потребуются снова. Но сколько таких комбинаций мы можем сохранить, пока не иссякнет память?
У декоратора @lru_cache
есть атрибут maxsize, определяющий максимальное количество записей до того, как кэш начнет удалять старые элементы. По умолчанию maxsize равен 128. Если мы присвоим maxsize значение None, то кэш будет расти без всякого удаления записей. Это может стать проблемой, если мы храним в памяти слишком много различных вызовов.
Применим @lru_cache
с использованием атрибута maxsize и добавим вызов метода cache_info():
@lru_cache(maxsize=16)
def steps_to(stair):
if stair == 1:
return 1
elif stair == 2:
return 2
elif stair == 3:
return 4
else:
return (steps_to(stair - 3)
+ steps_to(stair - 2)
+ steps_to(stair - 1))
# Вывод
>>> steps_to(30)
53798080
>>> steps_to.cache_info()
CacheInfo(hits=52, misses=30, maxsize=16, currsize=16)
В этой статье я постарался рассмотреть главные способы ускорения кода. Если данная статья наберет большой отклик, то я сделаю статью об том, как подключать свои самописные библиотеки на C в python, что сделает ваш код быстрее
С вами был доктор Аргентум, всем пока! Ставьте плюсы, пишите свои комментарии. Я надеюсь вам понравилась моя статья.
Проверка и конверсия типов — операции тяжёлые. Каждый раз, когда выполняется обращение к переменной, её чтение или запись, производится проверка типа.
Язык, обладающей подобной гибкостью, сложно оптимизировать. Причина, по которой другие языки настолько быстрее Python, заключается в том, что они идут на те или иные компромиссы, выбирая между гибкостью и производительностью.
Проект Cython объединяет Python и статическую типизацию, что, например, как показано в этом материале, приводит к 84-кратному росту производительности в сравнении с применением обычного Python. Обратите внимание на этот проект, если вам нужна скорость.