Новости

24.10.2023

Книга «Python. Лучшие практики и инструменты. 4-е изд.»

Для кого написана эта книга

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

Она написана для тех, кому интересно оттачивать мастерство владения Python, и прежде всего для профессиональных разработчиков, которые зарабатывают на жизнь программированием на Python. Из этих соображений книга ориентирована прежде всего на инструменты и приемы, необходимые для создания производительного, надежного и простого в сопровождении кода на Python.

Однако это не значит, что для программистов-любителей здесь нет ничего интересного. Книга прекрасно подойдет всем, кто хочет изучить продвинутые концепции языка Python. Чтобы освоить материал книги, достаточно владеть базовыми навыками Python, хотя менее опытным программистам, возможно, понадобится приложить дополнительные усилия. Книга также послужит неплохим введением в новейшие возможности Python для тех, кто немного отстал и пока еще использует старые версии языка.

Разработка расширений на Cython


Именем Cython обозначается как оптимизирующий статический компилятор, так и язык программирования, являющийся надмножеством Python. Он помогает ускорять приложения на Python, компилируя их в машинный код, а также служит «языком-оберткой» для кода, написанного на C и C++.

Как компилятор, Cython транспилирует «родной» код на Python и Cython в расширения на C для Python, которые используют Python/C API. Это позволяет объединить мощь Python и C без необходимости вручную работать с Python/C API.

Как надмножество Python, Cython позволяет использовать статическую типизацию и статическую компоновку библиотек C (в отличие от динамической компоновки общих библиотек), взаимодействовать с заголовочными файлами C и напрямую управлять блокировкой GIL в CPython.

Для начала обсудим Cython как транспилятор.

Cython как транспилятор


Главное преимущество расширений, созданных с помощью Cython, — это использование соответствующего надмножества языка. Впрочем, можно создавать расширения и из обычного кода Python, применяя транспиляцию (компиляцию «исходный код — исходный код»). Это простейший вариант использования Cython, потому что он почти не требует изменений в коде и может заметно улучшить быстродействие с минимальными затратами.

Чтобы создавать расширения на Cython, вам прежде всего понадобится пакет Cython. Его можно установить из PyPI с помощью pip:

$ python3 -m pip install Cython

В Cython есть простая вспомогательная функция cythonize, которая позволяет легко интегрировать процесс компиляции с пакетом setuptools. Допустим, вы хотите откомпилировать реализацию функции fibonacci(), написанную на чистом Python, в расширение на C. Если она находится в модуле fibonacci.py, то минимальный сценарий setup.py может выглядеть так:

from setuptools import setup
from Cython.Build import cythonize

setup(
     name='fibonacci',
     ext_modules=cythonize(['fibonacci.py'])
)

Такой модуль можно установить через pip так же, как обычное расширение на C:

$ python3 -m pip install -e .
Installing collected packages: fibonacci
     Running setup.py develop for fibonacci
Successfully installed fibonacci

Эта команда устанавливает пакет в режиме редактирования, благодаря чему можно просмотреть файлы, сгенерированные в процессе установки. Выполнив ее в своей командной оболочке, вы увидите, что она также создает дополнительные артефакты сборки:

$ ls -1ap
./
../
build/
fibonacci.c
fibonacci.cpython-39-darwin.so
fibonacci.egg-info/
fibonacci.py
setup.py

Файл fibonacci.c в этом выводе — это автоматически сгенерированный код расширения на C. Cython преобразовывает (транспилирует) обычный код на Python в код на C. В процессе установки этот код на C будет использоваться, чтобы сформировать библиотеку модуля расширения; в нашем случае это файл fibonacci.cpython-39-darwin.so.

Чтобы понять, какой объем работы Cython выполняет «под капотом», просмотрите файл fibonacci.c. Он получается довольно длинным. Даже для нашего простого модуля fibonacci.py его длина может превышать 4000 строк.

Если использовать Cython для того, чтобы компилировать исходный код на Python, это дает еще одно преимущество. В процессе установки пакета не всегда обязательно транспилировать его в расширение. Если в среде, где его надо установить, нет Cython или других предусловий сборки, возможна установка в виде обычного пакета Python. При такой модели распространения пользователь, как правило, не заметит никаких функциональных различий в поведении кода. Стандартная практика распространения расширений на Cython состоит в том, чтобы включать в дистрибутив как исходные файлы на Python/Cython, так и код C, сгенерированный из этих исходных файлов.

При таком подходе пакет можно установить тремя разными способами в зависимости от предусловий сборки:
  • Если в среде установки доступен Cython, код расширения на C генерируется на основе предоставленного исходного кода на Python/Cython.
  • Если Cython недоступен, но доступны предусловия сборки (компилятор C, заголовки Python/C API), расширение собирается на основе предварительно сгенерированных файлов C, которые входят в дистрибутив.
  • Если ни один из этих вариантов не доступен, а расширение состоит из исходного кода на чистом Python, то модули устанавливаются как обычный код Python, а этап компиляции пропускается.

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

