Сообщить об ошибке.

PyPy3, полноценная замена классического Python3

Цитата:

"Если необходимо, чтобы Ваш код работал быстрее,
то вероятно, следует просто использовать PyPy"Гвидо ван Россум (создатель Python).

Общие сведения о PyPy.

Компилятор PyPy3 - это полноценная замена CPython. Он построен с использованием языка RPython, который был разработан совместно с ним. Основная причина использовать его вместо CPython - скорость: обычно он работает быстрее.

Цель PyPy - получить скорость, но при этом поддерживать (в идеале) любую программу Python.

На июль 2024 года доступныен выпуск PyPy 7.3.16, который реализует три версии - это Python 3.10, Python 3.9 и Python 2.7. Отличительными особенностями этого выпуска являются совместимость с HPy-0.9, cffi 1.16, дополнительными интерфейсами C-API и другими исправлениями Python3.10. Этот выпуск поддерживает большинство используемых модулей стандартной библиотеки Python. Известные различия с CPython смотрите в разделе совместимости.

Версия включает в себя три разных интерпретатора:

  • PyPy2.7, который является интерпретатором, поддерживающим синтаксис и возможности Python 2.7, включая stdlib для CPython 2.7.18+ (+ предназначен для резервных обновлений безопасности)
  • PyPy3.9, который является интерпретатором, поддерживающим синтаксис и возможности Python 3.9, включая stdlib для CPython 3.9.19.
  • PyPy3.10, который является интерпретатором, поддерживающим синтаксис и возможности Python 3.10, включая stdlib для CPython 3.10.14.

Интерпретаторы основаны практически на одной и той же кодовой базе, следовательно, это множественный выпуск. Это микрорелиз, все API совместимы с другими выпусками 7.3.

Поддерживаются следующие платформы:

  • Linux x86 64 bit (совместим с CentOS7 и более поздними версиями);
  • Windows 64 bit (совместимый с любой 64-разрядной версией Windows, может понадобиться установщик библиотеки времени выполнения vcredist.x64.exe);
  • MacOS arm64 (macOS >= 11. Не подписан, для подписанных пакетов нужно использовать установщик conda. Homebrew пока не предоставляет PyPy3.9+.);
  • MacOS x86_64 (macOS >= 10.15, не для Mojave и ниже. Не подписан, для подписанных пакетов или более старых версий нужно использовать установщик conda. Homebrew пока не предоставляет PyPy3.9+.);
  • Linux ARM64 (совместим с CentOS7 и более поздними версиями);
  • Linux x86 32 bit (совместим с CentOS7 и более поздними версиями);
  • S390x (построен на Redhat Linux 7.2).

Страница загрузки дистрибутива PyPy3

Поддерживаемые архитектуры ЦП:

  • x86 (IA-32) и x86_64,
  • платформа ARM (ARMv6 or ARMv7, with VFPv3),
  • AArch64,
  • PowerPC 64bit как с прямым, так и с обратным порядком байтов,
  • System Z (s390x),

Скорость исполнения кода компилятором PyPy.

Основной исполняемый файл pypy поставляется с компилятором Just-in-Time. Он действительно быстро запускает большинство тестов, включая очень большие и сложные приложения Python, а не только 10-строчные.

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

  • Кратковременные процессы: если PyPy запускается со скриптами работающими меньше 2-х секунд, JIT-компилятору не хватит времени для разгона.
  • JIT-компилятор не поможет, если все время исполнения программы тратится в подключаемых C-библиотеках, а не на выполнение кода, написанного на Python.

Таким образом, лучше всего PyPy работает при выполнении длительно выполняющихся программ, когда значительная часть времени тратится на выполнение кода Python. Это случай, охватываемый большинством проводимых тестов.

Установка PyPy3 на ОС Windows:

Установка PyPy3 ни чем не отличается от установки классического Python3. Загрузить исходники PyPy3 для ОС Windows можно с официальной страницы. Дистрибутив PyPy3 Windows 32 bit совместим с любыми 32- или 64-битными ОС Windows.

Так же, может понадобиться установщик библиотеки времени выполнения VC. Загрузить файл vcredist.x86.exe можно с официальной страницы https://www.microsoft.com/ru-ru/download/details.aspx?id=52685

PyPy3 для ОС Windows готов к запуску сразу после установки из .exe или .msi файла.

Установка PyPy3 на Linux (ОС Ubuntu/Debian):

