Книга: Чистый Python. Тонкости программирования для профи
Назад: 7. Трюки со словарем
Дальше: 9. Итоги

8. Питоновские методы  повышения производительности

8.1. Исследование модулей и объектов Python

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

Многие языки программирования затрудняют инспектирование пакета или класса без сверки с онлайн-документацией или заучивания определений интерфейсов наизусть.

В Python дела обстоят по-другому — эффективный разработчик будет проводить массу времени в сеансах интерпретатора REPL, работая интерактивно с интерпретатором Python. Например, я часто это делаю для разработки коротких фрагментов кода и логики, после чего копирую их и вставляю в файл Python, с которым я работаю в своем редакторе.

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

Эти приемы будут работать с любой версией Python — надо лишь запустить интерпретатор Python командой python из командной строки и приступить к работе. Интерпретатор прекрасно подойдет для сеансов отладки в системах, где, например, у вас нет доступа к причудливому редактору или IDE, потому что вы работаете по Сети в терминальном сеансе.

Готовы? Поехали! Представьте, что вы пишете программу, которая использует модуль Python datetime стандартной библиотеки. Как узнать, какие функции или классы этот модуль экспортирует и какие методы и атрибуты находятся в его классах?

Один из способов заключается в том, чтобы обратиться за советом к поисковой системе или заглянуть в официальную документацию Python в Сети. Однако встроенная в Python функция dir() позволяет вам получать доступ к этой информации непосредственно из Python REPL:

>>> import datetime

>>> dir(datetime)

['MAXYEAR', 'MINYEAR', '__builtins__', '__cached__',

  '__doc__', '__file__', '__loader__', '__name__',

  '__package__', '__spec__', '_divide_and_round',

  'date', 'datetime', 'datetime_CAPI', 'time',

  'timedelta', 'timezone', 'tzinfo']

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

Поскольку в Python абсолютно «все» является объектом, тот же самый прием будет работать не только с модулями как таковыми, но и с классами и структурами данных, экспортируемыми этим модулем.

В действительности можно продолжить углубляться в подробности модуля, снова вызывая dir() с отдельными объектами, которые вызывают интерес. Например, ниже показано, как инспектируется класс datetime.date:

>>> dir(datetime.date)

['__add__', '__class__', ..., 'day', 'fromordinal',

  'isocalendar', 'isoformat', 'isoweekday', 'max',

  'min', 'month', 'replace', 'resolution', 'strftime',

  'timetuple', 'today', 'toordinal', 'weekday', 'year']

Как видите, функция dir() дает вам краткий обзор того, что доступно в модуле или классе. Если вы не помните точного написания конкретного класса или функции, то, возможно, это все, что вам нужно, чтобы продолжать работу, не прерывая процесс программирования.

Иногда вызов функции dir() с объектом в качестве аргумента выдаст слишком много информации — составной модуль или класс вызовет длинную распечатку, которую трудно быстро прочитать. Ниже приведен небольшой трюк, который можно применять для сведения списка атрибутов к тем, которыми вы интересуетесь:

>>> [_ for _ in dir(datetime) if 'date' in _.lower()]

['date', 'datetime', 'datetime_CAPI']

Здесь я использовал конструкцию включения в список для фильтрации результатов вызова dir(datetime), чтобы получить только имена, которые включают слово «date». Обратите внимание на то, как я вызывал метод lower() с каждым именем, тем самым гарантируя, что фильтрация будет нечувствительна к регистру.

Получение сырого списка атрибутов объекта не всегда обеспечивает нас достаточной информацией, необходимой для решения текущей задачи. Тогда каким образом можно получить больше информации и более подробные описания функций и классов, экспортируемых модулем datetime?

В этом случае вам поможет встроенная в Python функция help(). С ее помощью вы можете вызывать интерактивную справочную систему Python и просматривать автоматически сгенерированную документацию Python по любому объекту:

>>> help(datetime)

Если выполнить приведенный выше пример в сеансе интерпретатора Python, то ваш терминал покажет текстоориентированный экран справки для модуля datetime, который будет выглядеть примерно так:

Help on module datetime:

 

NAME

     datetime — Fast implementation of the datetime type.