С официальными рекомендациями по распространению кода на Cython можно ознакомиться по адресу cython.readthedocs.io/src/userguide/source_files_and_compilation.html.

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

Другой, более безопасный вариант — использовать функциональность extras_require из пакета setuptools, чтобы пользователь мог сам решить, хочет ли он использовать Cython с конкретной переменной среды:

import os

from setuptools import setup, Extension

try:
     # транспиляция Cython возможна,
     # только если Cython доступен
     # и специальная переменная среды явно сообщает,
     # что нужно использовать Cython,
     # чтобы генерировать исходный код на C
     USE_CYTHON = bool(os.environ.get("USE_CYTHON"))
     import Cython

except ImportError:
     USE_CYTHON = False

ext = '.pyx' if USE_CYTHON else '.c'

extensions = [Extension("fibonacci", ["fibonacci"+ext])]

if USE_CYTHON:
     from Cython.Build import cythonize
     extensions = cythonize(extensions)

setup(
     name='fibonacci',
     ext_modules=extensions,
     extras_require={
          # Эта конкретная версия Cython будет указана
          # в числе требований, если устанавливать пакет
          # с дополнительным ключом '[with-cython]'
          'with-cython': ['cython==0.29.22']
     }
)

pip поддерживает установку пакетов с дополнениями (extras), для чего к имени пакета добавляется суффикс [имя_дополнения]. Для приведенного примера можно запустить следующую команду, чтобы активировать необязательные требования к версии Cython и компиляцию из локальных источников во время установки:

$ USE_CYTHON=1 pip install .[with-cython]

Переменная окружения USE_CYTHON гарантирует, что pip будет использовать Cython, чтобы компилировать исходные коды .pyx в C, а ключ [with-cython] — что компилятор Cython будет загружен перед установкой.

Хотя с помощью Cython можно компилировать код на обычном Python, гораздо эффективнее будет использовать диалект Cython. Он поддерживает ряд дополнительных возможностей, которые недоступны в обычном коде на Python. Специфические средства Cython рассматриваются в следующем разделе.

Cython как язык


Cython — не только компилятор, но и надмножество языка Python. Термин «надмножество» означает, что Суthon не только поддерживает любой действительный код Python, но и способен расширять его новыми возможностями, такими как вызов функций C или объявление типов C для переменных и атрибутов классов. Таким образом, любой код на Python также является кодом на Cython, но обратное не всегда верно. Это объясняет, почему обычные модули Python так легко компилируются в C с помощью компилятора Cython.

Но мы не остановимся на этом простом факте. Вместо того чтобы просто сказать, что наша эталонная функция fibonacci() также является кодом на Cython, мы попытаемся ее немного усовершенствовать. Это не приведет к реальной оптимизации, потому что мы по-прежнему реализуем вычисление чисел Фибоначчи в рекурсивном виде. Но мы внесем небольшие изменения, чтобы извлечь больше пользы от того, что код написан на Cython.

Для исходного кода Cython используется другое расширение файла — .pyx вместо .py. Содержимое файла fibonacci.pyx может выглядеть так:

"""Модуль Cython, реализующий числа Фибоначчи."""

def fibonacci(unsigned int n):
     """Возвращает n-е число Фибоначчи, вычисленное рекурсивно."""
     if n == 0:
          return 0
     if n == 1:
          return 1
     else:
          return fibonacci(n - 1) + fibonacci(n - 2)

По сути, изменилась только сигнатура функции fibonacci(). Благодаря необязательной статической типизации в Cython можно объявить аргумент n c типом unsigned int, и от этого функция будет работать немного эффективнее. Есть и дополнительный эффект: если аргумент функции Cython объявлен со статическим типом, то расширение будет автоматически обрабатывать ошибки преобразования и переполнения, порождая соответствующие исключения. Следующий фрагмент интерактивного сеанса показывает, как это происходит с функцией fibonacci(), написанной на Cython:

>>> from fibonacci import fibonacci
>>> fibonacci(5)
5
>>> fibonacci(0)
0
>>> fibonacci(-1)
Traceback (most recent call last):
     File "<stdin>", line 1, in <module>
     File "fibonacci.pyx", line 4, in fibonacci.fibonacci
          def fibonacci(unsigned int n):
OverflowError: can't convert negative value to unsigned int
>>> fibonacci(10 ** 10)
Traceback (most recent call last):
     File "<stdin>", line 1, in <module>
     File "fibonacci.pyx", line 4, in fibonacci.fibonacci
          def fibonacci(unsigned int n):
OverflowError: value too large to convert to unsigned int