Команда разработчиков PyPy предоставляет предварительно скомпилированные двоичные файлы для многих платформ и ОС. Загрузить исходники PyPy3 можно с официальной страницы.

PyPy готов к запуску сразу после распаковки его из tarball или zip-архива, без необходимости устанавливать его в каком-либо конкретном месте:

# разархивируем pypy3.10 в директорию `/opt`
$ sudo tar xf pypy3.10-v7.3.16-linux64.tar.bz2 -C /opt
$ /opt/pypy3.10-v7.3.16-linux64/bin/pypy
# Python 3.10.14 (Jan 29 2024, 14:23:21)
# [PyPy 7.3.16 with GCC 10.2.1 20240130 (Red Hat 10.2.1-11)] on linux
# Type "help", "copyright", "credits" or "license" for more information.
# >>>>

Если необходимо сделать PyPy доступным для всей системы, то можно создать символическую ссылку на исполняемый файл ln -s /opt/pypy3.10-v7.3.16-linux64/bin/pypy /usr/local/bin/pypy. Важно разместить символическую ссылку, а не перемещать туда двоичный файл, иначе PyPy не сможет найти свои библиотеки.

Установка дополнительных модулей для PyPy3.

Если необходимо установить сторонние библиотеки, наиболее удобный способ - установить менеджер пакетов pip с помощью инструмента ensurepip. Если вы не хотите устанавливать virtualenv, то тогда можно напрямую использовать pip внутри виртуальной среды исполнения:

# установка менеджера пакетов `pip` в распакованный
#  и готовый сразу к запуску дистрибутив
$ ./pypy-xxx/bin/pypy -m ensurepip
# обновление `pip` до последней версии
$ ./pypy-xxx/bin/pypy -m pip install -U pip wheel 
# пример установки стороннего модуль Flask
$ ./pypy-xxx/bin/pypy -m pip install pygments

Если нужно иметь возможность использовать pip непосредственно из командной строки, то необходимо использовать аргумент --default-pip при вызове surepip. Сторонние библиотеки будут установлены в pypy-xxx/site-packages. Как и в случае с CPython, скрипты в linux и macOS будут в pypy-xxx/bin, а в Windows они будут в pypy-xxx/Scripts.

Установка PyPy3 в виртуальную среду исполнения virtualenv.

Наиболее удобно запускать PyPy3 внутри виртуальной среды исполнения virtualenv. Для этого необходимо установить версию virtualenv-1.6.1 или выше. Затем в установленную среду выполнения, можно установить PyPy как из предварительно скомпилированного архива, так и из разархивированной директории, после проверки PyPy3 на работоспособность.

# Установка из распакованной директории
$ virtualenv -p /opt/pypy-xxx/bin/pypy env-pypy

# активация виртуального аокружения
$ source env-pypy/bin/activate
# (env-pypy) $

Внимание! Не используйте для установки PyPy3 встроенный модуль venv (т.к. venv не копирует компилятор PyPy, а ставит на него только ссылки), создавайте виртуальную среду исполнения только при помощи virtualenv.

Примечание: Если, все-же решили использовать venv в качестве виртуальной среды, то ни в коем случае не удаляйте распакованный исходник. После удаления исходника, экземпляр pypy, расположенный в venv перестанет работать.

Пример установки PyPy, используя модуль venv:

# запускаем из распакованного исходника
/opt/pypy-xxx/bin/pypy -m venv venv-pypy --prompt PYPY3

# активация виртуального окружения
$ source env-pypy/bin/activate
# (PYPY3) $

Обратите внимание, что env-pypy/bin/python теперь является символической ссылкой на env-pypy/bin/pypy, следовательно можно запускать pypy, просто набрав python.

Для PyPy3, установленного в виртуальную среду, все равно необходимо обновить pip и wheel до последних версий через:

$ env-pypy/bin/pypy -m pip install -U pip wheel

Совместимость с классическим Python3.

Чистый код Python работает, но есть несколько отличий в управлении временем жизни объекта. Модули, использующие CPython C API, вероятно, будут работать, но не достигнут ускорения за счет JIT. Авторам библиотек, команда разработчиков рекомендуем использовать CFFI и HPy.

Если необходимо использовать PyPy3 с научной экосистемой Python, то рекомендуется использовать компилятор Python conda, т.к. он переупаковывают общие библиотеки, такие как scikit-learn и SciPy для PyPy.