CLASSES

     builtins.object

         date

             datetime

         time

Вы можете использовать клавиши «курсор вверх» и «курсор вниз», чтобы прокрутить документацию на экране. Как вариант, также можно нажимать клавишу «пробел», чтобы прокручивать вниз сразу несколько строк. Чтобы выйти из режима интерактивной справки, нужно нажать клавишу q. Это вернет вас назад к командной строке интерпретатора. Неплохая возможность, да?

Между прочим, вы можете вызывать help() с произвольными объектами Python, включая другие встроенные функции и ваши собственные классы Python. Интерпретатор Python автоматически сгенерирует эту документацию на основе атрибутов, определенных в объекте, и его строки документации docstring (при ее наличии). Все приведенные ниже примеры являются допустимыми применениями функции help:

>>> help(datetime.date)

>>> help(datetime.date.fromtimestamp)

>>> help(dir)

Разумеется, функции dir() и help() не заменят собой красиво отформатированную HTML-документацию, мощь поисковой системы или поиск на сайте Stack Overflow. Но они являются великолепными инструментами для оперативной сверки, не требующим от вас переключения с интерпретатора Python. Они также доступны вне Сети и работают без подключения к интернету, что может оказаться очень полезным в случае крайней необходимости.

Ключевые выводы

• Используйте встроенную функцию dir(), чтобы интерактивно исследовать модули и классы Python, находясь внутри сеанса интерпретатора.

• Встроенная функция help() позволяет просматривать документацию прямо из вашего интерпретатора (для выхода нажмите клавишу q).

8.2. Изоляция зависимостей проекта при помощи Virtualenv

Python содержит мощную систему управления пакетами, позволяющую управлять модулями, от которых зависят ваши программы. Вы, вероятно, ее использовали, когда устанавливали сторонние пакеты при помощи команды менеджера пакетов pip.

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

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

Что, если один из ваших проектов нуждается в версии 1.3 библиотеки, в то время как для другого проекта нужна версия 1.4 той же самой библиотеки?

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

И чем дальше, тем хуже. У вас также могут быть разные программы, для которых нужны различные версии самого языка Python. Например, некоторые программы могут по-прежнему выполняться в Python 2, в то время как основная часть вашей новой разработки происходит в Python 3. Или что, если для одного из ваших проектов нужен Python 3.3, в то время как все остальное работает в Python 3.6?

Помимо этого, глобальная установка пакетов Python также может стать фактором риска с точки зрения обеспечения безопасности. Для модификации глобальной среды нередко требуется, чтобы вы выполняли команды pip install с правами суперпользователя (root-, или админ-правами). Когда вы устанавливаете новый пакет, менеджер пакетов pip скачивает и исполняет код из интернета, а это обычно не рекомендуется. Хотелось бы надеяться, что устанавливаемый программный код заслуживает доверия, но кто его знает, что он делает на самом деле…

Виртуальные среды спешат на помощь

Решение этих проблем заключается в том, чтобы отделить вашу среду Python так называемыми виртуальными средами (virtual environment). Они позволяют вам отделять зависимости Python на основе того или иного проекта и предоставляют возможность выбирать между различными версиями интерпретатора Python.

Виртуальная среда — это изолированная среда Python. Физически она располагается внутри папки, содержащей все пакеты и другие программные средства, от которых они зависят, в виде библиотек с нативным (платформенно-ориентированным) кодом и средой выполнения интерпретатора, в которых нуждается проект Python. (За кадром, чтобы сэкономить место, эти файлы могут быть символическими ссылками, а не реальными копиями.)

Чтобы продемонстрировать работу виртуальной среды, я представлю небольшую пошаговую демонстрацию, в которой мы выполним настройку новой виртуальной среды (или virtualenv, как ее называют для краткости), а затем установим в нее сторонний пакет.

Прежде всего проверим, где в настоящее время располагается глобальная среда Python. В Linux или macOS для проверки пути к менеджеру пакетов pip мы можем использовать инструмент командной строки which:

$ which pip3

/usr/local/bin/pip3

Я обычно размещаю свои виртуальные среды прямо в папки проектов, чтобы держать их в безупречном виде и отделенными от остальных. Но вы также можете где-нибудь иметь специальный каталог «python-environments», который содержит все ваши среды для проектов. Выбор за вами.