Мы уже знаем, что Cython выполняет только транспиляцию (преобразование исходного кода в исходный код), а генерируемый код использует тот же Python/C API, с которым мы бы работали, если бы вручную писали расширения на C. Учтите, что функция fibonacci() является рекурсивной и поэтому очень часто вызывает саму себя. А значит, несмотря на то что мы объявили статический тип для входного аргумента, во время рекурсивного вызова функция будет рассматривать себя как любую другую функцию Python. Таким образом, n-1 и n-2 будут упаковываться обратно в объект Python, а затем передаваться скрытому уровню-обертке внутренней реализации fibonacci(), которая вернет их к типу unsigned int. Это будет повторяться снова и снова, пока программа не достигнет максимальной глубины рекурсии. Такая схема не обязательно является проблемой, но требует значительно большей обработки аргументов, чем действительно необходимо.

Чтобы сократить лишние затраты на вызовы функций Python и обработку аргументов, можно делегировать больше работы функции на чистом C, которая ничего не знает о структурах Python. Мы занимались этим раньше, когда создавали расширения на чистом C, и то же самое можно сделать в Cython. С помощью ключевого слова cdef объявляются функции в стиле C, которые принимают и возвращают только типы C:

cdef long long fibonacci_cc(unsigned int n):
     if n == 0:
          return 0
     if n == 1:
          return 1
     else:
          return fibonacci_cc(n - 1) + fibonacci_cc(n - 2)

def fibonacci(unsigned int n):
     """ Возвращает n-е число Фибоначчи, вычисленное рекурсивно.
     """
     return fibonacci_cc(n)

Функция fibonacci_cc() не будет доступна для импортирования в итоговом скомпилированном модуле fibonacci. Вместо этого функция fibonacci() формирует фасад к низкоуровневой реализации fibonacci_cc().

Можно пойти еще дальше. В примере с обычным C мы показали, как снимать GIL на время вызова функции на чистом C, чтобы расширение лучше вело себя в многопоточных приложениях. В предыдущих примерах использовались макросы препроцессора Py_BEGIN_ALLOW_THREADS и Py_END _ALLOW_THREADS из заголовков Python/C API, чтобы пометить участки кода, в которых нет вызовов Python. Соответствующий синтаксис Cython немного компактнее и проще запоминается. Чтобы снять блокировку GIL вокруг участка кода, достаточно выполнить команду nogil:

def fibonacci(unsigned int n):
     """ Возвращает n-е число Фибоначчи, вычисленное рекурсивно.
     """
     with nogil:
          return fibonacci_cc(n)

Также можно пометить всю функцию в стиле C как безопасную для вызова без GIL:

cdef long long fibonacci_cc(unsigned int n) nogil:
     if n < 2:
          return n
     else:
          return fibonacci_cc(n - 1) + fibonacci_cc(n - 2)

Важно знать, что объекты Python не могут быть ни аргументами, ни возвращаемыми типами таких функций. Каждый раз, когда функции с пометкой nogil нужно выполнить какой-нибудь вызов Python/C API, она должна установить GIL инструкцией gil.

Вы уже умеете создавать расширения Python двумя способами: в виде кода на обычном C с использованием Python/C API и с помощью Cython. Первый вариант предоставляет больше мощи и гибкости ценой более сложного и подробного кода, а во втором случае писать расширения проще, но Cython выполняет большую часть «черной магии» незаметно для вас. Мы также рассмотрели некоторые потенциальные достоинства расширений, поэтому пришло время поближе познакомиться с их потенциальными недостатками.

Недостатки расширений


Откровенно говоря, я перешел на Python только потому, что мне надоели сложности написания программ на C и C++. На самом деле многие программисты берутся за изучение Python, когда понимают, что другие языки не позволяют осуществить то, что нужно их пользователям.

По сравнению с C, C++ или Java программирование на Python выглядит легкой прогулкой. Вроде бы все здесь устроено просто и хорошо спроектировано. Начинает казаться, что тут вообще негде споткнуться, а другие языки программирования больше не нужны.

И конечно, это абсолютно не соответствует действительности. Безусловно, Python — замечательный язык со множеством интересных возможностей, и он востребован во многих областях. Но это не означает, что он идеален и у него нет недостатков. Код на Python легко писать и понимать, но за эту легкость приходится платить. Он выполняется не настолько медленно, как многие считают, однако никогда не догонит C. Python отличается высокой переносимостью, но его интерпретаторы доступны для меньшего количества архитектур, чем компиляторы некоторых других языков. И этот список можно было бы продолжить.

Чтобы преодолеть многие недостатки, можно разрабатывать расширения, которые позволяют задействовать в Python некоторые сильные стороны старого доброго C. В большинстве случаев этот подход неплохо работает. Вопрос в другом: разве мы пишем код на Python именно для того, чтобы расширять его с помощью C? Нет конечно. Это всего лишь неизбежное зло в ситуациях, где не удается придумать ничего лучше.

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