Расширения языка C должны быть перекомпилированы для PyPy, чтобы они работали. В зависимости от системы сборки они могут работать из коробки или будет немного сложнее.

В основном PyPy3 поддерживает стандартные библиотечные модули. Обратите внимание, что многие модули python3 реализованы на чистом Python, следовательно они точно будут работать. Просто нужно проверить, сможет ли PyPy3 на вашей системе импортировать следующие модули:

__builtin__, __pypy__, _ast, _cffi_backend, _codecs, _collections, _continuation, _csv, _file, _hashlib, _io, _locale, _lsprof, _md5, _minimal_curses, _multibytecodec, _multiprocessing, _pickle_support, _pypyjson, _random, _rawffi, _sha, _socket, _sre, _ssl, _struct, _testing, _warnings, _weakref, array, binascii, bz2, cStringIO, cmath, cppyy, cpyext, crypt, errno, exceptions, fcntl, gc, imp, itertools, marshal, math, mmap, operator, parser, posix, pwd, pyexpat, pypyjit, select, signal, symbol, sys, termios, thread, time, token, unicodedata, zipimport, zlib.

Если PyPy3 импортирует вышеуказанные модули без ошибок, то он полностью совместим с вашим Python3 и должен работать без каких либо ошибок.

Поддерживается и написано на чистом Python: cPickle, ctypes, datetime, dbm, _functools, grp, readline, resource, sqlite3, syslog.

Все сторонние модули, которые написаны на чистым python в CPython, конечно будут работать после успешной установки.

Различия, связанные со стратегиями сбора мусора.

Сборщики мусора, используемые или реализованные PyPy3, не основаны на подсчете ссылок, поэтому объекты не освобождаются мгновенно, когда они больше недоступны. Наиболее очевидный эффект от этого заключается в том, что файлы (и сокеты и т. д.) не закрываются сразу после выхода за пределы области видимости. Это отличие от классического Python3, не будет изменяться командой разработчиков.

Следующий код заполнит файл не сразу, а только через определенный промежуток времени, когда GC выполнит сборку мусора и очистит вывод:

open("filename", "w").write("stuff")

Правильное использование заключается в следующем:

with open("filename", "w") as f:
    f.write("stuff")

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

PyPy3 можно запустить с параметром командной строки -X track-resources (например, pypy -X track-resources myprogram.py). Это вызывает ResourceWarning, когда GC закрывает незакрытый файл или сокет. Также дается трассировка для места, где был открыт файл или сокет, что помогает найти места, где отсутствует метод .close().

Точно так же помните, что необходимо закрывать неизрасходованный генератор, чтобы ожидающие завершения операторы finally или with выполнялись немедленно:

def mygen():
    with foo:
        yield 42

for x in mygen():
    if x == 42:
        # foo .__ exit__ запускается не сразу!
        break

# Необходимо поступать следующим образом
gen = mygen()
try:
    for x in gen:
        if x == 42:
            break
finally:
    gen.close()

В более общем смысле, методы __del__() не выполняются так же предсказуемо, как на CPython: в PyPy3 они запускаются через некоторое время (или не запускаются вообще, если программа тем временем завершает работу).

Обратите внимание, что PyPy3 возвращает неиспользуемую память операционной системе, если есть системный вызов madvise() (по крайней мере в Linux, OS X, BSD) или в Windows. Важно понимать, что можно не увидеть этого в выводе команды терминала top.

Неиспользуемые страницы памяти помечаются MADV_FREE, который сообщает системе: Если вам понадобится больше памяти в какой-то момент, возьмите эту страницу. Пока памяти достаточно, столбец RES вверху может оставаться высоким. Исключением из этого правила являются системы без MADV_FREE, где PyPy3 использует MADV_DONTNEED, который принудительно снижает RES (включает Linux <= 4.4).

Почему PyPy3 жрет так много памяти?

PyPy3 возвращает неиспользуемую память операционной системе только после системного вызова madvise() (по крайней мере, в Linux, OS X, BSD) или в Windows. Важно понимать, что такое поведение может НЕ показываться топе утилиты bash htop. Неиспользуемые страницы помечаются MADV_FREE, что говорит системе: "Если в какой-то момент понадобится больше памяти, то возьмите эту страницу". Пока памяти много, верхний столбец RES остается высоким.

Исключением из этого правила являются системы без MADV_FREE, где PyPy3 использует MADV_DONTNEED, что принудительно снижает RES. Это включает Linux = 4.4.

Подклассы встроенных типов