Давайте создадим новую виртуальную среду Python:

$ python3 -m venv ./venv

Эта команда за одну минуту создаст новую папку venv в текущем каталоге, а также заполнит ее базовой средой Python 3:

$ ls venv/

bin        Include    Lib       pyvenv.cfg

Если вы проверите активную версию pip (командой which), то увидите, что она по-прежнему указывает на глобальную среду, в моем случае /usr/local/bin/pip3:

(venv) $ which pip3

/usr/local/bin/pip3

Это означает, что если установить пакеты сейчас, то они по-прежнему окажутся в глобальной среде Python. Одного создания папки виртуальной среды недостаточно — вам нужно явным образом активировать новую виртуальную среду, чтобы последующие выполнения команды pip указывали на нее:

$ source ./venv/bin/activate

(venv) $

Выполнение команды activate конфигурирует текущий сеанс вашей оболочки, чтобы вместо этого использовать Python и команды pip из виртуальной среды.

Обратите внимание на то, как это изменило вид подсказки в строке команд­ной оболочки, и теперь она содержит название активной виртуальной среды в круглых скобках: (venv). Давайте проверим, какой исполняемый файл pip теперь активен:

(venv) $ which pip3

/Users/dan/my-project/venv/bin/pip3

Как видите, выполнение команды pip3 теперь будет запускать ту версию, которая находится в виртуальной среде, а не глобальной. То же касается и исполняемого файла интерпретатора Python. Выполнение python из командной строки теперь также загрузит интерпретатор из папки venv:

(venv) $ which python

/Users/dan/my-project/venv/bin/python

Обратите внимание: она по-прежнему представляет собой «чистую доску», абсолютно пустую среду Python. Выполнение команды pip list покажет почти пустой список установленных пакетов, который будет включать только базовые модули, необходимые для поддержки pip как такового:

(venv) $ pip list

pip (9.0.1)

setuptools (28.8.0)

Давайте пойдем дальше и теперь установим пакет Python в виртуальную среду. Для этого вам следует применить знакомую команду pip install:

(venv) $ pip install schedule

Collecting schedule

   Downloading schedule-0.4.2-py2.py3-none-any.whl

Installing collected packages: schedule

Successfully installed schedule-0.4.2

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

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

Еще раз выполнив pip list, вы увидите, что библиотека schedule была успешно установлена в новую среду:

(venv) $ pip list

pip (9.0.1)

schedule (0.4.2)

setuptools (28.8.0)

Если запустить сеанс интерпретатора Python командой python или выполнить им автономный файл .py, то он будет использовать интерпретатор Python и зависимости, установленные в эту виртуальную среду, — при условии, что эта среда по-прежнему активна в текущем сеансе оболочки.

Но как снова деактивировать или «покинуть» виртуальную среду? Аналогично команде activate, существует команда deactivate, которая возвращает вас назад к глобальной среде:

(venv) $ deactivate

$ which pip3

/usr/local/bin

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

Понимание и использование виртуальных сред также направит вас по правильному пути использования более продвинутых методов управления зависимостями, таких как определение зависимостей проекта при помощи файлов requirements.txt.

Если вы ищете материал с глубоким изложением этой темы и с дополнительными советами по поводу производительности, обратитесь к моему Курсу управления зависимостями Python, который можно найти на dbader.org.

Ключевые выводы

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

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

8.3. По ту сторону байткода

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

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

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

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

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

В качестве подопытного образца давайте возьмем простую функцию greet(), с которой можно поэкспериментировать и которую можно использовать, чтобы разобраться в байткоде Python:

def greet(name):

     return 'Привет, ' + name + '!'

 

>>> greet('Гвидо')

'Привет, Гвидо!'

Если помните, я уже отмечал, что Python сначала транслирует наш исходный код в промежуточный язык, прежде чем он его «выполнит». Так вот, если это правда, то мы должны увидеть результаты этого шага компиляции. И мы можем.

Каждая функция имеет атрибут __code__ (в Python 3), который мы можем использовать, чтобы получить инструкции виртуальной машины, константы и переменные, используемые нашей функцией greet:

>>> greet.__code__.co_code

b'dx01|x00x17x00dx02x17x00Sx00'

>>> greet.__code__.co_consts

(None, 'Привет, ', '!')

>>> greet.__code__.co_varnames

('name',)

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

Поэтому вместо того, чтобы повторять фактические постоянные величины в потоке команд co_code, Python хранит константы отдельно в поисковой таблице. Поток команд затем может ссылаться на константу по индексу в поисковой таблице. То же самое верно и для переменных, хранящихся в поле co_varnames.

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

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

Дизассемблер байткода Python располагается в модуле dis, который является составной частью стандартной библиотеки. Поэтому мы можем его просто импортировать и вызвать dis.dis() с функцией greet в качестве аргумента, чтобы получить более удобочитаемое представление о ее байткоде:

>>> import dis

>>> dis.dis(greet)

   2          0 LOAD_CONST           1 ('Привет, ')

               2 LOAD_FAST           0 (name)

               4 BINARY_ADD

               6 LOAD_CONST          2 ('!')

               8 BINARY_ADD

              10 RETURN_VALUE

Главное, что сделал дизассемблер, было разбиение потока команд и назначение каждому находящемуся в нем коду операции человекочитаемого имени, как, например, LOAD_CONST.

Вы также видите, как ссылки на константы и переменные теперь чередуются с байткодом и выведены полностью, чтобы уберечь нас от мозговой гимнастики относительно поиска по таблице co_const или co_varnames. Круто!

Глядя на человекочитаемые коды операций, мы начинаем понимать, как Python представляет и исполняет выражение 'Привет, ' + name + '!' в исходной функции greet().

Сначала он извлекает константу в индексе 1 ('Привет, ') и помещает ее в стек. Затем он загружает содержимое переменной name и также помещает ее в стек.

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

Между прочим, здесь я касаюсь лишь верхов. Если вы интересуетесь этой темой, то дальше найдете рекомендации для дальнейшего изучения. Более глубокое ознакомление с принципами работы виртуальных машин открывает глаза на многое (а еще это весело).

Самое интересное относительно стека как абстрактной структуры данных состоит в том, что на минимальном уровне он поддерживает всего две операции: вталкивание (push) и выталкивание (pop). Вталкивание добавляет значение на вершину стека, а выталкивание удаляет и возвращает самое верхнее значение. В отличие от массива, в стеке отсутствует способ получить доступ к элементам «ниже» верхнего уровня.

Просто поразительно, что такая простая структура данных пользуется столь большой популярностью. Однако я увлекся.

Давайте предположим, что вначале стек пустой. После того как первые два кода операции были исполнены, содержимое стека виртуальной машины будет выглядеть следующим образом (0 — это самый верхний элемент):

0: 'Гвидо' (содержимое "name")

1: 'Привет, '

Инструкция BINARY_ADD выталкивает два строковых значения из стека, конкатенирует их, а затем вталкивает результат снова в стек:

0: 'Привет, Гвидо'

Затем идет еще одна инструкция LOAD_CONST, которая помещает в стек строку с восклицательным знаком:

0: '!'

1: 'Привет, Гвидо'

Следующий код операции BINARY_ADD снова объединяет два значения, чтобы сгенерировать заключительную приветственную строку:

0: 'Привет, Гвидо!'

Последняя байткодовая инструкция — RETURN_VALUE, которая сообщает виртуальной машине следующее: то, что в настоящее время находится на вершине стека, является возвращаемым значением этой функции, и поэтому оно может быть передано источнику вызова.

И — вуаля! — мы только что проследили за тем, как наша функция greet() была исполнена на внутреннем уровне виртуальной машиной Python. Разве не круто?

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

Можно получить массу удовольствия от создания и определения своих собственных байткодовых языков и построения для них небольших экспериментов с использованием виртуальной машины. По этой теме я порекомендовал бы книгу Проектирование компиляторов: виртуальные машины (Compiler Design: Virtual Machines, Wilhelm and Seidl).

Ключевые выводы

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

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

• Займитесь плотнее виртуальными машинами — оно того стоит.

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

См.

Назад: 7. Трюки со словарем
Дальше: 9. Итоги