Дополнительная сложность


Не секрет, что разрабатывать приложения на нескольких языках — непростая задача. Python и C — совершенно разные технологии, и между ними трудно найти что-то общее. Правда и в том, что не существует приложений, в которых вообще нет ошибок. Если активно использовать расширения в кодовой базе, отладка может стать весьма неприятной — не только из-за того, что отладка кода на C требует совершенно иных рабочих процессов и инструментов, но и потому, что вам придется слишком часто переключать мышление между двумя языками.

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

По статистике TIOBE, C до сих пор остается одним из самых популярных языков программирования. Однако, несмотря на это, программисты на Python очень часто знают его на минимальном уровне или не знают вообще. Лично я считаю, что C должен быть универсальным языком общения в мире программирования, но мое мнение вряд ли что-нибудь изменит.

Python также настолько соблазнителен и прост в изучении, что многие программисты забывают весь предыдущий опыт и полностью переключаются на новую технологию. Но не стоит думать, будто программирование — это что-то вроде умения ездить на велосипеде. Этот навык очень легко теряется, если не пользоваться им и не поддерживать квалификацию. Даже программисты с серьезной подготовкой в области C рискуют постепенно утратить свое мастерство, если погрузятся в Python слишком надолго.

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

Более сложная отладка


Дефектные расширения способны создать кучу проблем. Казалось бы, у статической типизации есть множество преимуществ перед обычным кодом на Python, и она позволяет выявить на стадии компиляции многие проблемы, которые было бы трудно обнаружить в Python, причем это даже не потребует скрупулезного тестирования и полного тестового покрытия. Однако это лишь одна сторона монеты.

Другая сторона заключается в том, что памятью приходится управлять вручную. А некорректное управление памятью — главная причина большинства ошибок в программах на C. В лучшем случае такие ошибки приведут к утечкам памяти, которые постепенно исчерпают ресурсы вашей среды. Впрочем, хотя это и лучший случай, это не означает, что с ним легко справиться. Утечки памяти очень трудно обнаружить без таких внешних инструментов, как Valgrind. Как правило, проблемы с управлением памятью в коде расширения приводят к ошибкам сегментации, с которыми невозможно бороться на уровне Python, а интерпретатор аварийно завершается, не порождая исключение, которое объясняло бы причину сбоя. Следовательно, в конце концов вам придется вооружиться дополнительными инструментами, которыми большинство программистов Python обычно не пользуются. От этого и среда разработки, и рабочие процессы становятся сложнее.

Недостатки расширений позволяют сделать вывод, что они не всегда оказываются оптимальным средством для интеграции Python с другими языками. Если вам нужно только взаимодействовать с уже готовыми общими библиотеками, иногда лучше помогает совершенно иной подход. В следующем разделе идет речь о том, как взаимодействовать с динамическими библиотеками, не используя расширения.
Об авторах
Михал Яворски (Michał Jaworski) более 10 лет профессионально разрабатывает программное обеспечение на разных языках. Большую часть этого времени Михал занимался высокопроизводительными распределенными бэкенд-сервисами для веб-приложений. Он работал на разных должностях в нескольких компаниях: от рядового разработчика до ведущего специалиста по архитектуре. Python всегда был и остается его любимым языком.

Тарек Зиаде (Tarek Ziadé) — программист из Бургундии (Франция). Он трудится в Elastic и создает инструменты для разработчиков. До Elastic 10 лет проработал в Mozilla и основал AFPy — французскую группу пользователей Python. Тарек также написал ряд статей о Python для разных журналов и несколько книг на французском и английском языках.
О научном редакторе
Тал Эйнат (Tal Einat) занимается разработкой программного обеспечения более 20 лет, и Python всегда был его основным языком. Тал входит в группу главных разработчиков языка Python с 2010 года. Он получил диплом бакалавра по математике и физике в Тель-Авивском университете. Тал увлекается пешим туризмом, компьютерными играми и философской научной фантастикой, а также любит проводить время с семьей.

В последние 8 лет Тал занимался образовательными технологиями: сначала в Compedia, где создал группу разработки обучающих приложений с использованием виртуальной и дополненной реальности, а потом в стартапе FullProof, где он является одним из основателей.

Сейчас Тал работает в стартапе Rhino Health, который разрабатывает и внедряет модели искусственного интеллекта в области медицины. Эти модели используют данные пациентов со всего мира, обеспечивая их конфиденциальность.

Более подробно с книгой можно ознакомиться на сайте издательства.

Комментарии: 0

Пока нет комментариев


Оставить комментарий






CAPTCHAОбновить изображение

Наберите текст, изображённый на картинке

Все поля обязательны к заполнению.

Перед публикацией комментарии проходят модерацию.