Официально в CPython вообще нет правила, когда точно переопределенный метод подклассов встроенных типов вызывается неявно или нет. В качестве приближения эти методы никогда не вызываются другими встроенными методами того же объекта. Например, переопределенный __getitem__() в подклассе dict не будет вызываться, например, встроенный метод dict.get().

Сказанное выше верно как для CPython, так и для PyPy. Могут возникнуть различия в том, будет ли встроенная функция или метод вызывать переопределенный метод другого объекта, кроме self. В PyPy они часто вызываются в тех случаях, когда CPython этого не делает.

Два примера:

class D(dict):
    def __getitem__(self, key):
        return "%r from D" % (key,)

class A(object):
    pass

a = A()
a.__dict__ = D()
a.foo = "a's own foo"
print(a.foo)
# CPython => a's own foo
# PyPy => 'foo' from D

glob = D(foo="base item")
loc = {}
exec "print foo" in glob, loc
# CPython => base item
# PyPy => 'foo' from D

Игнорируемые исключения.

Во многих случаях CPython может молча проглатывать исключения. Точный список случаев, когда это происходит, довольно длинный, хотя большинство случаев очень редки. Наиболее известные места - это настраиваемые расширенные методы сравнения (например, __eq__); поиск по словарю; вызовы некоторых встроенных функций, таких как isinstance().

Если это поведение явно не предусмотрено конструкцией и не задокументировано как таковое (например, для hasattr()), в большинстве случаев PyPy будет поднимать исключения.

Идентичность встроенных типов (is и id).

Идентичность объектов примитивных типов работает по равенству значений, а не по идентичности id. Это означает, что x + 1 is x + 1 всегда верно для произвольных целых чисел x. Правило распространяется на следующие встроенные типы:

  • int;
  • float;
  • long;
  • complex;
  • str (только пустые или односимвольные строки)`;
  • unicode (только пустые или односимвольные строки)`;
  • tuple (только пустые кортежи)`;
  • frozenset (только пустой frozenset)`.

Это изменение также требует некоторых изменений в id. id выполняет следующее условие: x is y <=> id(x) == id(y). Поэтому id вышеперечисленных типов будет возвращать значение, которое вычисляется из аргумента, и, таким образом, может быть больше, чем sys.maxint (т. е. может быть произвольно длинным).

Обратите внимание, что строки длиной 2 или более могут быть равны, не будучи идентичными. Аналогично, x is (2,) не обязательно истинно, даже если x содержит кортеж и x == (2,). Правила уникальности применимы только к частным случаям, описанным выше. Правила str, unicode, tuple и frozenset были добавлены в PyPy выпуск 5.4; до этого тест типа if x is "?" или if x is () мог потерпеть неудачу, даже если x был равен "?" или (). Новое поведение, добавленное в PyPy выпуск 5.4, ближе к CPython, который кэширует именно пустой tuple/frozenset и (как правило, но не всегда) str и unicode длинной <= 1.

Обратите внимание, что для float существует только один объект на “битовый шаблон” float. Таким образом, float('nan') is float('nan') истинно на PyPy3, но не на CPython, потому что они являются двумя объектами; но 0.0 is -0.0 всегда False, так как битовые шаблоны различны. Как обычно, float('nan') == float('nan') всегда ложно. При использовании в контейнерах (например, в виде элементов list или в set) точное правило равенства используется так: “if x is y or x == y” (как на CPython, так и на PyPy); как следствие, поскольку все nan идентичны в PyPy3, вы не можете иметь несколько из них в множестве set, в отличие от CPython.

Другим следствием является то, что cmp(float('nan'), float('nan')) == 0, потому что cmp() сначала проверяет is, идентичны ли аргументы (нет хорошего значения для возврата из этого cmp(), так как функция cmp() делает вид, что существует полный порядок для чисел с плавающей запятой, но это неверно для NaN).

Различия в производительности.

CPython имеет оптимизацию, которая может сделать повторную конкатенацию строк неквадратичной. Например, такой код выполняется за время O(n):

s = ''
for string in mylist:
    s += string

В PyPy3 этот код всегда будет иметь квадратичную сложность. Также обратите внимание, что оптимизация CPython хрупкая и в любом случае может сломаться из-за небольших изменений в коде. Так что все равно необходимо заменить код выше на:

parts = []
for string in mylist:
    parts.append(string)
s = "".join(parts)

В принципе это основные отличия с которыми сталкивается 80% разработчиков.