, Внутреннее устройство CPython

Внутреннее устройство CPython

Энтони Шоу

Copyright © Real Python (realpython.com), 2012–2021

Данный документ предоставляется по лицензии Creative Commons Attribution 3.0 License, за исключением разделов со специальными оговорками.

http://creativecommons.org/licenses/by/3.0/legalcode

Обновлена: 25 января 2021

Логотипы “Python” and the Python являются торговой маркой или зарегистрированной торговой маркой Python Software Foundation, применяемой Real Python с разрешения этого фонда.

Благодарим вас за выгрузку этой электронной книги. Эта электронная книга лицензирована исключительно для вашего персонального обладания. Эта книга не может быть перепродана или выдаваться прочим людям. Если вы желаете совместно пользоваться этой книгой с прочими персонами, будьте добры приобрести дополнительную копию для каждого получателя. Если вы читаете эту книгу, но не покупали ей, либо она не была приобретена исключительно для вашего, будьте так любезны вернутся в realpython.com/cpython-internals и приобрести свою собственную копию. Благодарим Вас за уважение к тяжёлой работе над этой книгой.

ISBN 9781775093343 (на бумажной основе)

ISBN 9781775093350 (электронная)

realpython.com

2021-05-03

 Состав исполнителей

Автор

Энтони Шоу

Разработка обложки

Алдрен Сантос

Дополнительное редактирование и правка корректуры

Джейкоб Шмидт

Технолог

Джаоти Чауан

Что говорят читатели об этой книге

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

- Кэрол Виллинг, разработчик ядра CPython и участник CPython Steering Council.

Глава "Параллелизм и одновременность" одна из моих самых любимых. Я искал глубокого проникновения в эту тему и нашёл Вашу книгу чрезвычайно полезной.

Естественно, после прочтения этой главы я не смог устоять и перед остальными Я жду- не дождусь, когда у меня появится моя собственная отпечатанная копия!

Я уже знакомился с Вашей статьёй `Руководство по исходному коду CPython`, которая побудила меня узнать больше о его внутреннем устройстве.

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

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

- Милан Пэйтел, вице- президент (одного из ведущих инвестиционных банков)

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

Погружение в основы C языка Python оказалось большим развлечением и это сняло у меня некоторое стародавние вопросы. Особенно поучительной я нашёл главу о распределении памяти CPythob.

Эта книга отличный (и уникальный) ресурс для всех, кто хочет поднять свои знания Python до более глубокого уровня.

- Дэн Бэйдер, автор Python Tricks и главный редактор Real Python.

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

- Флориан Далитц, приверженец Python

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

- Абнишек Шарма, специалист в области данных

 Об авторе

Энтони Шоу является заядлым последователем Python и участник Python Software Foundation.

Энтони занимается программированием с 12 лет и возлюбил Python, оказавшись в ловушке в отеле в Сиэтле, штат Вашингтон, 15 лет спустя. Отказавшись от прочих языков программирования, которые он изучал, Энтони с тех пор занимается исследованиями, написанием и созданием курсов по Python.

Энтони также участвует в малых и крупных проекта Открытого исходного кода, в том числе CPython, а также участник Apache Software Foundation.

Страсть Энтони состоит в разбирательстве со сложными системами с последующим их упрощением и обучению им людей.

 Рецензенты

Джим Андерсон давно занимается программированием на разных языках. Он работал над встраиваемыми системами, создавал системы рапределённой сборки, управлял внешними поставщиками и участвовал во многих мероприятиях.

Джоанна Джаблонски является исполнительным редактором Real Python. Ей нравятся естественные языки в той же степени, что и языки программирования. Её любовь к головоломкам, узорам и мелким неприятным подробностям привела её к карьере переводчика. Это стало лишь вопросом времени, чтобы она полюбила новый язык Python! Она присоединилась к Real Python в 2018 году и с тех пор помогает последователям Python наращивать свой уровень.

 realpython.com

 Предисловие

 Вступление

"Созданный сообществом язык программирования благоприятствует счастью его пользователей по всему миру."

- Гвидо ван Россум "Речь в честь Дня короля"

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

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

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

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

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

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

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

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

"Сейчас лучше чем никогда"

- Тим Питерс, "Дзен Python"

Давайте последуем этой мудрости Тома и приступим прямо сейчас.

Сердечно с вами,

- Кэрол Виллинг, основной разработчик CPython и участник CPython Steering Council

 Введение

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

Ответ заключается в том, что CPython, наиболее популярная среда времени выполнения Python, написана на читаемым человеком коде C и Python.

CPython абстрагируется от сложности базовой платформы C и вашей операционной системы. Это делает многопоточность простой и переносимой между платформами. Он избавляет от головной боли управления памятью в C и упрощает её.

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

После того как вы разберётесь с тем, как работает CPython, вы сможете полностью воспользоваться его мощностью и оптимизировать свои приложения. Эта книга поясняет основные концепции, идеи и технические стороны CPython.

В этой книги вы рассмотрите основные концепции стоящие за внутренним устройством CPython и изучите как:

Читать исходный код и путешествовать по нему

Компилировать CPython из исходного кода

Вносить изменение в синтаксис Python и компилировать его в вашу версию CPython

Управлять внутренней работой таких функций как списки, словари и генераторы, а также разобраться с ними.

Станьте хозяином возможностей управления памятью CPython

Масштабируйте свой код Python при помощи параллельности и одновременности

Изменяйте центральные типы новыми функциональными возможностями

Исполняйте комплекты тестирования

Профилируйте и выполняйте эталонное тестирование производительности своего кода Python и времени выполнения

Отлаживайте код C и Python как профессионал

Изменяйте и обновляйте компоненты библиотеки CPython для их внесения в последующие версии

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

 Как пользоваться этой книгой

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

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

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

При достаточной практике вы овладеете этим материалом и в процессе получите удовольствие!

Насколько искушён в Python я должен быть чтобы воспользоваться этой книгой?

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

Должен ли я владеть C чтобы пользоваться этой книгой?

Вам нет нужды быть профессионалом в C чтобы применять эту книгу. Если вы новичок для C, просмотрите Дополнение A. Введение в C для программистов Python для быстрого входа в кус дел.

Сколько времени потребуется на освоение этой книги?

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

Не устареет ли содержимое этой книги слишком быстро?

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

В действительности, при написании этой книги мы исследовали многие строки кода, которые были написаны Гвидо ван Россумом (автором Python) и остаются неприкасаемыми начиная с 1 версии.

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

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

 Премиальные материалы и учебные ресурсы

Данная книга поставляется с некоторым числом бесплатных премиальных ресурсов, к которым вы можете получить доступ через realpython.com/cpython-internals/resources/. На этой веб- странице вы также можете обнаружить перечень исправлений с коррекциями, сопровождаемыми командой Real Python.

Образцы кода

Примеры и образцы настроек на протяжении этой книги будут помечаться заголовком, обозначающих их как часть папки cpython-book-samples :

cpython-book-samples/01/example.py

import this

Вы можете выгрузить образцы кода с realpython.com/cpythoninternals/resources/.

Лицензии кода

Все примеры сценариев Python, относящихся к данной книге лицензированы в соответствии с Creative Commons Public Domain (CC0) License. Это означает, что приветствуется использование всех частей данного кода для любых целей в ваших собственных программах.

CPython лицензируется в соответствии с Python Software Foundation 2.0 license. Фрагменты и образцы исходного кода CPython, используемые в этой книге подпадают под терминологию этой лицензии PSF 2.0.

[Замечание]
Замечание

Весь код из этой книги был протестирован при помощи Python 3.9 в Windows 10, macOS 10.15 и Linux.

Соглашения о формате

Блоки кода применяются для представления образцов кода:

# This is Python code:
print("Hello, World!")

Не зависящие от операционной системы команды соответствуют формату в стиле Unix:

$ # This is a terminal command:
$ python hello-world.py

(Символ $ не является частью команды).

Специфичные для Windows команды имеют вормат командной строки Windows:

> python hello-world.py

(Символ > не является частью команды).

Синтаксис командной строки следует такому формату:

Текст без скобок должен набираться так, как он отображён.

<Текст внутри угловых скобок> указывает на некую переменную, для который вам надлежит подставить
	 некое значение. Например, вы можете заменить <filename> соответствующим названием для
	 определённого файла.

[Текст внутри квадратных скобок] указывает необязательный аргумент, который вы можете
	 предоставить.

Жирный текст обозначает новый или важный термин.

Замечания и предостережения появляются в следующем виде:

	   [Замечание]
Замечание

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

      [Предостережение]
Предостережение

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

Любые ссылки на некий файл внутри исходного кода CPython будут показаны так:

path/to/file.py

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

Ctrl + Space

Клавиатурные команды и Быстрые клавиши и для формата macOS, и для Windows будут выдаваться так:

File > Other > Option

Обратная связь и ошибки

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

Мы всегда стремимся улучшать свои учебные материалы. Какой бы ни была причина, отправьте свой отзыв по следующей ссылке:
   realpython.com/cpython-internals/feedback

О команде Real Python

В Real Python вас обучат навыкам программирования реальных задач силами
   сообщества профессиональных Последователей Python (Питонистов) по всему миру.

Наш вебсайт realpython.com, был запущен в 2012 году и в настоящее время помогает
   более чем трём миллионам разработчиков Python каждый месяц книгами, руководствами по программированию и прочими ресурсами  глубокого обучения.

Вот как вы можете найти Real Python на просторах Веб:

realpython.com

@realpython on Twitter

The Real Python Newsletter

The Real Python Podcast

 Содержание

Предисловие

Вступление

Введение

Как пользоваться этой книгой

Премиальные материалы и учебные ресурсы

Глава 1. Начинаем работать с исходным кодом CPython

Что у нас в исходном коде?

Глава 2. Настройка вашей среды разработки

IDE или редактор?

Монтаж Visual Studio

Монтаж кода Visual Studio

Установка

Рекомендуемые для этой книги расширения

Применение Расширенной навигации и расширения кода

Настройка задачи и запуск файлов

Монтаж JetBrains CLion

Монтаж Vim

Выводы

Глава 3. Компиляция CPython

Компиляция CPython в macOS

Компиляция CPython в Linux

Установка персональной версии

Краткое руководство по Make

Цели Make CPython

Цели сборки

Цели тестирования

Цели очистки

Цели установки

Различные цели

Компиляция CPython в Windows

Установка необходимых зависимостей

Компиляция из вашей командной строки

Компиляция из Visual Studio

Проводимая профилем оптимизация

Выводы

Глава 4. Язык и грамматика Python

Почему CPython написан на C, а не на Python

Спецификация языка Python

Документация языка

Пример

Файл грамматики

Пример: оператор while

Генератор синтаксического разбора

Повторная генерация грамматики

Лексемы

Выводы

Глава 5. Конфигурация и входные данные

Состояние конфигурации

Предварительная инициализация конфигурации

Относящиеся к делу исходные файлы

Структура данных конфигурации времени исполнения

Монтаж конфигурации времени исполнения при помощи командной строки

Просмотр флагов времени исполнения

Сборка конфигурации

Сборка модуля из входных данных

Относящиеся к делу файлы исходного кода

Считывание файлов/ входных данных

Ввод строки из приглашения на ввод команд

Просмотр флагов времени исполнения

Ввод данных из файла сценария или стандартного ввода

Просмотр флагов времени исполнения

Выводы

Глава 6. Лексический и синтаксический разбор при помощи синтаксических деревьев

Выработка реального синтаксического дерева

CPython Parser-Tokenizer

Относящиеся к делу файлы

Ввод данных в синтаксический анализатор из файла

Поток синтаксического анализатора и механизма разбора лексем

Абстрактные синтаксические деревья

Относящиеся к делу файлы

Применение Instaviz для представления деревьев абстрактного синтаксиса

Компиляция деревьев абстрактного синтаксиса

Важные термины для запоминания

Пример: Добавление оператора сравнения почти-равенства

Выводы

Глава 7. Собственно компилятор

Относящиеся к делу исходные файлы

Важные термины

Создание экземпляра компилятора

Флаги фьючерсов и флаги компилятора

Флаги фьючерсов

Справка по флагам фьючерсов для 3.9

Флаги компилятора

Символьные таблицы

Относящиеся к делу исходные файлы

Структура данных символьной таблицы

Применение стандартного библиотечного модуля symtable

Реализация символьной таблицы

Ядро процесса компиляции

Доступ к компилятору из Python

API компилятора C

Инструкции

Тип инструкции

Инструкции безусловного перехода

Блоки базового кадра

Операции и аргументы

Сборка

Структура данных ассемблера

Алгоритм ассемблера поиска в глубину

API C ассемблера

Поиск в глубину

Создание объекта кода

Применение Instaviz для отображения объекта кода

Пример: Реализация оператора почти-равенства

Выводы

Глава 8. Цикл расчёта

Относящиеся к делу исходные файлы

Важные термины

Построение состояния потока

Тип состояния потока

Относящиеся к делу исходные файлы

Построение объектов кадров

Тип объекта кадра

Относящиеся к делу исходные файлы

API инициализации объекта кадра

Преобразование параметров ключевых слов в словарь

Преобразование позиционных аргументов в переменные

Упаковка позиционных аргументов в *args

Загрузка аргументов ключевых слов

Добавление пропущенных позиционных аргументов

Добавление пропущенных аргументов ключевых слов

Свёртывание замыканий

Создание генераторов, сопрограмм и асинхронных генераторов

Исполнение кадра

Отслеживание выполнения кадра

Стек значений

Пример операции байтового кода: BINARY_OR

Имитация стека значений

Действия стека

Пример: Добавление элемента в список

Выводы

Глава 9. Управление памятью

Выделение памяти в C

Статическое выделение памяти в C

Автоматическое выделение памяти в C

Динамическое выделение памяти в C

Проектирование системы управления памятью Python

Выделение областей

Распределители памяти

Распределитель памяти CPython

Относящиеся к делу исходные файлы

Важные термины

Блоки, пулы и арены

Арены

Пулы

Таблицы пулов

Блоки

API выделения блоков

Применение API отладки Python

Области распределителя памяти объекта и PyMem

Применение модуля tracemalloc

Область распределителя сырой памяти

Персональные распределители доменов

Персональные уборщики выделенной памяти

AddressSanitizer

MemorySanitizer

UndeрnedBehaviorSanitizer

Арена памяти PyArena

Относящиеся к делу файлы

Подсчёт ссылок

Создание переменных в Python

Инкрементальные ссылки

Понижаемые ссылки

Подсчёт ссылок в операциях байтового кода

Преимущества подсчёта ссылок CPython

Сборка мусора

Относящиеся к делу исходные файлы

Проектирование сборки мусора

Включённые в сборщик мусора типы контейнеров

Не отслеживаемые объекты и изменяемость

Алгоритм сборки мусора

Выводы

Глава 10. Параллельность и одновременность

Модели параллельности и одновременности

Структура процесса

Многопроцессный параллелизм

Ветвление процесса в POSIX

Многопроцессность в Windows

Пакет multiprocessing

Относящиеся к делу файлы

Порождение и ветвление процессов

Создание дочернего процесса

Конвейеризация данных дочернему процессу

Исполнение дочернего процесса

Обмен данными при помощи очередей и конвейеров

Очереди

Конвейеры

Семафоры

Разделяемые состояния между процессами

Пример приложения

Аннотация мультипроцессности

Многопоточность

GIL

Относящиеся к делу исходные файлы

Старт потоков в Python

Состояние потока

Потоки POSIX

Потоки Windows

Аннотация многопоточности

Асинхронное программирование

Генераторы

Структура генератора

Относящиеся к делу исходные файлы

Создание генераторов

Выполнение генераторов

Сопрограммы

Относящиеся к делу исходные файлы

Циклы событий

Пример

Асинхронные генераторы

Подчинённые интерпретаторы

Относящиеся к делу исходные файлы

Пример

Выводы

Глава 11. Объекты и типы

Примеры в этой главе

Встроенные типы

Объекты и типы переменных объектов

Тип type

Слоты типов

Работа с типами в C

Словари свойств типов

Типы bool и long

Тип long

Пример

Тип строки Unicode

Относящиеся к делу исходные файлы

Обработка позиций кода Unicode

Сопоставление UTF-8 и UTF-16

Совместимость с ASCII

Тип широкого символа

Маркеры порядка байт

Пакет encodings

Модуль Codecs

Реализации Codec

Внутренние Codecs

Пример

Тип словаря

Хэширование

Относящиеся к делу исходные файлы

Структура словаря

Нахождение

Выводы

Глава 12. Стандартная библиотека

Модули Python

Python и модули C

Глава 13. Комплект тестов

Выполнение комплекта тестов в Windows

Выполнение комплекта тестов в Linux или macOS

Флаги теста

Исполнение особых тестов

Модули тестирования

Утилиты тестирования

Выводы

Глава 14. Отладка

Применение обработчика крушений

Компиляция поддержки отладки

Windows

macOS или Linux

Применение LLDB для macOS

Создание точек прерывания

Запуск CPython

Подключение к исполняемому интерпретатору CPython

Обработка точки прерывания

Применение расширения cpython_lldb

Применение GDB

Создание точек прерывания

Запуск CPython

Подключение к исполняемому интерпретатору CPython

Обработка точки прерывания

Применение расширения python-gdb

Применение отладчика Visual Studio

Создание точек прерывания

Запуск отладчика

Обработка точки прерывания

Применение отладчика CLion

Отладка приложения Make

Подключение отладчика

Создание точек прерывания

Выводы

Глава 15. Эталонное тестирование, профилирование и трассировка

Использование timeit для микро- эталонного тестирования

Пример timeit

Применение Комплекта эталонного тестирования Python для эталонного тестирования времени выполнения

Исполнение эталонного тестирования

Сопоставление эталонного тестирования

Профилирование кода Python при помощи cProfile

Экспорт профилей

Профилирование кода C при помощи DTrace

Относящиеся к делу исходные файлы

Установка DTrace

Компиляция поддержки DTrace

Применение DTrace из Clion

Пример DTrace

Выводы

Глава 16. Последующие шаги

Написание расширений C для CPython

Улучшение ваших приложений Python

Вклад в проект CPython

Сортировка проблем

Возбуждение пула запроса на исправление проблемы

Прочий вклад

Продолжайте обучаться

Еженедельные советы для разработчиков Real Python

Библиотека видеокурсов Python

Дополнение A. Введение в C для программистов Python

Препроцессор C

#include

#define

#undef

#if

#pragma

#error

Базовый синтаксис C

В целом

Предложения if

Предложения switch

Циклы

Функции

Указатели

Строки

Структуры

Выводы

  <	/dl>

Дополнение A. Введение в C для программистов Python

Содержание

Дополнение A. Введение в C для программистов Python

Препроцессор C

#include

#define

#undef

#if

#pragma

#error

Базовый синтаксис C

В целом

Предложения if

Предложения switch

Циклы

Функции

Указатели

Строки

Структуры

Выводы

Данное введение имеет целью ознакомить опытного программиста Python с основами языка программирования C и его применением в исходном коде CPython. Предполагается, что у вас уже имеются средние навыки синтаксиса Python.

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

Одним из первых отличительных моментов Python от C является препроцессор C. Давайте вначале рассмотрим именно его.

Препроцессор C

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

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

Основная цель такого препроцессора состоит в выполнении подстановок текста в его исходном файле, но он также выполняет и некий код базового условия при помощи #if или аналогичных предложений.

Давайте начнём с наиболее часто применяемой директивы препроцессора, #include.

  #include

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

Например, если вы рассматриваете свой файл Modules/_multiprocessing/semaphore.c, то в пределах его вершины вы увидите такую строку:

#include "multiprocessing.h"

Она сообщает своему препроцессору вытащить всё содержимое multiprocessing.h и поместить его в получаемый на выходе файл в этом месте.

Вы отметите две различные формы для такого предложения #include. Одна из них использует кавычки ("") для определения названия вкладываемого файла, а другая применяет угловые скобки (<>). Основное отличие состоит в том, в каком пути выполняется поиск такого файла в файловой системе.

Если вы применяете для названия файла <>, тогда препроцессор будет искать только файлы вложения системы. Применение же кавычек вокруг имён файлов вместо этого заставит ваш препроцессор вначале просматривать локальный каталог, а затем возвращаться обратно к каталогам системы.

  #define

#define позволяет вам выполнять простую подстановку текста, а также выступать в директивах #if, с которыми вы познакомитесь далее.

По самой основе, #define позволяет вам определять некий новый символ, который будет замещаться текстовой строкой в выводе препроцессора.

Продолжая с semphore.c, вы обнаруживаете такую строку:

#define SEM_FAILED NULL

Это предписывает препроцессору заменять всякое вхождение SEM_FAILED ниже данной точки литеральной строкой NULL перед тем, как этот код отправляется в его компилятор.

Элементы #define могут также получать параметры, например, специфичную для Windows версию SEM_CREATE:

#define SEM_CREATE(name, val, max) CreateSemaphore(NULL, val, max, NULL)

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

Например, в строке 460 semphore.c, макро SEM_CREATE применяется следующим образом:

handle = SEM_CREATE(name, value, max);

При компиляции в Windows этот макро будет расширен с тем, чтобы данная строка выглядела так:

handle = CreateSemaphore(NULL, value, max, NULL);

В последующих разделах вы увидите как такой макро определяется по разному в Windows и прочих операционных системах.

  #undef

Эта директива уничтожает все предыдущие определения препроцессора из #define. Это позволяет иметь воздействие #define только на часть файла.

  #if

Препроцессор также допускает условные предложения, позволяя вам либо включать, либо исключать разделы текста на основе определённых условий. Условные предложения закрываются при помощи директивы #endif и вы также можете применять #elif и #else для более тонких регулировок.

Существуют три формы #if, которые вы можете наблюдать в исходном коде CPython:

#ifdef <macro>: содержит идущий следом блок текста, когда предписанный макро определён. Вы можете обнаружить также написание #if defined(<macro>).

#ifndef <macro> содержит следующим далее блок текста, когда заданный макро не определён.

#if <macro> содержит далее блок текста, если его макро определён и он вычисляется как True.

Обратите внимание на применение слова "текст" вместо "код" при описании того, что включается в соответствующий файл или исключается из него. Препроцессор абсолютно ничего не знает о синтаксисе C и не заботится о том, что предписывает текст.

  #pragma

Прагмы это инструкции или советы компилятору. Обычно вы можете игнорировать их при чтении своего кода, поскольку они, как правило, относятся к тому, как этот код компилируется, а не к тому как этот код исполняется.

  #error

Наконец, #error отображает некое сообщение и вызывает останов исполнения препроцессора. И снова, вы безопасно можете игнорировать их при чтении своего исходного кода CPython.

Базовый синтаксис C

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

  В целом

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

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

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

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

Давайте рассмотрим некие примеры:

/* Comments are included between slash-asterisk and asterisk-slash */
/* This style of comment can span several lines -
   so this part is still a comment. */

// Comments can also come after two slashes
// This type of comment only goes until the end of the line, so new
// lines must start with double slashes (//).

int x = 0; // declares x to be of type 'int' and initializes it to 0

if (x == 0) {
    // This is a block of code
    int y = 1;  // y is only a valid variable name until the closing }
    // More statements here
    printf("x is %d y is %d\n", x, y);
}

// Single-line blocks do not require curly brackets
if (x == 13)
    printf("x is 13!\n");
printf("past the if block\n");

Обычно вы будете наблюдать, что код CPython очень ясно форматирован и, как правило, придерживается единого стиля внутри определённого модуля.

  Предложения if

В C if работает в целом так же как это происходит и в Python. Когда его условие истинно, тогда исполняется следующий блок. Синтаксис else и elseif должен быть достаточно знаком программистам Python. Обратите внимание н то, что предложения if C не нуждаются в неком endif по той причине, что блоки ограничены {}.

Для предложений if … else в C имеется сокращённая форма, именуемая тернарным (троичным) оператором:

condition ? true_result : false_result

Вы можете обнаружить его в semaphore.c, где, для Windows, он определяет макро под SEM_CLOSE():

#define SEM_CLOSE(sem) (CloseHandle(sem) ? 0 : -1)

Возвращаемым значением данного макро будет 0 если функция CloseHandle() возвращает true и -1 в противном случае.

[Замечание]
Замечание

Булевы типы переменных поддерживаются и применяются в частях исходного кода CPython, однако они не являются частью оригинального языка программирования. C интерпретирует бинарные условия применяя простое правило: 0 или NULL это ложь, всё остальное истина.

  Предложения switch

В отличии от Python, C также поддерживает switch. Применение switch может рассматриваться как сокращение для слишком длинных цепочек if … elseif. Вот пример из semaphore.c:

switch (WaitForSingleObjectEx(handle, 0, FALSE)) {
case WAIT_OBJECT_0:
    if (!ReleaseSemaphore(handle, 1, &previous))
        return MP_STANDARD_ERROR;
    *value = previous + 1;
    return 0;
case WAIT_TIMEOUT:
    *value = 0;
    return 0;
default:
    return MP_STANDARD_ERROR;
}

Это выполняет переключение по возвращаемому из WaitForSingleObjectEx() значению. Когда этим значением является WaitForSingleObjectEx(), выполняется первый блок. Значение WAIT_TIMEOUT в результате приводит ко второму блоку, всё остальное помечается блоком default.

Обратите внимание на то, что подлежащие проверке значения, в данном случае это возвращаемые из WaitForSingleObjectEx() значения, обязано быть либо целым значением, либо неким перечислимым типом, а каждый case должен быть неким значением константы.

  Циклы

В C существует три структуры циклов:

Циклы for

Циклы while

Циклы do … while

Давайте рассмотрим каждый из них по очереди.

Цикл for обладает синтаксисом, достаточно отличающимся от Python:

for ( <initialization>; <condition>; <increment>) {
    <code to be looped over>
}

Дополнительно к подлежащему для исполнения в самом циле коду, существуют также три блока кода для управления циклом for:

Раздел <initialization> исполняется в точности один раз при запуске самого цикла. Он обычно используется дл установки счётчика цикла в его начальное значение (а возможно и для определения этого счётчика цикла).

Код <increment> выполняется непосредственно после каждого прохода основного блока данного цикла. Традиционно, он увеличивает на единицу значение счётчика цикла.

Наконец, <condition> запускается после <increment> Возвращаемое значение этого кода будет вычисляться и прервёт данный цикл когда такое условие возвращает ложь.

Вот некий образец из Modules/sha512module.c:

for (i = 0; i < 8; ++i) {
    S[i] = sha_info->digest[i];
}

Этот цикл выполнится 8 раз с i, возрастающим от 0 до 7 и он прекратится при проверке условия и i равно 8.

Цикл while почти идентичен своему противопоставлению из Python. Тем не менее, синтаксис do … while слегка отличается. Само условие в цикле do … while не проверяется до тех пор, пока само тело в этом цикле не исполнится для самого первого раза.

В кодовой основе CPython присутствует множество экземпляров циклов for и циклов while, однако do … while не применяется.

  Функции

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

<return_type> function_name(<parameters>) {
    <function_body>
}

Возвращаемый тип может быть любым допустимым в C типом, включая такие встроенные типы как int и double, а также настраиваемые типы подобные PyObject, как в данном примере из semaphore.c:

static PyObject *
semlock_release(SemLockObject *self, PyObject *args)
{
 <statements of function body here>
}

Здесь в действии вы наблюдает пару специфичных для C функциональных возможностей. Пржде всего, помните, что пробельные символы не играют значения. Бо́льшая часть исходного кода CPython помещает возвращаемый тип функции в строке над всем остальным определением функции. Это часть PyObject *. Вы подробнее рассмотрите применение * слегка позднее, на данный момент важно понимать что имеется ряд модификаторов, которые вы можете помещать в функциях и переменных.

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

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

Список параметров для функций является списком разделяемых запятой переменных, аналогичный тому, который мы применяем в Python. И опять, C требует определённого типа для каждого параметра, а потому SemLockObject *self сообщает, что наш первый параметр это указатель на SemLockObject с названием self. Обратите внимание на то, что все параметры в C являются позиционными.

Давайте рассмотрим что означает часть этого предложения "указатель".

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

Такие адреса носят название указателей и они обладают типом, а потому int * это указатель на некое целое значение и оно имеет иной тип чем double *, который выступает указатель на число с плавающей запятой двойной точности.

  Указатели

Как уже было упомянуто выше, указатели это переменные, которые удерживают значение адреса некого значения. Это часто применяется в C, что видно по такому примеру:

static PyObject *
semlock_release(SemLockObject *self, PyObject *args)
{
 <statements of function body here>
}

Здесь параметр self будет содержать значение адреса или, некий указатель на значение SemLockObject. Кроме того, обратите внимание на то, что данная функция возвратит некий указатель на значение PyObject.

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

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

  Строки

C не обладает типом строк. Существует некое соглашение относительно множества написанных функций стандартной библиотеки, но в реальности нет такого типа. Точнее, строки в C хранятся как массивы значений char (для ASCII) или как wchar (для Unicode), причём каждое содержит единственны символ. Строки помечаются терминатором null, который обладает значением 0 и обычно отображается в коде как \0.

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

По той причине, что строки это всего лишь массивы значений, они не могут напрямую копироваться или сравниваться. Стандартная библиотека обладает функциями strcpy() и strcmp() (и их кузинами wchar) для выполнения таких операций и прочего.

  Структуры

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

struct <struct_name> {
    <type> <member_name>;
    <type> <member_name>;
    ...
};

Этот частично воспроизведённый пример из Modules/arraymodule.c отображает объявление некой struct:

struct arraydescr {
    char typecode;
    int itemsize;
    ...
};

Оно создаёт некий новый тип данных с названием arraydescr, который имеет большое число участников, причём первыми двумя из них выступают char typecode и int itemsize.

Зачастую структуры будут использоваться как часть некого typedef, который предоставляет простой псевдоним для её именования. В приведённом выше примере все переменные в таком новом типе должны объявляться с полным названием struct arraydescr x;.

Зачастую вы обнаружите такой синтаксис:

typedef struct {
    PyObject_HEAD
    SEM_HANDLE handle;
    unsigned long last_tid;
    int count;
    int maxvalue;
    int kind;
    char *name;
} SemLockObject;

Он создаёт новый, настраиваемый тип структуры и снабжает его названием SemLockObject. Для объявления некой переменной этого типа вы можете просто воспользоваться его псевдонимом SemLockObject x;.

Выводы

На этом мы сворачиваем ваше краткое знакомство с синтаксисом C. Хотя данное описание лишь поверхностно касается описания самого языка программирования C, теперь вы обладаете достаточными сведениями для чтения и понимания самого исходного кода CPython.

Глава 1. Начинаем работать с исходным кодом CPython

Содержание

Глава 1. Начинаем работать с исходным кодом CPython

Что у нас в исходном коде?

Когда вы набираете python в своей консоли или устанавливаете дистрибутив Pytgon с Python.org вы запускаете CPython. CPython это одна из множества реализаций Python сопровождаемая и пишущаяся различными командами разработчиков. Некоторыми из альтернатив, о которых вы могли слышать, это PyPy, Cython и Jython.

Уникальность CPython состоит в том, что он содержит как среду выполнения, так и спецификацию совместно используемого языка программирования, которую применяют все прочие реализации Python. CPython это официальная или справочная реализация Python.

Собственно Спецификация языка Python это тот документ, который описывает язык Python. Например, она декларирует, что assert это зарезервированное ключевой слово и что [] применяются для индексации, расщепления и создания пустых списков.

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

После того как вы набираете python без некого файла или модуля, это снабжает вас интерактивным приглашением на ввод (REPL).

Вы можете импортировать встроенные модули, такие как json, csv и collections из имеющейся стандартной библиотеки.

Вы можете устанавливать пакеты из Интернета при помощи pip.

Вы можете тестировать свои приложения при помощи встроенной библиотеки unittest.

Всё это части вашего дистрибутива CPython. Он содержит намного больше чем просто компилятор.

В этой книге мы исследуем различные части дистрибутива CPython:

Спецификацию самого языка программирования

Его компилятор

Модули его стандартной библиотеки

Типы его ядра

Его комплект тестирования

Что у нас в исходном коде?

Исходный код дистрибутива CPython поставляется совместно с целым диапазоном инструментов, библиотек и компонентов, которые вы исследуете в этой книге.

[Замечание]
Замечание

Эта книга нацелена на версию 3.9 исходного кода CPython.

Чтобы выгрузить копию исходного кода CPython, вы можете воспользоваться git чтобы вытянуть самую последнюю версию:

$ git clone --branch 3.9 https://github.com/python/cpython
$ cd cpython

Все примеры в данной книге основываются на Python версии 3.9.

[Совет]
Совет

Важно переключиться на ветку 3.9. Основная ветка меняется ежечасно. Многие примеры и упражнения из данной книги вряд ли подойдут для главной ветки.

[Замечание]
Замечание

Если у вас не доступен Git, вы можете установить его с git-scm.com. В качестве альтернативы вы можете выгрузить файл ZIP исходного кода CPython напрямую с вебсайта GitHub.

Если вы выгрузили этот исходный код в виде файла ZIP, тогда он не будет содержать никаких истории, тегов или ветвей.

Внутри только что выгруженного каталога cpython вы обнаружите следующие подкаталоги:

Рисунок 1-1

Далее вы настроите свою среду разработки.

Глава 2. Настройка вашей среды разработки

Содержание

Глава 2. Настройка вашей среды разработки

IDE или редактор?

Монтаж Visual Studio

Монтаж кода Visual Studio

Установка

Рекомендуемые для этой книги расширения

Применение Расширенной навигации и расширения кода

Настройка задачи и запуск файлов

Монтаж JetBrains CLion

Монтаж Vim

Выводы

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

Исходный код CPython это около 6 процентов Python (из которых основную часть составляет его тестирование) и 24 процента C. Остаток это смесь прочих языков программирования.

IDE или редактор?

Если вы ещё пока не определились с тем какую среду разработки применять, имеется одно решений, которое вам следует вначале принять: будете ли вы применять встроенную среду разработки (IDE, integrated development environment) или редактор кода.

IDE имеет целью некий конкретный язык программирования и цепочки инструментов. Большинство IDE обладают встроенными тестированием, проверкой синтаксиса, контролем версий и компиляцией.

Редактор кода позволяет вам изменять файлы кода, причём вне зависимости от языка программирования. Большинство редакторов кода это простые текстовые редакторы с выделением синтаксиса.

По причине своей природы полной укомплектованности, IDE часто потребляют больше аппаратных ресурсов. Поэтому, если вы обладаете ограниченной оперативной памятью (менее 8ГБ), тогда рекомендуется кодовый редактор.

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

Бесплатными или за некую цену имеются доступными сотни редакторов и IDE. Вот некоторые из обычно применяемых IDE и редакторов, приспособленных под разработку CPython:

Таблица 2-1. Подходящие для разработки CPython IDE и редакторы

Приложение
Стиль
Поддержка

Microsoft Visual Studio Code

Редактор

Windows, macOS и Linux

Atom

Редактор

Windows, macOS и Linux

Sublime Text

Редактор

Windows, macOS и Linux

Vim

Редактор

Windows, macOS и Linux

Emacs

Редактор

Windows, macOS и Linux

Microsoft Visual Studio

IDE (C, Python и прочие)

Windows

PyCharm by JetBrains

IDE (C, Python и прочие)

Windows, macOS и Linux

CLion by JetBrains

IDE (C, Python и прочие)

Windows, macOS и Linux

Некая версия Microsoft Visual Studio также доступна для Mac, однако она не поддерживает Python Tools для Visual Studio или компиляции C.

В последующих разделах мы исследуем этапы настройки для следующих редакторов и IDE:

Microsoft Visual Studio

Microsoft Visual Studio Code

JetBrains CLion

Vim

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

Монтаж Visual Studio

Самая новая версия Visual Studio, Visual Studio 2019, обладает встроенной поддержкой кода Python и C в Windows. Я рекомендую пользоваться им для всех примеров и упражнений из этой книги. Если вы уже обладаете установленным Visual Studio 2017, тогда это также должно работать.

[Замечание]
Замечание

Никакие платные функциональные возможности Visual Studio не требуются для компиляции CPython или завершения этой книги. Вы можете пользоваться бесплатной Редакцией Сообщества.

Тем не менее, руководимая профилем оптимизация сборки профиля требует Профессиональной редакции или более высокой.

Visual Studio бесплатно доступен с вебсайта Microsoft’s Visual Studio.

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

Рабочий поток Python development

Необязательные Python native development tools

64- битный Python 3 (3.7.2)

Если у вас уже имеется установленным Python 3.7, вы можете отменить выбор Python 3 64-bit (3.7.2). Если вы желаете сберечь дисковое пространство, вы также можете отменить выбор любых прочих необязательных функциональных возможностей.

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

После того как установка выполнена, кликните Launch чтобы запустить Visual Studio. Вы получите приглашение на то чтобы подписать его. Если у вас имеется учётная запись Microsoft, вы также сможете зарегистрироваться с ней и пропустить этот шаг.

Затем вы получите приглашение на открытие некого проекта. Вы можете клонировать репозиторий Git CPython напрямую из Visual Studio, выбрав параметр Clone or check out code.

Для местоположения необходимого репозитория введите https://github.com/python/cpython, выберите свой локальный путь и нажмите Clone.

Затем Visual Studio выгрузит некую копию CPython из GitHubпри помощи встроенного в Visual Studio Git. Этот шаг также убережёт вас от преграды в необходимости установки в Windows Git. Эта выгрузка может отнять до 10 минут.

[Совет]
Совет

Visual Studio автоматически проверит наличие главной ветви. Перед компиляцией убедитесь что вы изменили её на ветвь 3.9 с той, которая настроена окном Team Explorer. Переключение на ветвь 3.9 является важным шагом. Многие из примеров и упражнений этой книги вероятно не сработают в главной ветви.

После выгрузки нашего проекта, вам потребуется указать Visual Studio на файл решения PCBuild/pcbuild.sln, кликнув по Projects > pcbuild.sln:

Рисунок 2-1

Теперь, когда вы обладаете настроенным Visual Studio и выгруженным исходным кодом, вы можете компилировать CPython для Windows, выполняя последовательность шагов следующей главы.

Монтаж кода Visual Studio

Microsoft Visual Studio Code это расширяемый редактор кода с Интернет магазином подключаемых модулей.

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

  Установка

Visual Studio Code, порой именуемый как VS Code, доступен с неким простым установщиком в code.visualstudio.com.

Сразу после установки VS Code обладает всеми необходимыми возможностями редактирования кода, однако он станет намного более мощным после установки вами расширений.

Вы можете получить доступ к панели Расширений, выбрав из своего верхнего меню View > Extensions:

Рисунок 2-2

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

  Рекомендуемые для этой книги расширения

Для работы с CPython имеется несколько полезных расширений:

C/C++ (ms-vscode.cpptools) предоставляет поддержку для C/C++, включая IntelliSense, отладку и выделение кода.

Python (ms-python.python) снабжает богатой поддержкой для редактирования, отладки и чтения кода Python.

reStructuredText (lextudio.restructuredtext) снабжает богатой поддержкой для reStructuredText, того формата который используется в документации Python.

Task Explorer (spmeesseman.vscode-taskexplorer) добавляет панель Проводника задач вовнутрь вкладки Explorer, что упрощает запуск задач make.

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

Многим из применяемых в данной книге задач требуется командная строка. Вы можете добавить в VS Code встроенный терминал, выбрав Terminal > New Terminal.. Под вашим редактором кода появится терминал:

Рисунок 2-3

  Применение Расширенной навигации и расширения кода

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

Например, если вы кликните правой кнопкой по некому вызову функции в C и выберете Go to References, VS Code обнаружит прочие ссылки на эту функцию в вашем базовом коде:

Рисунок 2-4

Go to References очень полезен для выявления надлежащего вида вызова некой функции.

Если вы кликните или встанете поверх некого макро C, тогда ваш редактор раскроет этот макро в уже откомпилированный код:

Рисунок 2-5

Чтобы выполнить переход к соответствующему определению функции, встаньте над любым её вызовом и нажмите Cmd + Click в macOS или Ctrl + Click в Linux и Windows.

  Настройка задачи и запуск файлов

В своём рабочем каталоге VS Code пользуется папкой .vscode. Если такой папки нет, создайте её сейчас. Внутри этой папки вы можете создавать такие файлы:

tasks.json для закладок на команды, которые выполняют данный проект

launch.json для настройки своего отладчика (см. Главу 14, Отладка)

Прочие относящиеся к встраиваемым модулям файлы.

Внутри своего каталога .vscode создайте файл tasks.json, если он ещё не существует. Такой файл tasks.json будет запускать вас:

cpython-book-samples/11/tasks.json

Рисунок 2-6

При помощи подключаемого модуля Проводника задач вы обнаружите некий список настроенных задач внутри своей группы vscode:

Рисунок 2-7

В нашей следующей главе вы изучите дополнительные сведения о процессе сборки для компиляции CPython.

Монтаж JetBrains CLion

JetBrains делает некий IDE для Python с названием C/C++, а также IDE для разработки C/C++ с названием CLion.

CPython обладает и кодом C, и кодом Python. Вы можете установить поддержку C/C++ для C/C++, однако CLion поставляется снабжённым поддержкой Python.

[Совет]
Совет

Поддержка Makefile доступна только в версиях 2020.2 CLion и выше.

[Совет]
Совет

Этот этап требует чтобы вы обладали как сгенерированным makefile посредством исполнения configure, так и скомпилированным CPython.

Прочтите, пожалуйста, Главу 3, Компиляция CPython для своей операционной системы и затем вернитесь к этой главе.

После компиляции CPython в самый первый раз, вы получите в корне своего каталога исходного кода makefile.

Откройте CLion и выберите Open or Import из приветственного окна. Переместитесь в каталог своего исходного кода, выберите необходимый makefile и нажмите Open:

Рисунок 2-8

CLion запросит у вас желаете ли вы открыть этот каталог и импортировать соответствующий makefile в качестве некого нового проекта. Выберите Open as Project для импорта в качестве проекта.

CLion запросит какую цель make исполнить перед импортом. Оставьте параметр по умолчанию, clean, и продолжите:

Рисунок 2-9

Затем проверьте что вы можете собрать свой исполняемый CPython из CLion. Из верхнего меню выберите Build Build Project.

В планке состояния вы должны обнаружить индикатор хода исполнения для собираемого проекта:

Рисунок 2-10

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

Выберите Run " Edit Confgurations чтобы открыть окно конфигурации Run/ Debug. Внутри этого окна выберите + Makefle " Application и осуществите следующие шаги:

Установите Name в cpython.

Оставьте целью сборки all.

Для своего исполняемого выберите ниспадающее меню и укажите Select Other, затем отыщите свой скомпилированный исполняемый файл CPython в каталоге исходного кода. Он будет иметь название python или python.exe.

Введите все параметры программы, которые вы всегда хотите получать, например, -X dev для включения режима разработки. Эти флаги позднее мы обсудим в Монтаж конфигурации времени исполнения при помощи командной строки.

Установите рабочий каталог для макро $ProjectFileDir$ CLion:

Рисунок 2-11

Кликните OK для добавления этой конфигурации. Вы можете повторить эти шаги столько раз сколько пожелаете для всех своих целей make CPython. Для получения всех справочных сведений обратитесь к разделу Цели Make CPython из главы Главы 3, Компиляция CPython.

В верхнем правом окне CLion теперь доступна конфигурация сборки CPython:

Рисунок 2-12

Чтобы протестировать это кликните по иконке стрелки или выберите Run > Run ’cpython’ в своём верхнем меню. Вы теперь должны обнаружить соответствующий REPL внизу окна CLion:

Рисунок 2-13

Отлично! Теперь вы способны вносить изменения и быстро испытывать их, кликая по Build и Run. Если вы поместите некую точку прерывания в своём коде C, тогда make гарантирует вам выбор Debug вместо Run.

Внутри своего редактора кода соответствующие Быстрые клавиши Cmd + Click в macOS и Ctrl + Click в Windows и Linux переведут вас в функциональные возможности навигации по редактору:

Рисунок 2-14

Монтаж Vim

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

[Замечание]
Замечание

В большинстве дистрибутивов Linux, а также внутри терминала macOS vi это некий псевдоним для vim. В этой книге мы будем применять команду vim, однако если у вас имеется соответствующий псевдонимvi, он также будет работать.

Сразу после своей установки Vim обладает лишь базовой функциональностью, слегка превышающей функциональность подобного Notepad редактора. При некоторых настройках и расширениях, тем не менее, Vim способен стать более мощным инструментом как для редактирования Python, так и для C.

Расширения Vim пребывают в разных местах, включая GitHub. Для упрощения настроек и установки подключаемых модулей из GitHub вы можете установить диспетчер подключаемых модулей, например, Vundle.

Для установки Vundle выполните в своём терминале следующую команду:

$ git clone https://github.com/VundleVim/Vundle.vim.git \
  ~/.vim/bundle/Vundle.vim

После того как Vundle выгружен, вам потребуется настроить Vim для загрузки его механизма Vundle.

Вы установите два подключаемых модуля:

Fugitive: Полосу состояния для Git с

закладками для множества задач Git.

Fugitive: Панель для упрощения перехода к функциям, методам и классам.

Для установки этих подключаемых модуле, вначале измените содержимое своего файла настроек Vim (обычно HOME/.vimrc) чтобы он содержал следующие строки:

cpython-book-samples/11/.vimrc

syntax on
set nocompatible              " be iMproved, required
filetype off                  " required

" set the runtime path to include Vundle and initialize
set rtp+=~/.vim/bundle/Vundle.vim
call vundle#begin()

" let Vundle manage Vundle, required
Plugin 'VundleVim/Vundle.vim'

" The following are examples of different formats supported.
" Keep Plugin commands between vundle#begin/end.
" plugin on GitHub repo
Plugin 'tpope/vim-fugitive'
Plugin 'majutsushi/tagbar'
" All of your Plugins must be added before this line
call vundle#end()            " required
filetype plugin indent on    " required
" Open tagbar automatically in C files, optional
autocmd FileType c call tagbar#autoopen(0)
" Open tagbar automatically in Python files, optional
autocmd FileType python call tagbar#autoopen(0)
" Show status bar, optional
set laststatus=2
" Set status as git status (branch), optional
set statusline=%{FugitiveStatusline()}

Для выгрузки и установки этих подключаемых модулей запустите такую команду:

$ vim +PluginInstall +qall

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

При изменении и изучении своего исходного кода CPython вы пожелаете выполнять быстрый переход между методами, функциями и макросами. Обычный текстовый поиск не достигает некого вызова функции или её определения из самой реализации. Однако вы можете воспользоваться приложением с названием ctags для индексации исходных файлов по множеству языков программирования в некую базу данных обычного текстового содержимого.

Для индексации заголовков для всех файлов C и Python в стандартной библиотеке исполните такой код:

$./configure
$ make tags

Теперь откройте в Vim файл Python/ceval.c:

$ vim Python/ceval.c

Вы обнаружите значение состояния Git в самом низу, а также функции, макросы и переменные в панели справа:

Рисунок 2-15

Затем откройте файл Python, например, Lib/subprocess.py:

$ vim Lib/subprocess.py

Полоска тегов отобразит ваши импорты, классы, методы и функции:

Рисунок 2-16

При помощи Vim вы можете переключаться между окнами применяя Ctrl + W, переходить в правую панель используя L и действовать клавишами стрелок для перемещения вверх и вниз между всеми помеченными функциями.

Нажмите Enter чтобы скакнуть в реализацию любой функции. Чтобы вернуться обратно в свою панель редактирования нажмите Ctrl + W и затем H.

[Замечание]
Замечание

Посетите VIM Adventures чтобы ознакомиться с командами Vim и запомнить их.

Выводы

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

Отладка - критически важная функциональность для производительности, а потому наличие надёжного отладчика, который вы могли бы применять для изучения среды исполнения и выявления ошибок, сэкономит вам много времени. Если в Python вы привыкли к отладке при помощи print(),, тогда важно отметить, что такой подход не работает с C. Отладка будет рассмотрена позднее в данной книге.

Глава 14. Отладка

Содержание

Глава 14. Отладка

Применение обработчика крушений

Компиляция поддержки отладки

Windows

macOS или Linux

Применение LLDB для macOS

Создание точек прерывания

Запуск CPython

Подключение к исполняемому интерпретатору CPython

Обработка точки прерывания

Применение расширения cpython_lldb

Применение GDB

Создание точек прерывания

Запуск CPython

Подключение к исполняемому интерпретатору CPython

Обработка точки прерывания

Применение расширения python-gdb

Применение отладчика Visual Studio

Создание точек прерывания

Запуск отладчика

Обработка точки прерывания

Применение отладчика CLion

Отладка приложения Make

Подключение отладчика

Создание точек прерывания

Выводы

CPython поставляется со встроенным отладчиком, pdb, для отладки приложений Python. Этот отладчик pdb исключителен при отладке крушений внутри некого приложения Python, а также для написания тестов и инспектирования локальных переменных.

Когда же дело касается CPython, однако, вам требуется второй отладчик, тот, который понимает C.

В этой главе вы изучите как:

Подключать отладчик к своему интерпретатору CPython

Применять этот отладчик чтобы заглянуть вовнутрь выполнения процесса CPython

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

В этой главе рассматриваются следующие отладчики:

Таблица 14-1. Рассматриваемые отладчики

Отладчик
Тип
Платформа

LLDB

консольный

macOS

GDB

консольный

Linux

Отладчик Visual Studio

наглядный

Windows

Отладчик CLion

наглядный

Windows, macOS, Linux

Отладчик VS Code

наглядный

Windows, macOS, Linux

Применение обработчика крушений

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

Если CPython вызывает ошибку сегментации, тогда вы получите очень мало сведений о том что произошло:

[1]    63476 segmentation fault  ./python portscanner.py

CPython поставляется со встроенным обработчиком отказов. Если вы запускаете CPython с -X faulthandler или -X dev, тогда вместо выдачи на печать системного сообщения ошибки сегментации, имеющийся обработчик ошибок выведет на печать значение исполнявшегося потока и трассировку стека самого Python где произошла эта ошибка:

Fatal Python error: Segmentation fault
Thread 0x0000000119021dc0 (most recent call first):
  File "/cpython/Lib/threading.py", line 1039 in _wait_for_tstate_lock
  File "/cpython/Lib/threading.py", line 1023 in join
  File "/cpython/portscanner.py", line 26 in main
  File "/cpython/portscanner.py", line 32 in <module>
[1]    63540 segmentation fault  ./python -X dev portscanner.py

Эта функциальная возможность также полезна при разработке и тестированию расширения C CPython.

Компиляция поддержки отладки

Для получения существенных сведений из своего отладчика, вам следует компилировать необходимые символы отладки в CPython. Без таких символов, тот стек отладки, которй пребывает внутри сеанса отладки не будет содержать верных названий функций, имён переменных или названий файлов.

  Windows

Следуйте тем же самым шагам, которые вы предпринимали в разделе Компиляция CPython в Windows из Главы 3, Компиляция CPython, при этом обеспечьте компиляцию в конфигурации отладки для получения символов отладки:

> build.bat -p x64 -c Debug

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

  macOS или Linux

Соответствующие шаги из Главы 3, Компиляция CPython предписывают запуск сценария ./configure с флагом --with-pydebug. Если вы не включили этот флаг, тогда вернитесь обратно и снова запустите ./configure с первонаяальными параметрами и этим флагом --with-pydebug. Это произведёт правильный исполняемый файл и символы для отладки.

Применение LLDB для macOS

Отладчик LLDB поставляется с инструментами разработки Xcode, поэтому вы должны иметь их уже установленными.

Запустите LLDB и загрузите скомпилированный исполняемый файл CPython в качестве его цели:

$ lldb ./python.exe
(lldb) target create "./python.exe"
Current executable set to './python.exe' (x86_64).

Теперь вы будете обладать приглашением на ввод, в котором вы можете вводить некоторые команды для отладки.

  Создание точек прерывания

Для создания точки прерывания воспользуйтесь командой break set с необходимым файлом (относительно вашего корня) и значением номера строки:

lldb) break set --file Objects/floatobject.c --line 532
Breakpoint 1: where = python.exe`float_richcompare + 2276 at
    floatobject.c:532:26, address = 0x000000010006a974

[Замечание]
Замечание

Существует также условное обозначение для настройки точек прерывания: (lldb) b Objects/floatobject.c:532

Вы можете добавлять множество точек прерывания при помощи команды break set. Для просмотра перечня утановленных в данный момент точек прерывания воспользуйтесь командой break list:

(lldb) break list
Current breakpoints:
1: file = 'Objects/floatobject.c', line = 532, exact_match = 0, locations = 1
  1.1: where = python.exe`float_richcompare + 2276 at floatobject.c:532:26,
            address = python.exe[...], unresolved, hit count = 0

  Запуск CPython

Для запуска CPython воспользуйтесь командой process launch -- параметрами командной строки которые вы обычно применяете с Python.

Для запуска Python с некой строкой, например, python -c "print(1)" воспользуйтесь следующей командой:

(lldb) process launch -- -c "print(1)"

Для запуска Python с неким сценарием применяйте такую команду:

(lldb) process launch -- my_script.py

  Подключение к исполняемому интерпретатору CPython

Если вы уже обладаете запущенным интерпретатором CPython, тогда вы можете подключаться к нему.

Изнутри своего сеанса LLDB исполните process attach --pid с соответствующим идентификатором процесса:

(lldb) process attach --pid 123

Вы можете получить идентификатор процесса из монитора активности или воспользовавшись os.getpid() в Python.

Все настроенные до или после этой точки точки прерывания остановят этот процесс.

  Обработка точки прерывания

Чтобы посмотреть как обрабатываются точки прерывания, установите некую точку прерывания в функции float_richcompare() Objects/floatobject.c.

Затем запустите этот процесс и сравните два плавающих значения при помощи своего оператора почти равенства, который вы разрабатывали в предыдущих главах:

(lldb) process launch -- -c "1.0~=1.1"
Process 64421 launched: '/cpython/python.exe' (x86_64)
Process 64421 stopped
* thread #1, queue = '...', stop reason = breakpoint 1.1
    frame #0: 0x000000010006a974 python.exe`float_richcompare(v=1.0,
        w=1.1, op=6) at floatobject.c:532:26
   529          break;
   530      case Py_AlE: {
   531              double diff = fabs(i - j);
-> 532              const double rel_tol = 1e-9;
   533              const double abs_tol = 0.1;
   534              r = (((diff <= fabs(rel_tol * j)) ||
Target 0: (python.exe) stopped.

LLDB выдаст вам приглашение на ввод снова. Вы можете просмотреть локальные переменные при помощи команды v:

(lldb) v
(PyObject *) v = 0x000000010111b370 1.0
(PyObject *) w = 0x000000010111b340 1.1
(int) op = 6
(double) i = 1
(double) j = 1.1000000000000001
(int) r = 0
(double) diff = 0.10000000000000009
(const double) rel_tol = 2.1256294105914498E-314
(const double) abs_tol = 0

Вы можете вычислять выражение C при помощи команды expr с любой допустимой командой C. Могут применяться любые переменные из области действия. Например, чтобы вызвать fabs(rel_tol) и привести его к double выполните такую команду:

(lldb) expr (double)fabs(rel_tol)
(double) $1 = 2.1256294105914498E-314

Это выдаст на печать получаемые в результате переменные и назначать их некому идентификатору ($1). Вы можете повторно применять этот идентификатор в качестве временной переменной.

Вы можете также пожелать изучать экземпляры PyObject:

(lldb) expr v->ob_type->tp_name
(const char *) $6 = 0x000000010034fc26 "float"

Для отслеживания хода исполнения с этой точки прерывания применяйте команду bt:

(lldb) bt
* thread #1, queue = '...', stop reason = breakpoint 1.1
  * frame #0: ...
        python.exe`float_richcompare(...) at floatobject.c:532:26
    frame #1: ...
        python.exe`do_richcompare(...) at object.c:796:15
    frame #2: ...
        python.exe`PyObject_RichCompare(...) at object.c:846:21
    frame #3: ...
        python.exe`cmp_outcome(...) at ceval.c:4998:16

Чтобы вмешиваться, применяйте команду step или s.

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

Для продолжения исполнения употребляйте команду continue или c.

Для выхода из сеанса пользуйтесь командой quit или q.

[Совет]
Смотри также

LLDB Documentation Tutorial содержит наиболее исчерпывающий список команд.

  Применение расширения cpython_lldb

LLDB поддерживает расширения, написанные на Python. Имеется некое расширение открытого исходного кода, cpython_lldb, которая выводит на печать дополнительные сведения в вашем сеансе LLDB для естественных объектов CPython/

Для его установки запустите такие команды:

$ mkdir -p ~/.lldb
$ cd ~/.lldb && git clone https://github.com/malor/cpython-lldb
$ echo "command script import ~/.lldb/cpython-lldb/cpython_lldb.py" \
  >> ~/.lldbinit
$ chmod +x ~/.lldbinit

Теперь, всякий раз когда вы наблюдаете переменные в LLDB, вы также видите некоторые дополнительные сведения справа, такие как численное значение для целых чисел и чисел с плавающей запятой или текст для строк Unicode. Внутри консоли LLDB у вас также имеется некая дополнительная команда, py-bt, которая выводит на печать значение отслеживания стека для кадров Python.

Применение GDB

GDB это отладчик, обычно применяемый для приложений C/C++, написанных для платформ Linux. Он также очень популярен в команде разработчиков CPython.

При компиляции CPython он вырабатывает некий сценарий, python-gdb.py. Не исполняйте этот сценарий напрямую. Вместо этого GDB обнаружит и исполнит его автоматически после настройки.

Для настройки этого этапа отредактируйте файл .gdbinit внутри своего домашнего пути (~/.gdbinit) и добавьте приводимую ниже строку, в которой /path/to/checkout это путь к проверке git cpython:

add-auto-load-safe-path /path/to/checkout

Для запуска GDB запустите его с аргументом, указывающим на ваш скомпилированный исполняемый файл CPython:

$ gdb ./python

GDB загрузит все символы для скомпилированного исполняемого файла и предоставит вам приглашение командной строки. GDB обладает набором встроенных команд, а пакет расширений CPython некоторыми дополнительными командами.

  Создание точек прерывания

Для установки точки прерывания воспользуйтесь командой b <file>:<line> относительно пути этого исполняемого файла:

(gdb) b Objects/floatobject.c:532
Breakpoint 1 at 0x10006a974: file Objects/floatobject.c, line 532.

Вы можете устнаовить столько точек прерывания, сколько пожелаете.

  Запуск CPython

Для запуска конкретного процесса применяйте команду run с идущими далее аргументами для запуска интерпретатора Python.

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

(gdb) run -c "print(1)"

Чтобы запустить Python со сценарием употребляйте такую команду:

(gdb) run my_script.py

  Подключение к исполняемому интерпретатору CPython

Когда вы уже имеете запущенным интерпретатор CPython, вы можете присоединиться к нему.

Изнутри сеанса GDB запустите команду attach с идентификатором нужного процесса:

(gdb) attach 123

Вы можете получить значение идентификатора процесса из монитора активности или воспользовавшись os.getpid() в Python.

Все установленные до и после этой точки будут останавливать этот процесс.

  Обработка точки прерывания

Когда GDB попадает в эту точку прерывания, вы можете пользоваться командой print или p для вывода на печать переменной:

(gdb) p *(PyLongObject*)v
$1 = {ob_base = {ob_base = {ob_refcnt = 8, ob_type = ...}, ob_size = 1}, ob_digit = {42}}

Чтобы вмешиваться, применяйте команду step или s.

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

  Применение расширения python-gdb

Расширение python-gdb загрузит некоторый дополнительный набор команд в имеющуюся консоль GDB:

Таблица 14-2. Дополнительные команды python-gdb

Команда
Назначение

py-print

Отыскивает переменную Python и выводит её на печать

py-bt

Выводит её на печать отслеживание стека Python

py-locals

Выводит на печать результат locals()

py-up

Спускаемся вниз на один кадр Python

py-down

Поднимаемсяна один кадр Python вверх

py-list

Выводит её на печать исходный код Python для текущего кадра

Применение отладчика Visual Studio

Microsoft Visual Studio поступает в пакете с визуальным отладчиком. Этот отладчик является мощным и поддерживает визуализатор стека кадра, список отслеживаемого, а также возможность вычисления выражений.

Для его применения откройте Visual Studio и соответствующий файл решения PCBuild/pcbuild.sln.

  Создание точек прерывания

Для добавления новой точки прерывания, проследуйте в тот файл, в который вы желаете в своём окне решения, затем кликните в соответствующем внутреннем поле слева от номера вашей строки.

Это добавит красный кружок для указания того что вы установили в этой строке точку прерывания:

Рисунок 14-1

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

Рисунок 14-2

  Запуск отладчика

Из верхнего меню выберите Debug/Start Debugger или нажмите F5.

Visual Studio запустит новую среду исполнения Pythob и REPL.

  Обработка точки прерывания

Когда вы попадаете в некую точку прерывания вы можете пройти вперёд по предложениям применяя кнопки навигации или следующих горячих клавиш:

Для вмешательства: F11

Для продвижения: F10

Чтобы покинуть: Shift + F11

Снизу вы увидите стек вызова. Вы можете выбирать в этом стеке кадры для изменения навигации и инспектирования переменных в прочих кадрах:

Рисунок 14-3

В своём редакторе кода вы можете выделить любую переменную или выражение чтобы увидеть их значение. Вы также можете кликнуть правой кнопкой мыши и выбрать Add Watch. Это добавляет соответствующую переменную в окно просмотра, в котором вы сможете быстро увидеть значения переменных, которые требуются вам для отладки:

Рисунок 14-4

Применение отладчика CLion

IDE CLion оборудован мощным визуальным отладчиком. В macOS он работает с LLDB и с GDB в macOS, Windows и Linux.

Для настройки этого отладчика пройдите в Preferences и выберите Build, Execution, Deployment/Toolchains:

Рисунок 14-5

Имеется блок выбора для целевого отладчика. Выберите подходящие параметры для своей операционной системы:

macOS: Снабжён LLDB

Windows или Linux: Снабжаются GDB

[Замечание]
Важно

И параметры LLDB, и параметры GDB приносят выгоду из расширений cpython_lldb и python-gdb, соответственно. Прочтите разделы LLDB и GDB из этой главы для получения сведений относительно установки и включения этих расширений.

  Отладка приложения Make

Из CLion 2020.2 вы можете компилировать и отлаживать все основывающиеся на makefile проекты, в том числе и CPython.

Для запуска отладки выполните шаги из раздела Монтаж JetBrains CLion Главы 2, Настройка вашей среды разработки. После выполнения этих шагов вам придётся Make Application цели. Выберите Run/Debug из верхнего меню чтобы запустить необходимый процесс и приступить к отладке.

В качестве альтернативы вы можете подключить необходимый отладчик для исполнения процесса CPython.

  Подключение отладчика

Для подключения к исполняющему процесс CPython отладчику CLion выберите Run/Attach to Process.

Всплывёт список запущенных процессов. Отыщите процесс Python, к которому вы желаете присоединиться и выберите Attach. Начнётся сеанс отладки.

[Предостережение]
Важно

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

Он использует отладчик Python, не отладчик C:

Рисунок 14-6

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

  Создание точек прерывания

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

Рисунок 14-7

Кликните правой кнопкой для подключения условий:

Рисунок 14-8

Чтобы просматривать и управлять всеми текущими точками прерывания переместитесь из самого верхнего меню к Run/View Breakpoints :

Рисунок 14-9

Теперь вы можете включать и отключать точки прерывания, а также отключать их при попадании в иную точку прерывания.

  Обработка точек прерывания

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

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

Рисунок 14-10

Внутри прерывания вы можете вычислять выражения для получения дополнительных сведений о локальных переменных. Вы можете отыскать окно Evaliate в Run/Debugging Actions/Evaluate Expression или иконку закладки в окне отладки.

Внутри окна Evaluation вы можете набрать выражение и CLion наберёт далее названия свойств и типы:

Рисунок 14-11

Вы также можете преобразовывать выражения, что полезно для приведения PyObject* в реальный тип, например в PyFloatObject*:

Рисунок 14-12

Выводы

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

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

Глава 3. Компиляция CPython

Содержание

Глава 3. Компиляция CPython

Компиляция CPython в macOS

Компиляция CPython в Linux

Установка персональной версии

Краткое руководство по Make

Цели Make CPython

Цели сборки

Цели тестирования

Цели очистки

Цели установки

Различные цели

Компиляция CPython в Windows

Установка необходимых зависимостей

Компиляция из вашей командной строки

Компиляция из Visual Studio

Проводимая профилем оптимизация

Выводы

Теперь, когда вы выгрузили некую среду разработки и настроили её, вы можете скомпилировать сам исходный код CPython в некий исполняемый интерпретатор.

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

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

Эти инструменты применяются в зависимости от используемой вами операционной системы, а потому перепрыгните сразу в раздел для своей операционной системы.

[Замечание]
Замечание

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

При компиляции CPython или изменении его исходного кода или стандартной библиотеки, это всегда происходит в рамках песочницы его каталога исходного кода.

Если вы желаете установить некую индивидуальную версию, этот этап обсуждается в данной главе.

Компиляция CPython в macOS

Компиляция CPython в macOS требует некоторые дополнительные приложения и библиотеки. Прежде всего, вам требуется существенный набор инструментов компилятора C. Вы можете обновить в macOS через App Store прикладное приложение Command Line Tools. Вам требуется выполнить его начальную установку в соответствующем терминале.

[Замечание]
Замечание

Для открытия терминала в macOS пройдите в Applications > Other > Terminal/ Вы захотите сохранить это приложение в своём Dock, Ctrl + Click его иконку и выберите Keep in Dock.

При помощи терминала установите компилятор C и набор инструментов выполнив следующее:

$ xcode-select --install

После запуска этой команды вы получите приглашение на выгрузку и установку набора инструментов, включающего Git, Make, а также компилятор C GNU.

Вам также потребуется рабочая копия OpenSSL для применения при извлечении пакетов с вебсайта PyPI. Если вы планируете применять эту сборку для установки дополнительных пакетов, тогда потребуется удостоверение SSL.

Наиболее простой способ установки OpenSSL в macOS состоит в использовании Homebrew.

[Замечание]
Замечание

Если у вас нет Homebrew, вы можете выгрузить его напрямую из GitHub и установить при помощи такой команды:

$ /usr/bin/ruby -e "$(curl -fsSL \
  https://raw.githubusercontent.com/Homebrew/install/master/install)"

После того как у вас имеется установленным Homebrew, вы можете установить необходимые зависимости для CPython при помощи команды brew install:

$ brew install openssl xz zlib gdbm sqlite

Теперь, когда у вас имеются необходимые зависимости, вы можете выполнить сценарий configure

Команда Homebrew brew --prefix <package> снабдит вас каталогом, в котором установлен <package>. Компилируя то местоположение, которое применяет Homebrew, вы включаете поддержку SSL.

Установка флага --with-pydebug включает специальные точки входа отладки. Если вы намерены выполнять отладку для целей разработки или тестирования, добавьте этот флаг. Отладка CPython рассматривается в Главе 14. Отладка.

Этап настройки необходимо выполнить только один раз, причём определив местоположение zlib:

$ CPPFLAGS="-I$(brew --prefix zlib)/include" \
  LDFLAGS="-L$(brew --prefix zlib)/lib" \
  ./configure --with-openssl=$(brew --prefix openssl) \
  --with-pydebug

Запуск ./configure выработает некий makefile в корне данного репозитория. Вы можете применять его для автоматизации своего процесса сборки.

Теперь вы можете собрать свой исполняемый файл CPython, выполнив такую команду:

$ make -j2 -s

[Совет]
Смотри также

За дополнительными сведениями относительно вариантов для make обратитесь к разделу Краткое руководство по Make.

В процессе сборки вы можете получать некоторые ошибки. В итоге своей сборки make уведомит вас что собраны не все пакеты. Например, пакеты ossaudiodev, spwd и _tkinter завершились неудачей при сборке при данном наборе инструкций. Это допустимо если вы не планируете разработку таких пакетов. Если же вы всё- таки планируете такое, обратитесь к Python Developer’s Guide за дополнительными сведениями.

Нашей сборке потребуется несколько минут и она выработает исполняемый файл с названием python.exe. Всякий раз, когда вы вносите изменения в свой исходный код, вам потребуется повторно запускать make с теми же самыми флагами.

Ваш исполняемый файл python.exe это исполняемый файл CPython для отладки. Чтобы увидеть работающим REPL, запустите python.exe:

$ ./python.exe
Python 3.9 (tags/v3.9:9cf67522, Oct 5 2020, 10:00:00)
[Clang 10.0.1 (clang-1001.0.46.4)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>>

[Замечание]
Замечание

Да, всё верно. ваша сборка macOS имеет расширение файла .exe. Это расширение вовсе не потому что это исполняемый файл Windows!.

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

Если вы позднее запустите make install или make altinstall, тогда файл будет переименован в python до того как он установится в вашей системе.

Компиляция CPython в Linux

Для компиляции CPython в Linux вам вначале требуется выгрузить и установить make, gcc, configure и pkgconfig.

Для Fedora Core, RHEL, CentOS или прочих основанных на YUM {Прим. пер.: DNF} систем воспользуйтесь такой командой:

$ sudo yum install yum-utils
или
$ sudo dnf -y install dnf-utils

Для Debian, Ubuntu или прочих основанных на APT системах воспользуйтесь такой командой:

$ sudo apt install build-essential

Затем установите некоторые дополнительные требующиеся пакеты.

Для Fedora Core, RHEL, CentOS или прочих систем на основе YUM {DNF} воспользуйтесь следующей командой:

$ sudo yum-builddep python3
или
$ sudo dnfbuilddep python3

Для Debian, Ubuntu или прочих, основанных на APT система= применяйте такую команду:

$ sudo apt install libssl-dev zlib1g-dev libncurses5-dev \
  libncursesw5-dev libreadline-dev libsqlite3-dev libgdbm-dev \
  libdb5.3-dev libbz2-dev libexpat1-dev liblzma-dev libffi-dev

Теперь, когда вы получили зависимости, вы можете выполнить сценарий configure, причём с необязательным включением особых точек входа отладки --with-pydebug:

$ ./configure --with-pydebug

Далее, исполнив выработанный makefile, вы можете собрать исполняемый файл CPython:

$ make -j2 -s

[Совет]
Смотри также

За дополнительными сведениями относительно вариантов для make обратитесь к разделу Краткое руководство по Make.

Проверьте полученный вывод чтобы убедиться что не возникло проблем компиляции с модулем _ssl. Если таковые возникли, проверьте свой дистрибутив относительно инструкций по установке необходимых заголовков для OpenSSL.

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

Сама сборка займёт несколько минут и сгенерирует исполняемый файл с названием python. Это исполняемый файл CPython, предназначенный для отладки. Чтобы увидеть рабочий REPL, выполните ./python:

$ ./python
Python 3.9 (tags/v3.9:9cf67522, Oct 5 2020, 10:00:00)
[Clang 10.0.1 (clang-1001.0.46.4)] on Linux
Type "help", "copyright", "credits" or "license" for more information.
>>>

Установка персональной версии

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

Для macOS и Linux воспользуйтесь командой altinstall, которая не буде устанавливать символическую ссылку для python3 и установит обособленную версию:

$ make altinstall

Для Windows вам придётся изменить конфигурацию из Debug to Release,затем скопировать эти упакованные исполняемые файлы в некий каталог в вашем компьютере, который является частью его системного пути.

Краткое руководство по Make

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

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

Было бы нереальным ожидать от конкретного разработчика, что он знает местоположение всех этих библиотек и скопирует со вставкой их в свою командную строку. Поэтому в проектах C/C++ для автоматизации сценария создания и сборки обычно используются make и configure.

Когда вы выполняете ./configure, autoconf выполняет поиск необходимых CPython библиотек и копирует их пути в некий makefile.

Выработка makefile аналогична некоторому сценарию оболочки и разбивается на разделы, именуемые targets (целями).

Неким примером выступает цель docclean. Эта цель удаляет некоторые файлы документации применяя команду rm:

docclean:
    -rm -rf Doc/build
    -rm -rf Doc/tools/sphinx Doc/tools/pygments Doc/tools/docutils

Для выполнения этй цели, исполните make docclean. docclean это всего лишь цель, которая выполняет лишь две команды.

Вот соглашение для выполнения цели make:

$ make [options] [target]

Если вы вызовите make без определения некой цели, тогда make выполнит установленную первой предписанную в соответствующем makefile цель. Для Cpython это цель all, которая выполняет сборку всех частей CPython.

make обладает большим числом параметров. Вот некоторые, которые вы найдёте полезными на протяжении данной книги:

Таблица 3-1. Некоторые параметры make

Параметр
Применеие

-d, --debug[=FLAGS]

Перекрываемые makefile -ами переменные среды

-e, --environment-overrides

Не позволяет никакого доступа, но возвращает ошибку

-i, --ignore-errors

Игнорировать ошибки из команд

-j [N], --jobs[=N]

Разрешить N заданий за раз или неограниченное число заданий в противном случае

--k, --keep-going

Продолжать выполнение, когда некоторые цели не могут быть выполнены

-l [N], --load-average[=N], --max-load[=N]

Запускать множество заданий только когда load < N

-n, --dry-run

Выводить команды на печать вместо их выполнения

-s, --silent

Не выводить команды эхом

-S, --stop

Останавливаться когда цели не могут быть сделаны

В своём следующем разделе и на протяжении этой книги вы будете выполнять make с такими параметрами:

$ make -j2 -s [target]

Установленный флаг -о2 позволяет make выполнять одновременно два задания. Если у вас имеется четыре и более ядре, тогда вы можете изменить его на четыре или более и компиляция завершится быстрее. {Прим. пер.: предлагаем некое эмпирическое правило по настройке данного значения в нашем переводе Программирование ядра Linux Кайваня Н Биллимории, изданной Packt Publishing в марте 2021.}

Установка флага -s прекращает вывод на печать в консоль всех команд из makefile при их выполнении. Если вы желаете увидеть что произойдёт, можете удалить этот флаг -s.

Цели Make CPython

И для Linux, и для macOS вы обнаружите, что вам требуется очищать файлы, сборки или освежать имеющуюся конфигурацию. Приводимые далее разделы содержат таблицы, структурирующих некоторое число полезных целей make, собираемых в makefile CPython.

  Цели сборки

Следом идцт цели, применяемые для сборки исполняемого файла CPython:

Таблица 3-2. Цели сборки исполняемого файла CPython

Цель
Назначение

all (по умолчанию)

Собирает компилятор, библиотеки и модули

clinic

Запускает параметр Clinic во всех исходных файлах

profile-opt

Компилирует исполняемый файл Python с руководимой профилем оптимизацией

regen-all

Повторно вырабатывает все генерируемые файлы

sharedmods

Собирает совместно используемые модули

  Цели тестирования

Приводимые ниже цели используются для тестирования компилируемого вами исполняемого файла:

Таблица 3-3. Цели тестирования исполняемого файла CPython

Цель
Назначение

coverage

Скомпилировать и запустить тестирование при помощи gcov

coverage-lcov

Создать HTML отчёты зоны действия

quicktest

Запускает более быстрый набор регрессионного тестирования, исключая те тесты, которые требуют большего времени

test

Запускает базовое регрессионное тестирование

testall

Запускает имеющийся комплект тестов дважды, один раз без файлов .pyc и один раз с ними

testuniversal

Запускает имеющийся комплект тестов для обеих архитектур в универсальной сборке для OS X

  Цели очистки

Первичными целями очистки выступают clean, clobber и distclean. Цель clean служит общему удалению скомпилированных и кэшированных библиотек и файлов .pyc.

Если вы обнаружите, что clean не справляется со своим заданием, тогда испытайте clobber. Цель clobber удалит ваш makefile, поэтому вам придётся снова выполнить ./configure.

Для полной очистки всей среды вплоть до дистрибутива выполните цель distclean.

Последующий список включает перечисленные нами только что первичные цели, а также некоторые дополнительные цели очистки:

Таблица 3-4. Цели очистки каталога исходного кода

Цель
Назначение

check-clean-src

При сборке из исходного кода проверяет что он чистый

clean

Удаляет файлы .pyc, скомпилированные библиотеки и профили

cleantest

Удаляет каталоги test_python_* ранее отказавших заданий тестирования

clobber

То же что и clean, но также удаляет библиотеки, теги, конфигурации и сборки

distclean

То же что и clobber, но помимо этого также удаляет всё выработанное из исходного кода, например makefile

docclean

Удаляет в Doc/ собранную документацию

profile-removal

Удаляет все профили оптимизации

pycremoval

Удаляет файлы .pyc

  Цели установки

Существуют две разновидности целей установки: установленная по умолчанию версия, такая как install и версия alt, такая как altinstall. Если вы желаете установить скомпилированную вами версию в своём компьютере, но не желаете чтобы она стала установкой Python 3 версии по умолчанию, пользуйтесь alt версией соответствующих команд:

Таблица 3-5. Цели установки исполняемого файла CPython

Цель
Назначение

altbininstall

Устанавливает собранный интерпретатор python с дополненным номером версии, например, python3.9

altinstall

Устанавливает совместные библиотеки, исполняемые файлы и документацию с соответствующим суффиксом версии

altmaninstall

Устанавливает пронумерованные версией руководства

bininstall

Устанавливает все исполняемые файлы, такие как python, idle и 2to3

commoninstall

Устанавливает совместные библиотеки и модули

install

Устанавливает совместные библиотеки, исполняемые файлы и документацию (исполнит commoninstall, bininstall и maninstall)

libinstall

Установит совместные библиотеки

maninstall

Установит все руководства

sharedinstall

Динамически загружает модули

После того как вы выполните установку при помощи make install, с вашей скомпилированной библиотекой будет связана соответствующая команда python3. Однако, если вы воспользовались make altinstall, будет установлен лишь python$(VERSION), а существующая привязка к python3 останется нетронутой.

  Различные цели

Ниже приводятся некоторые дополнительные цели make, которые вы можете найти полезными:

Таблица 3-6. Различные дополнительные цели

Цель
Назначение

autoconf

Автоматически повторно вырабатывает configure и pyconfig.h.in

python-config

Вырабатывает сценарий python-config

recheck

Повторно исполняет configure с теми же самыми параметрами, которые применялись в последний раз

smelly

Проверяет что экспортируемые файлы начинаются с Py или _Py (см. PEP 7)

tags

Создаёт файл тегов для vi

TAGS

Создаёт файл тегов для Emacs

Компиляция CPython в Windows

В Windows имеются два способа компиляции исполняемых файлов и библиотек CPython:

Компиляция из вашей командной строки. Она всё ещё требует наличия компилятора Microsoft Visual C++, который поставляется совместно с Visual Studio.

В Visual Studio откройте PCbuild/pcbuild.sln и непосредственно выполняйте сборку.

В приводимых далее разделах мы опишем оба этих варианта.

  Установка необходимых зависимостей

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

Внутри папки PCbuild имеется файл .bat, который автоматизирует это процесс для вас. Откройте окно своей командной строки внутри PCbuild и выполните PCbuild/get_externals.bat:

> get_externals.bat
Using py -3.7 (found 3.7 with py.exe)
Fetching external libraries...
Fetching bzip2-1.0.6...
Fetching sqlite-3.28.0.0...
Fetching xz-5.2.2...
Fetching zlib-1.2.11...
Fetching external binaries...
Fetching openssl-bin-1.1.1d...
Fetching tcltk-8.6.9.0...
Finished.

Теперь вы можете выполнять компиляцию либо из приглашения командной строки, либо из Visual Studio.

  Компиляция из вашей командной строки

Для компиляции из приглашения командной строки вам требуется выбрать архитектуру того ЦПУ, под который вы желаете выполнять компиляцию. Установленной по умолчанию является win32, но имеется шанс, что вы пожелаете исполняемый 64- битный файл (amd64).

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

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

> build.bat -p x64 -c Debug

Эта команда произведёт исполняемый файл Python PCbuild/amd64/python_d.exe. Запустит это исполняемый файл непосредственно из приглашения командной строки:

> amd64\python_d.exe

Python 3.9 (tags/v3.9:9cf67522, Oct 5 2020, 10:00:00)
 [MSC v.1922 64 bit (AMD64)] on win32
Type "help", "copyright", "credits" or "license" for more information.
>>>

Теперь вы пребываете внутри REPL своего откомпилированного исполняемого файла CPython.

Для компиляции выпуска исполняемого кода воспользуйтесь этой командой:

> build.bat -p x64 -c Release

Данная команда создаст исполняемый файл PCbuild/amd64/python.exe.

[Замечание]
Замечание

Значение суффикса _d определяет, что CPython был собран в отладочной конфигурации.

Соответствующие выпуски исполняемых файлов Python.org компилируются в конфигурации, руководимой профилем (PGO). Относительно дополнительных сведений по PGO обртитесь к разделу Проводимая профилем оптимизация в самом конце этой главы.

Параметры

В build.bat доступны следующие аргументы:

Таблица 3-7. Различные дополнительные цели

Флаг
Назначение
Ожидаемое значение

-p

Архитектура ЦПУ платформы сборки

x64, Win32 (значение по умолчанию), ARM, ARM64

-c

Конфигурация сборки

Release (значение по умолчанию), Debug, PGInstrument или PGUpdate

-t

Цель сборки

Build (значение по умолчанию), Rebuild, Clean, CleanAll

Флаги

Вот некоторые необязательные флаги, которые вы можете применять для build.bat:

Таблица 3-8. Различные дополнительные цели

Флаг
Назначение

-v

Verbose mode (Подробный режим): в процессе сборки отображает информационные сообщения

-vv

Very Verbose mode (Очень подробный режим): в процессе сборки отображает подробные информационные сообщения

-q

Quiet mode (Тихий режим): в процессе сборки отображает только предупреждения и сообщения об ошибках

-e

Выгружает и устанавливает внешние зависимости (значение по умолчанию)

-E

Не выгружает и не устанавливает внешние зависимости (значение по умолчанию)

--pgo

Собирает проводимую профилем оптимизацию

--regen

Повторно вырабатывает всю грамматику и все лексемы (применяется при обновлении самого языка)

Для выдачи полного списка выполните build.bat -h.

  Компиляция из Visual Studio

Внутри папки PCbuild находится файл решения Visual Studio, PCbuild/pcbuild.sln, предназначенный для сборки и исследования исходного кода CPython.

Когда это файл загружен, он попросит у вас повторно ввести цель проекта внутри данного решения для той версии компилятора C/C++, что была установлена вами. Visual Studio также установит цель на установленный вами выпуск Windows SDK.

Убедитесь что установленная версия Windows SDK является самой новой установленной версией и что набор инструментов вашей платформы самый последний. Если вы пропустили это окно, вы можете потом кликнуть правой кнопкой по своему файлу решения в окне Solutions and Projects и выбрать Retarget Solution.

Переместитесь в Build > Configuration Manager и проверьте что ваш ниспадающий перечень Active Solution Configuration установлен в Debug, а Active Solution Platform настроена на x64 для 64- битной архитектуры ЦПУ или на win32 для 32- битной.

Затем, нажав Ctrl + Shift + B или выбрав Build > Build Solution выполните сборку CPython. Если вы столкнётесь с ошибкой об отсутствии Windows SDK, убедитесь что вы правильно настроили целевые установки в своём окне Retarget Solution . Вы долны также видеть папку Windows Kits в своём меню Пуска с Windows Software Development Kit внутри неё.

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

Для запуска отладочной версии CPython нажмите F5 и CPython запустит ваш REPL в режиме отладки:

Рисунок 3-1

Вы можете запускать свой собранный выпуск изменив конфигурацию сборки с Debug на Release в верхней полосе меню и посторно запустив Build > Build Solution. Теперь у вас внутри PCbuild/amd64/ имеются обе версии исполняемого файла CPython, и отладочная, и сборка выпуска.

Вы можете настроить Visual Studio способным открывать REPL либо со сборкой выпуска, либо с отладочной, выбрав Tools > Python > Python Environments в своём меню сверху. в Панели Python Environments кликните Add Environment и далее укажите целью исполняемый файл отладки или выпуска. Отладочный исполняемый файл будет завершаться на _d.exe, например, python_d.exe или pythonw_d.exe.

Скорее всего вы пожелаете применять отладочный исполняемый файл, так как он поставляется с поддержкой отладки в Visual Studio и будет полезен по мере чтения вами этой книги.

В окне Add Environment укажите целью файл python_d.exe, так как это интерпретатор внутри PCbuild/amd64, а pythonw_d.exe в качестве оконного интерпретатора:

Рисунок 3-2

Запустите сеанс REPL, кликнув по Open Interactive Window в окне Python Environments и вы обнаружите REPL для своей скомпилированной версии Python:

Рисунок 3-3

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

Чтобы проще перемещаться по вашему коду, в представлении Solution кликните по кнопке next за иконкой Home чтобы переключиться на представление Folder:

Рисунок 3-4

Проводимая профилем оптимизация

Все процессы сборки macOS, Linux и Windows обладают флагами для PGO (proрle-guided optimization, проводимой профилем оптимизации). PGO не является чем- то, создаваемым командой Python, а просто некой функциональной возможностью большого числа компиляторов, включая и те, что применяются для CPython.

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

Для CPython этап профилирования запускается python -m test --pgo, что выполняет регрессивное тестирование, определяемое в Lib/test/libregrtest/pgo.py. Эти тесты выбраны намеренно, так как они применяют широко используемые модули и типы C.

[Замечание]
Замечание

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

Если же вы желаете распространять некую индивидуально скомпилированную версию CPython в какую- то промышленную среду, тогда вам следует запускать Code с флагом ./configure --with-pgo в Linux и macOS, а также применять флаг --pgo для build.bat для Windows.

Поскольку оптимизация специфична для конкретной платформы и архитектуры, для которых выполнялось определённое профилирование, профили PGO не могут совмещаться среди операционных систем и архитектур ЦПУ. Сами дистрибутивы CPython в Python.org уже прошли через PGO, поэтому если вы выполните эталонное тестирование некого унифицированным образом скомпилированного исполняемого файла, он окажется более медленным чем выгружаемый с Python.org .

Проводимые профилем оптимизации Windows, macOS и Linux содержат такие проверки и улучшения:

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

Упреждающее исполнение и вложение виртуального вызова: Когда некий вызов виртуальной функции часто имеет целью определённую функцию, тогда PGO способен вставлять условное исполнение, напрямую исполняющее вызов такой функции. Подобный непосредственный вызов затем может быть заключён вовнутрь.

Оптимизация выделения регистров: На основании результатов данных профилирования PGO оптимизирует выделение регистров.

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

Оптимизация горячих точек: Те функции, на которые соответствующая программа тратит максимальное время выполнения могут быть оптимизированы в отношении скорости.

Оптимизация схемы функции: После того как PGO проанализирует граф вызовов, те функции, которые обладают тенденцией располагаться в одном и том же пути выполнения перемещаются в один и тот же раздел своего компилируемого приложения.

Оптимизация условного ветвления: PGO способен рассматривать ветвления решений, таких как операторы if … else if или switch и замечать наиболее часто используемые пути. Например, когда в неком операторе switch имеется десять вариантов , а один из них используется в 9 процентах раз, тогда этот вариант может быть перемещён вверх с тем, чтобы он выполнялся немедленно в пути своего кода.

Выделение мёртвых мест: Код, который не исполняется на протяжении PGO перемещается в отдельный раздел вашего приложения.

Выводы

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

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

Глава 4. Язык и грамматика Python

Содержание

Глава 4. Язык и грамматика Python

Почему CPython написан на C, а не на Python

Спецификация языка Python

Документация языка

Пример

Файл грамматики

Пример: оператор while

Генератор синтаксического разбора

Повторная генерация грамматики

Лексемы

Выводы

Основная цель компилятора состоит в преобразовании одного языка в другой. Представляйте себе компилятор транслятором. Вы, наверное, слышали о трансляторе, который слушает как вы говорите по- английски, а затем повторяет ваши слова на другом языке, например, японском.

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

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

Одним из подлежащих рассмотрению вопросов при выборе компилятора заключается в требованиях переносимости системы. Java и .NET CLR будут выполнять компиляцию в некий промежуточный язык с тем, чтобы скомпилированный код являлся переносимым по множеству системных архитектур. C, Go, C++ и Pascal будут компилировать в исполняемый файл кода. Этот исполняемый файл собран для той платфрмы, под которую он был скомпилирован.

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

Код Python не компилируется в машинный код. Он компилируется в некий промежуточный язык нижнего уровня с названием bytecode (байтного кода). Такой байтный код хранится в файлах .pyc и кэшируется для исполнения. Если вы запустите то же самое приложение без изменения своего исходного кода, он окажется быстрее при своём повторном исполнении. Это обусловлено тем, что он загружает откомпилированный байтовый код вместо его повторной компиляции всякий раз.

Почему CPython написан на C, а не на Python

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

Итак, почему наш компилятор CPython написан на C, а не на Python?

Ответ основывается на том как работают компиляторы. Существует два типа компиляторов:

Компиляторы с самостоятельной поддержкой это те компиляторы, которые написаны на том языке, который они компилируют, например, компилятор Go. Это выполняется процессом, называемым bootstrapping (самозапуск).

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

Когда вы пишите некий новый язык программирования с чистого листа, вам требуется некое исполняемое приложение для компиляции вашего компилятора! Вам требуется некий компилятор чтобы исполнять что бы то ни было, а потому когда разрабатывается некий новый язык, они пишутся начале на более старом, более устоявшемся языке программирования. Также имеются доступными инструменты, которые берут некую спецификацию языка и создают некий синтаксический анализ, который вы скоро изучите в этой главе. Популярные компиляторы компиляторов включают в свой состав GNU Bison, Yacc и ANTLR.

[Совет]
Смотри также

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

Исключительным примером компилятора с самостоятельным запуском выступает язык программирования Go. Самый первый компилятор Go был написан на C, затем, раз Go оказался доступным для компиляции, его компилятор был повторно написан на Go.

CPython, с другой стороны, придерживается своего наследия C. Многие стандартные библиотечные модули, например, модуль ssl или модуль sockets написаны на C для доступа к API нижнего уровня операционной системы.

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

Существует также компилятор Python написанный на Python с названием PyPy. Логотипом PyPy выступает Ouroboros для представления самодостаточной природы этого компилятора.

Другим примером кросс- компилятора для Python выступает Jython. Jython написан на Java и компилирует из исходного кода в байтовый код Java. Точно также как CPython упрощает импорт библиотек C и их применение в Python, Jython упрощает импорт опорных модулей и классов Java.

Самый первый шаг создания некого компилятора состоит в определении самого языка. Например, эо недопустимо для Python:

def my_example() <str> :
{
    void* result = ;
}

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

[Замечание]
Замечание

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

Для Windows:

> python.exe

Для Linux:

$ ./python

Для macOS:

$ ./python.exe

Спецификация языка Python

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

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

  Документация языка

Каталог Doc/reference содержит толкование reStructuredText всех функциональных возможностей языка Python. Это файлы с официального справочного руководства Python docs.python.org/3/reference.

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

Рисунок 4-1

  Пример

Внутри Doc/reference/compound_stmts.rst вы можете видеть некий простой пример определения оператора with.

Оператор with имеет большое число форм, самой простой выступает реализация экземпляра диспетчера контекста и встроенный блок кода:

with x():
   ...

Вы можете назначить полученный результат некой переменной при помощи ключевого слова as:

with x() as y:
   ...

Вы также можете соединять цепочки диспетчеров контекста совместно при помощи запятой:

with x() as y, z() as jk:
   ...

Эта документация содержится в читаемой человеком спецификации своего языка программирования. Машинно- читаемая спецификация располагается в отдельном файле, Grammar/python.gram.

  Файл грамматики

Файл грамматики Python пользуется спецификацией грамматики синтаксического разбора выражений (PEG, parsing expression grammar). В файле грамматики вы можете пользоваться следующей нотацией:

* для повтора

+ для по крайней мере одного повтора

[] для необязательных частей

| для альтернатив

() для группирования

В качестве примера задумайтесь о том, как вы определяете чашку кофе:

Она должна обладать чашкой

Она обязана содержать по крайней мере одну дозу эспрессо и может содержать множество доз

Она может иметь молоко, но это не обязательно

Она может иметь воду, но это не обязательно

Она может иметь молоко, затем молоко может быть разных типов, например, сливками, обезжиренным или соевым

Будучи определённым в PEG, заказ кофе мог бы выглядеть как- то так:

coffee: 'cup' ('espresso')+ ['water'] [milk]
milk: 'full-fat' | 'skimmed' | 'soy'

[Совет]
Смотри также

В CPython 3.9, исходный код CPython имеет два грамматических файла. Одна наследуемая грамматика писалась в свободной от контекста нотации с названием Backus-Naur Form (BNF). В CPython 3.10, соответствующий файл грамматики BNF (Grammar/Grammar) был удалён.

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

В этой главе вы будете визуализировать грамматику железнодорожными диаграммами. Вот железнодорожная диаграмма для нашего оператора кофе:

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

Рисунок 4-2

  Пример: оператор while

Существует несколько форм оператора while. Самая простая содержит некое выражение, далее завершающее :, за которым следует блок кода:

while finished == True:
    do_things()

В качестве альтернативы вы можете применять некое выражение присваивания, на которое грамматика ссылается как на named_expression (именованное выражение). Это новая функциональная возможность начиная с Python 3.8:

while letters := read(document, 10):
    print(letters)

В качестве необязательного элемента, за операторами while может следовать оператор else и блок:

while item := next(iterable):
    print(item)
else:
    print("Iterable is empty")

Если вы отыщите в своём грамматическом файле while_stmt, тогда вы можете обнаружить такое определение:

hile_stmt[stmt_ty]:
    | 'while' a=named_expression ':' b=block c=[else_block] ...

Всё, что заключено в кавычки, является неким строковым литералом с названием терминал. Терминалы это то, что распознаётся в качестве ключевых слов.

В этих двух строках существуют ссылки на два прочих определения:

block ссылается на блок кода с одним или множеством операторов.

named_expression ссылается на простое выражение или назначаемое выражение.

Будучи визуализированным в железнодорожной диаграмме, наш оператор while выглядит подобно следующему:

Рисунок 4-3

В качестве более сложного примера, оператор try определяется в грамматике следующим образом:

try_stmt[stmt_ty]:
    | 'try' ':' b=block f=finally_block { _Py_Try(b, NULL, NULL, f, EXTRA) }
    | 'try' ':' b=block ex=except_block+ el=[else_block] f=[finally_block]..
except_block[excepthandler_ty]:
    | 'except' e=expression t=['as' z=target { z }] ':' b=block {
        _Py_ExceptHandler(e, (t) ? ((expr_ty) t)->v.Name.id : NULL, b,  ...
    | 'except' ':' b=block { _Py_ExceptHandler(NULL, NULL, b, EXTRA) }
finally_block[asdl_seq*]: 'finally' ':' a=block { a }

Существует два применения оператора try:

try с единственным оператором finally

try с одним или множеством выражений except, за которым следует необязательный else, а затем необязательный finally.

Вот те же самые варианты, вызуализируемые железнодорожной диаграммой:

Рисунок 4-4

Оператор try это хороший образец более сложной структуры.

Если вы желаете разобраться в подробностях языка программирования Python, прочтите его грамматику, определяемую в Grammar/python.gram

Генератор синтаксического разбора

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

Имеющийся синтаксический анализатор CPython был переписан в Python 3.9 из автомата таблицы синтаксического анализа (модуль pgen) в синтаксический анализатор контекстной грамматики.

В Python 3.9 его старый синтаксический анализатор доступен из командной строки при использовании соответствующего флага -X oldparser, а в Python 3.10 он удалён полностью. Данная книга ссылается на новый синтаксический анализатор, реализованный в 3.9.

Повторная генерация грамматики

Чтобы увидеть pegen, наш новый генератор PEG, введённый в CPython 3.9 в действии, вы можете изменить часть имеющейся грамматики Python. Отыщите в Grammar/python.gram small_stmt чтобы увидеть установленное определение для небольших операторов:

small_stmt[stmt_ty] (memo):
    | assignment
    | e=star_expressions { _Py_Expr(e, EXTRA) }
    | &'return' return_stmt
    | &('import' | 'from') import_stmt
    | &'raise' raise_stmt
    | 'pass' { _Py_Pass(EXTRA) }
    | &'del' del_stmt
    | &'yield' yield_stmt
    | &'assert' assert_stmt
    | 'break' { _Py_Break(EXTRA) }
    | 'continue' { _Py_Continue(EXTRA) }
    | &'global' global_stmt
    | &'nonlocal' nonlocal_stmt

В частности, наша строка 'pass' { _Py_Pass(EXTRA) } служит для pass (передачи) оператора:

Рисунок 4-5

Измените эту строку на приём терминалов (ключевых слов) 'pass' или 'proceed' в качестве ключевых слов, добавив выбор, | и литерал 'proceed':

| ('pass'|'proceed') { _Py_Pass(EXTRA) }

Рисунок 4-6

Затем повторно соберите имеющиеся грамматические файлы. CPython поставляется со сценариями для автоматической повторной выработки грамматики.

В macOS и Linux запустите цель make regen-pegen:

$ make regen-pegen

Для Windows приведите в действие приглашение командной строки в каталоге PCBuild и запустите build.bat с установленным флагом --regen:

> build.bat --regen

Вы должны обнаружить некий вывод, отображающий что был повторно выработан файл Parser/pegen/parse.c.

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

Если код скомпилирован успешно, тогда вы сможете выполнить свой новый исполняемый файл CPython и запустите REPL.

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

$ ./python

Python 3.9 (tags/v3.9:9cf67522, Oct 5 2020, 10:00:00)
[Clang 10.0.1 (clang-1001.0.46.4)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> def example():
...    proceed
...
>>> example()

Поздравляем, вы изменили синтаксис CPython и скомпилировали свою собственную версию CPython!

Далее давайте изучим лексемы и их взаимосвязь с грамматикой.

  Лексемы

Помимо файла грамматики в нашей папке Grammar имеется файл Grammar/Tokens, который содержит все уникальные типы, обнаруживаемые в качестве узлов листьев в неком дереве синтаксического разбора. Всякая лексема (токен) обладает названием и выработанным уникальным идентификатором. Значения имён упрощают ссылки на лексемы в соответствующем обработчике лексем (tokenizer).

[Замечание]
Замечание

Файл Grammar/Tokens является новой функциональной возможностью в Python 3.8.

Например, левая круглая скобка (left parenthesis) имеет название LPAR, а двоеточие именуется как SEMI. Позднее в этой книге вы увидите эти лексемы:

LPAR                    '('
RPAR                    ')'
LSQB                    '['
RSQB                    ']'
COLON                   ':'
COMMA                   ','
SEMI                    ';'

Как и в случае с файлом Grammar, если вы вносите изменения в файл Grammar/Tokens, вам потребуется повторно выполнить pegen.

Чтобы увидеть лексемы в действии, вы можете воспользоваться модулем tokenize из CPython.

[Замечание]
Замечание

Этот написанный на Python обработчик лексем выступает в роли модуля утилиты. Реальный синтаксический анализатор Python применяет иной процесс для идентификации лексем.

Создайте простой сценарий Python с названием test_tokens.py: cpython-book-samples/13/test_tokens.py

# Demo application
def my_function():
   proceed

Дайте этот файл test_tokens.py на вход модулю, собираемому в стандартной библиотеке, с названием tokenize. Вы обнаружите список лексем по строкам и символам. Для вывода в точности (exact) названий лексем воспользуйтесь флагом -e:

$ ./python -m tokenize -e test_tokens.py

0,0-0,0:            ENCODING       'utf-8'
1,0-1,14:           COMMENT        '# Demo application'
1,14-1,15:          NL             '\n'
2,0-2,3:            NAME           'def'
2,4-2,15:           NAME           'my_function'
2,15-2,16:          LPAR           '('
2,16-2,17:          RPAR           ')'
2,17-2,18:          COLON          ':'
2,18-2,19:          NEWLINE        '\n'
3,0-3,3:            INDENT         '   '
3,3-3,7:            NAME           'proceed'
3,7-3,8:            NEWLINE        '\n'
4,0-4,0:            DEDENT         ''
4,0-4,0:            ENDMARKER      ''

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

В своём выводе применяемый модуль tokenize подразумевает наличие некоторых лексем:

Токен ENCODING для utf-8.

Некий DEDENT для закрытия объявления рассматриваемой функции.

Некий ENDMARKER для закрытия соответствующего файла

Пустая строка в самом конце

Хорошей практикой является присутствие пустой строки в самом конце ваших исходных файлов Python. Если вы пропустите её, CPython добавит её вам.

Модуль tokenize написан на чистом Python и располагается в Lib/tokenize.py.

Чтбы увидеть подробные показания синтаксического анализатора C вы можете запустить отладочную сборку Python с установленным флагом -d. Воспользуйтесь ранее созданным вами сценарием test_tokens.py, запустите его следующим образом:

$ ./python -d test_tokens.py

 > file[0-0]: statements? $
  > statements[0-0]: statement+
   > _loop1_11[0-0]: statement
    > statement[0-0]: compound_stmt
 ...
  + statements[0-10]: statement+ succeeded!
 + file[0-11]: statements? $ succeeded!

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

Чтобы очистить свой код, верните обратно изменения в Grammar/python.gram, повторно выработайте свою грамматику снова и затем очистите свою сборку и повторно выполните компиляцию.

Для macOS и Linux воспользуйтесь следующим:

$ git checkout -- Grammar/python.gram
$ make regen-pegen
$ make -j2 -s

Или примените такое в Windows:

> git checkout -- Grammar/python.gram
> build.bat --regen
> build.bat -t CleanAll
> build.bat -t Build

Выводы

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

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

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

Изменения в грамматике вводят обратную несовместимость, что создаёт работу всем разработчикам.

Когда разработчик ядра Python предлагаЕт некое изменение в установленной грамматике,он обязан внести предложение в PEP (Python Enhancement Proposal, План расширения Python). Все PEP нумеруются и нндексируются в PEP. Документы PEP 5 документирует руководство по развитию языка и определяет какие изменения должны предлагаться PEP.

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

После того как PEP приходит к согласию и проект финализировался, управляющий совет обязан принять или отклонить его. Основной мандат управляющего совета, определённый в PEP 13, постулирует, что участники совета должны работать над "сопровождением качества и стабильности собственно языка программирования Python и интерпретатора CPython".

Глава 5. Конфигурация и входные данные

Содержание

Глава 5. Конфигурация и входные данные

Состояние конфигурации

Предварительная инициализация конфигурации

Относящиеся к делу исходные файлы

Структура данных конфигурации времени исполнения

Монтаж конфигурации времени исполнения при помощи командной строки

Просмотр флагов времени исполнения

Сборка конфигурации

Сборка модуля из входных данных

Относящиеся к делу файлы исходного кода

Считывание файлов/ входных данных

Ввод строки из приглашения на ввод команд

Просмотр флагов времени исполнения

Ввод данных из файла сценария или стандартного ввода

Просмотр флагов времени исполнения

Выводы

Теперь, когда в рассмотрели грамматику Python, самое время исследовать как код переходит в исполняемое состояние.

Существует множество способов, коими код Python может запускаться в CPython. Вот некоторые из наиболее употребимых подходов:

Запустить python -c и некую строку Python

Запустить python -m и название некого модуля

Запустить python <file> с путём к некому файлу, который содержит код Python

Направить в конвейере код Python в некий исполняемый python через stdin, скажем cat <file> | python

Запустить REPL и выполнять команды по одной за раз

Применяя API C пользоваться Python как некой встроенной средой

[Совет]
Смотри также

В Python имеется настолько много способов исполнения сценариев, что это может слегка подавлять. Для дополнительных сведений относительно запуска сценариев Python отсылаем вам к материалам Real Python "How to Run Your Python Scripts".

Для исполнения кода Python его интерпретатору требуется обладать тремя элементами:

Исполняемым модулем

Неким состоянием для удержания таких сведений как переменные

Некой конфигурацией, например, какие варианты включены

При наличии этих трёх компонентов имеющийся интерпретатор способен исполнить код и предоставить некий вывод:

Рисунок 5-1

[Замечание]
Замечание

Аналогично стилю руководства PEP 8 для кода Python, имеется руководство стиля PEP 7 для кода C CPython. Он включает в себя следующие стандарты именования для исходного кода C:

Установку префикса Py для общедоступных функций, не для статических функций.

Префикс Py_ предназначен для подпрограмм глобальной службы, например, Py_FatalError. Определённые группы подпрограмм (такие как API особого объектного типа) обязаны применять более длинный префикс, такой как PyString_ для строковых функций.

Общедоступные функции и переменные должны быть написаны Верблюжьей нотацией, причём с разделением слов символом подчёркивания, например, PyObject_GetAttr(), Py_BuildValue() и PyExc_TypeError().

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

Макросы обязаны обладать Верблюжьим префиксом и далее применять верхний регистр, причём все слова разделяются символом подчёркивания, например, PyString_AS_STRING и Py_PRINT_RAW.

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

Единственным встроенным инструментом для автоматизации этого процесса выступает сценарий с названием smelly.py, который вы можете исполнить при помощи цели make smelly в macOS или Linux, либо через командную строку:.

$ ./python Tools/scripts/smelly.py

Он распространит ошибку вверх для всех символов, которые пребывают в libpython (стандартной библиотеке CPython), но не начинаются с Py или _Py.

Состояние конфигурации

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

Как это предопределено PEP 587, значение конфигурации времени исполнения располагается в трёх местах:

PyPreConfig, используемом для конфигурации предварительной инициализации

PyConfig, применяемом для конфигурации времени выполнения

В скомпилированной конфигурации самого интерпретатора CPython

Обе структуры данных, PyPreConfig и PyConfig определены в nclude/cpython/initconfig.h.

  Предварительная инициализация конфигурации

Конфигурация предварительной инициализации отделена от конфигурации времени выполнения, поскольку её свойства относятся к установленной операционной системе или среде пользователя.

PyPreConfig обладает тремя первичными функциями:

Монтаже распределителя памяти Python

Настройке значения локализации LC_CTYPE на значение системной или предпочитаемой пользователем локализации

Монтаже режима UTF-8 (PEP 540)

Сам тип PyPreConfig содержит следующие поля, причём все с типом int:

allocator: Выбирает распределитель памяти, например, PYMEM_ALLOCATOR_MALLOC. Для получения дополнительных сведений относительно распределителя памяти запустите ./configure --help.

configure_locale: Устанавливает локализацию LC_CTYPE в значение предпочитаемой пользователем. Когда равен 0, тогда устанавливает coerce_c_locale и coerce_c_locale_warn равным 0.

coerce_c_locale: Если равен 2, то приводится к локализации C. Если равен 1, тогда считывается значение локализации LC_CTYPE для принятия решения относительно тог должна ли они приводиться.

coerce_c_locale_warn: В случае когда не равно нулю, выдаётся предупреждение если приводится локализация C/

dev_mode: Включает режим разработки.

isolated: Разрешает режим изоляции. sys.path не содержит ни значения каталога сценария, ни значения каталога пакетов площадки пользователя.

legacy_windows_fs_encoding: (Только для Windows) при неравенстве нулю, отключает режим UTF-8 и устанавливает файловую систему Python на кодирование в mbcs.

parse_argv: При неравенстве нулю используются параметры командной строки.

use_environment: Когда больше нуля, применяются переменные среды.

utf8_mode: При неравенстве нулю разрешается режим UTF-8.

  Относящиеся к делу исходные файлы

Ниже приводятся относящиеся к PyPreConfig исходные файлы:

Таблица 5-1. Относящиеся к PyPreConfig исходные файлы

Файл
Назначение

Python/initconfig.c

Загружает значения конфигурации из среды своей системы и сливает её со всеми флагами командной строки

Include/cpython/initconfig.h

Определяет структуру данных инициализирующей конфигурации

  Структура данных конфигурации времени исполнения

Второй этап настроек состоит в конфигурации времени выполнения. Структура данных настроек времени выполнения в PyConfig содержит некоторые значения, включая следующие:

Флаги времени выполнения для таких режимов как отладочный и оптимизирующий

Значение режима исполнения, например, в качестве файла сценария, stdin или модуля

Расширенные опции, предписываемые через -X <option>

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

Эти данные конфигурации применяются CPython времени выполнения для включения и отключения функциональных возможностей.

  Монтаж конфигурации времени исполнения при помощи командной строки

Python также поставляется с некоторыми вариантами взаимодействия командной строки. К примеру, CPython обладает режимом с названием verbose mode (подробный режим). Он в первую очередь предназначен разработчикам для отладки CPython.

Вы можете разрешить подробный режим при помощи флага -v и Python будет печатать на экран сообщения при загрузке модулей:

$ ./python -v -c "print('hello world')"

# installing zipimport hook
import zipimport # builtin
# installed zipimport hook
...

Вы обнаружите сотни строк или даже больше со всеми вашими импортами пакетов вашей пользовательской площадки и чем- то ещё в вашей системной среде.

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

Установленные по умолчанию значения для config->verbose жёстко закодированы в -1 в имеющемся исходном коде

Для настройки значения config->verbose применяется переменная среды PYTHONVERBOSE

Если эта переменная не существует, тогда остаётся назначенным установленное по умолчанию значение значение -1

В config_parse_cmdline() внутри Python/initconfig.c, если он предоставлен, применяется для установки соответствующего значения флаг командной строки.

Это значение копируется в глобальную переменную, Py_VerboseFlag через _Py_GetGlobalVariablesAsDict().

Все значения PyConfig следуют теми же самыми последовательностями и порядком следования:

Рисунок 5-2

  Просмотр флагов времени исполнения

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

Все флаги -X доступны внутри установленного словаря sys._xoptions:

$ ./python -X dev -q

>>> import sys
>>> sys.flags
sys.flags(debug=0, inspect=0, interactive=0, optimize=0,
 dont_write_bytecode=0, no_user_site=0, no_site=0,
 ignore_environment=0, verbose=0, bytes_warning=0,
 quiet=1, hash_randomization=1, isolated=0,
 dev_mode=True, utf8_mode=0)

>>> sys._xoptions
{'dev': True}

Сборка конфигурации

Помимо значений конфигурации времени исполнения в Include/cpython/initconfig.h, также имеются встроенная конфигурация, располагающаяся в pyconfig.h в корне папки. Этот файл динамически создаётся на этапе ./configure в процессе сборки для macS и Linux, или через build.bat в Windows.

Вы можете обнаружить эту встроенную конфигурацию, выполнив следующее:

$ ./python -m sysconfig

Platform: "macosx-10.15-x86_64"
Python version: "3.9"
Current installation scheme: "posix_prefix"

Paths:
    data = "/usr/local"
    include = "/Users/anthonyshaw/CLionProjects/cpython/Include"
    platinclude = "/Users/anthonyshaw/CLionProjects/cpython"
...

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

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

Сборка модуля из входных данных

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

Локальные файлы и пакеты

Потоки ввода/ вывода, например, stdin или конвейер в памяти

Строки

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

Рисунок 5-3

Благодаря такой гибкости большая часть исходного кода CPython посвящена обработке входных данных в своём синтаксическом анализаторе CPython.

  Относящиеся к делу файлы исходного кода

Имеются четыре основных файла, с которыми имеет дело интерфейс командной строки:

Таблица 5-2. Относящиеся к взаимодействию с командной строкой файлы

Файл
Назначение

Lib/runpy.py

Модуль стандартной библиотеки для импорта модулей Python и их исполнения

Modules/main.c

Обёртывающие исполнение внешнего кода, например, из файла, модуля или входного потока функции

Programs/python.c

Точка входа для исполняемого python в Windows, Linux и macOS. Служит только в качестве оболочки для Modules/main.c

Python/pythonrun.c

Обёртывающие внутренние API C функции для обработки входных данных из командой строки

  Считывание файлов/ входных данных

После того как CPythonобладает конфигурацией времени исполнения и необходимыми аргументами командной строки, он может загружать код, который ему надлежит выполнять. Эта задача обрабатывается pymain_main() из Modules/main.c.

CPython теперь выполнит предоставленный код со всеми предписанными параметрами во вновь созданном экземпляре PyConfig.

  Ввод строки из приглашения на ввод команд

CPython способен выполнять небольшие приложения Python из командной строки с параметром -c. Например, рассмотрим что произойдёт когда вы выполните print(2 ** 2):

$ ./python -c "print(2 ** 2)"

4

Прежде всего, внутри Modules/main.c выполняется pymain_run_command(), беря ту команду, которая передаётся в -c в качестве аргумента в C с типом wchar_t*.

[Замечание]
Замечание

Тип wchar_t* часто используется для типа хранения нижнего уровня под данные Unicode по всему CPython, так как размер этого типа способен хранить символы UTF-8.

При преобразовании значения wchar_t* в некую строку Python, файл Objects unicodeobject.c обладает вспомогательной функцией, PyUnicode_FromWideChar(), которая и возвращает строку Unicode. Её кодирование в UTF-8 далее производится посредством PyUnicode_AsUTF8String().

Строки Unicode Python подробно обсуждаются в разделе Тип строки Unicode Главы 11, Объекты и типы.

После того как это осуществлено, pymain_run_command() передаёт Python объект байт в PyRun_SimpleStringFlags() для исполнения.

PyRun_SimpleStringFlags() является частью Python/pythonrun.c. Его цель состоит в повороте строки в модуль Python с последующей отправкой её на выполнение.

Модулю Python требуется некая точка входа, __main__, для его обособленного исполнения и PyRun_SimpleStringFlags() создаёт её в неявном виде.

После того как PyRun_SimpleStringFlags() создал модуль и некий словарь, он вызывает PyRun_StringFlags(). PyRun_StringFlags() создаёт поддельное имя файла и затем вызывает синтаксический анализатор Python для создания дерева абстрактного синтаксиса (AST, abstract syntax tree) из этой строки и возвращает некий модуль. В нашей следующей главе вы получите дополнительные сведения относительно AST.

[Замечание]
Замечание

Модули Python это определённые структуры данных, применяемые для удобства синтаксического анализа кода в самом компиляторе. Структурой C для модуля Python выступает mod_ty и она определена в Include/Python-ast.h.

  Просмотр флагов времени исполнения

Другой способ выполнения команд Python состоит в применении параметра -m с названием некого модуля. Типичным примером является python -m unittest, который запускает модуль unittest в стандартной библиотеке.

Такая возможность исполнения модулей в качестве сценариев изначально предлагалась в PEP 338. Окончательный стандарт для относительных импортов в явном виде был определён в PEP 366.

Флаг -m подразумевает, что внутри соответствующего пакета модуля вы желаете выполнить что- то внутри соответствующей точки входа (__main__). Это также подразумевает, что вы хотите отыскать sys.path для модуля с этим названием.

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

CPython импортирует стандартный библиотечный модуль, runpy и выполняет его при помощи и PyObject_Call(). Такой импорт выполняется при помощи API функции Import_ImportModule() C, обнаруживаемой внутри файла Python/import.c.

[Замечание]
Замечание

В Python, если у вас имеется некий объект и вы желаете получить атрибут, вы можете вызвать getattr(). В соответствующем API C этим вызовом является PyObject_GetAttrString(), который обнаруживается в Objects/object.c.

Если вы желаете вызвать вызывавшего, тогда вы можете заключить его в круглые скобки, или же можете вызывать свойство __call__() для любого объекта Python. __call__() реализуется внутри Objects/object.c.

>>> my_str = "hello, world"
>>> my_str.upper()
'HELLO, WORLD'
>>> my_str.upper.__call__()
'HELLO, WORLD'

Этот модуль runpy написан на чистом Python и располагается в Lib/runpy.py.

Выполнение python -m <module> эквивалентно python -m runpy <module>. Модуль runpy был создан для абстрагирования самого процесса от местоположения и выполнения модулей в некой операционной системе.

Модуль runpy для запуска своего целевого модуля выполняет три основных момента:

Вызывает _import__() для того модуля, название которого вы предоставили

Устанавливает __name__ (название вашего модуля) в пространство имён с названием __main__

Выполняет данный модуль внутри соответствующего пространства имён __main__

Модуль runpy также поддерживает исполнение каталогов и файлов ZIP.

  Ввод данных из файла сценария или стандартного ввода

Когда самый первый аргумент для python является названием файла, например, python test.py, тогда CPython откроет некий файловый дескриптор и передаст его обработку в PyRun_SimpleFileExFlags() внутри файла Python/pythonrun.c.

Существует три пути, которые может предпринять эта функция:

Когда путь к файлу это файл .pyc, тогда она вызовет run_pyc_file()

Если путём к файлу выступает некий файл сценария .py, тогда она запускает Главе 2,

Если файловым путём является stdin по той причине, что пользователь запустил <command> | python, тогда он трактует stdin как некий дескриптор файла и запускает PyRun_FileExFlags()

Для stdin и базовых файлов сценариев CPython будет передавать файловый дескриптор в PyRun_FileExFlags(), расположенный в файле Python/pythonrun.c.

Основное назначение PyRun_FileExFlags() аналогично PyRun_SimpleStringFlags(). CPython будет загружать полученный файловый дескриптор в PyParser_ASTFromFileObject().

Идентично PyRun_SimpleStringFlags(), после того как PyRun_FileExFlags() создал некий модуль Python из соответствующего файла, он отправляет этот модуль в run_mod() на исполнение.

  Просмотр флагов времени исполнения

Когда некий пользователь запускает python с каким- то путём к файлу .pyc, тогда вместо загрузки такого файла как обычного текстового файла и его синтаксического анализа, CPython будет предполагать что такой файл .pyc содержит некий кодовый объект, записанный на диск.

В PyRun_SimpleFileExFlags() имеется некое выражение для того чтобы соответствующий пользователь предоставлял некий путь файла для файла .pyc.

run_pyc_file() внутри Python pythonrun.c выстраивает (маршализует) соответствующий кодовый объект из своего файла .pyc применяя некий файловый дескриптор.

Такая структура кодового объекта на диске является тем способом, коим компилятор CPython кэширует скомпилированный код с тем, чтобы не выполнять его синтаксический анализ всякий раз, когда вызывается данный сценарий.

[Замечание]
Замечание

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

После того как соответствующий кодовый объект был выстроен в оперативной памяти, он отправляется в run_eval_code_obj(), который вызывает Python/ceval.c для выполнения этого кода.

Выводы

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

Гибкость Python с входными данными превращает его в великолепный инструмент для некого диапазона приложений, таких как:

Утилиты командной строки

Сетевые приложения с длительным временем работы, например, веб серверы

Коротких, компактных сценариев

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

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

Соответствующие свойства времени компиляции, обнаруживаемые в системной конфигурации могут разниться между дистрибутивами Python. Например, Python 3.8, выгруженный с Python.org под macOS имеет другие значения по умолчанию, нежели тот дистрибутив Python 3.8, который обнаруживается в Homebrew или в дистрибутиве Anaconda.

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

Глава 11. Объекты и типы

Содержание

Глава 11. Объекты и типы

Примеры в этой главе

Встроенные типы

Объекты и типы переменных объектов

Тип type

Слоты типов

Работа с типами в C

Словари свойств типов

Типы bool и long

Тип long

Пример

Тип строки Unicode

Относящиеся к делу исходные файлы

Обработка позиций кода Unicode

Сопоставление UTF-8 и UTF-16

Совместимость с ASCII

Тип широкого символа

Маркеры порядка байт

Пакет encodings

Модуль Codecs

Реализации Codec

Внутренние Codecs

Пример

Тип словаря

Хэширование

Относящиеся к делу исходные файлы

Структура словаря

Нахождение

Выводы

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

Например, чтобы создать некий новый список, вы просто вызываете list():

lst = list()

Или же вы можете воспользоваться квадратными скобками:

lst = []

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

Все типы в Python наследуются из object, некого встроенного базового типа. Даже строки, кортежи и списки наследуются из object.

В Objects/object.c вся базовая реализация самого типа object написана на чистом коде C. Существует несколько конкретных реализаций базовой логики, таких как поверхностные сопоставления.

Вы можете представлять себе объект Python как состоящий из двух вещей:

Модели данных ядра с указателями на скомпилированные функции

Некий словарь со всеми индивидуальными атрибутами и методами

Большинство составляющей базового API объекта объявляется в Objects/object.c, например, реализация встроенной функции repr() , PyObject_Repr. Вы также обнаружите PyObject_Hash() и прочие API.

Все эти функции функции могут перекрываться в неком персональном объекте через реализацию dunder methods в неком объекте Python:

class MyObject(object):
    def __init__(self, id, name):
        self.id = id
        self.name = name

    def __repr__(self):
        return "<{0} id={1}>".format(self.name, self.id)

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

[Совет]
Смотри также

Одним из наилучших ресурсов для модели данных Pyton выступает Fluent Python, 2nd Edition, by Luciano Ramalho.

Примеры в этой главе

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

Если вы ещё не осилили те подобные замены, которые подробно изложены в главах по грамматике и компилятору CPython, тогда несомненно вернитесь обратно и сделайте это, прежде чем продолжить. Это необходимо для реализации всех приводимых ниже примеров.

Встроенные типы

Модель данных ядра определена в PyTypeObject, а основные функции определяются в Objects/typeobject.c.

Каждый из исходных файлов обладает соответствующим заголовком в Include. Например, Objects/rangeobject.c имеет файл заголовка Include/rangeobject.h.

Вот список исходных файлов и соответствующие им типы:

Таблица 11-1. Исходные файлы и их типы

Исходный файл
Тип

Objects/object.c

Встроенные методы и базовый объект

Objects/boolobject.c

Тип bool

Objects/bytearrayobject.c

Тип byte[]

Objects/bytesobject.c

Тип bytes

Objects/cellobject.c

Тип cell

Objects/classobject.c

Абстракция типа class, применяемая в мета- программировании

Objects/codeobject.c

Встроенный тип объекта code

Objects/complexobject.c

Тип комплексного числа

Objects/iterobject.c

Тип итератора

Objects/listobject.c

Тип list

Objects/longobject.c

Численный тип long

Objects/memoryobject.c

Тип базовой памяти

Objects/methodobject.c

Тип метода класса

Objects/moduleobject.c

Тип модуль

Objects/namespaceobject.c

Тип пространства имён

Objects/odictobject.c

Тип упорядоченного словаря

Objects/rangeobject.c

Тип генертора диапазона

Objects/setobject.c

Тип set

Objects/sliceobject.c

Тип ссылки на долю

Objects/structseq.c

Тип struct.Struct

Objects/tupleobject.c

Тип tuple

Objects/typeobject.c

Тип type

Objects/unicodeobject.c

Тип str

Objects/weakrefobject.c

Тип weakref

В этой главе мы изучим некоторые из этих типов.

Объекты и типы переменных объектов

Поскольку C не является объектно- ориентированным языком в отличии от Python, объекты в C не наследуются один из другого. PyObject выступает изначальным сегментом данных для каждого объекта Python, а PyObject * представляет ссылку на него.

При определении типов Python собственно typedef применяет один из следующих макросов:

PyObject_HEAD (PyObject) для простого типа

PyObject_VAR_HEAD (PyVarObject) для типа контейнера

Самый простой тип PyObject обладает следующими полями:

Таблица 11-2. Поля PyObject

Поле
Тип
Назначение

ob_refcnt

Py_ssize_t

Счётчик ссылок на экземпляр

ob_type

_typeobject*

Тип объекта

Например, cellobject определяет одно дополнительное поле, ob_ref и эти базовые поля:

typedef struct {
    PyObject_HEAD
    PyObject *ob_ref;       /* Content of the cell or NULL when empty */
} PyCellObject;

Тип переменной, PyVarObject, расширяет этот тип PyObject и также обладает следующими полями:

Таблица 11-3. Поля PyVarObject

Поле
Тип
Назначение

ob_base

PyObject

Базовый тип

ob_size

Py_ssize_t

Число содержащихся в нём элементов

Например, тип int, PyLongObject, обладает следующим описанием:

struct _longobject {
    PyObject_VAR_HEAD
    digit ob_digit[1];
}; /* PyLongObject */

Тип type

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

>>> t = type("hello")
>>> t
<class 'str'>

Результатом type() выступает некий экземпляр PyTypeObject

>>> type(t)
<class 'type'>

Объекты типа используются для определения самой реализации абстракции базовых классов.

Например, объекты всегда реализуют метод __repr__():

>>> class example:
... x = 1
>>> i = example()
>>> repr(i)
'<__main__.example object at 0x10b418100>'

Реализация __repr__() всегда пребывает по тому же самому адресу в самом типе определения всякого объекта. Эта позиция именуется слотом типа.

Слоты типов

Все слоты типов определены в Include/cpython/object.h.

Каждый слот типа обладает неким названием свойства и функцией подписи. Функция __repr__(), например, носит название tp_repr и обладает подписью reprfunc:

struct PyTypeObject
---
typedef struct _typeobject {
    ...
    reprfunc tp_repr;
    ...
} PyTypeObject;

Такая подпись reprfunc определена в Include/cpython/object.h как обладающая единственным аргументом PyObject* (self):

typedef PyObject *(*reprfunc)(PyObject *);

В качестве примера, cellobject реализует слот tp_repr при помощи функции cell_repr:

PyTypeObject PyCell_Type = {
    PyVarObject_HEAD_INIT(&PyType_Type, 0)
    "cell",
    sizeof(PyCellObject),
    0,
    (destructor)cell_dealloc,                   /* tp_dealloc */
    0,                                          /* tp_vectorcall_offset */
    0,                                          /* tp_getattr */
    0,                                          /* tp_setattr */
    0,                                          /* tp_as_async */
    (reprfunc)cell_repr,                        /* tp_repr */
    ...
};

Помимо базовых слотов типов PyTypeObject, обозначаемых префиксом tp_, имеются прочие типы определений слотов:

Таблица 11-4. Определения типов слотов

Тип слота
Префикс

PyNumberMethods

nb_

PySequenceMethods

sq_

PyMappingMethods

mp_

PyAsyncMethods

am_

PyBufferProcs

bf_

Все слоты типов обладают заданным уникальным номером, определяемым в Include/typeslots.h. При ссылке на слот типа некого объекта, или же при его выборке, вы обязаны пользоваться этими константами.

К примеру, tp_repr обладает положением константы 66, эта константа Py_tp_repr всегда ставит в соответствие положение данного слота типа. Эти константы полезны при проверке того, что некий объект реализует определённую функцию слота типа.

Работа с типами в C

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

Например, если вы запускаете x[n] в неком объекте с индексацией, таком как список или строка, тогда это вызовет PyObject_GetItem(), который просмотрит соответствующий объект x чтобы определить как его индексировать, строка 146 Objects/abstract.c:

PyObject *
PyObject_GetItem(PyObject *o, PyObject *key)
{
    PyMappingMethods *m;
    PySequenceMethods *ms;
...

PyObject_GetItem() обслуживает как типы сопоставления, такие как словари, так и типы последовательностей, такие как списки и кортежи.

Когда соответствующий экземпляр, o, обладает методами последовательности, тогда o->ob_type->tp_as_sequence будет вычисляться истинным. К тому же, если этот экземпляр обладает определённой функцией слота sq_item, тогда предполагается, что он обладает верно реализованным протоколом своей последовательности.

Само значение key вычисляется для проверки что это целое и данный элемент запрашивается из соответствующего объекта последовательности при помощи PyObject_GetItem():

    ms = o->ob_type->tp_as_sequence;
    if (ms && ms->sq_item) {
     if (PyIndex_Check(key)) {
            Py_ssize_t key_value;
            key_value = PyNumber_AsSsize_t(key, PyExc_IndexError);
            if (key_value == -1 && PyErr_Occurred())
                return NULL;
            return PySequence_GetItem(o, key_value);
        }
        else {
            return type_error("sequence index must "
                              "be integer, not '%.200s'", key);
        }
    }

  Словари свойств типов

Python поддерживает определение новых типов при помощи ключевого слова class. Определяемые пользователем типы создаются type_new() из модуля объекта самого типа.

Определяемые пользователем типы будут обладать словарём свойств, доступным через __dict__(), причём реализуемый по умолчанию __getattr__() выполняет поиск в этом словаре свойств. Методы класса, свойства класса и свойства экземпляра, все они располагаются в этом словаре.

PyObject_GenericGetDict() реализует собственно логику извлечения такого экземпляра словаря для заданного объекта. PyObject_GetAttr() реализует устанавливаемую по умолчанию реализацию __getattr__(), а PyObject_SetAttr() реализует __setattr__().

[Совет]
Смотри также

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

Если вы желаете подробнее изучить метапрограммирование, отсылаем вас к книге Real Python Python Metaclasses.

Типы bool и long

Тип bool это самая простая реализация из всех встроенных типов. Он наследуется из long и обладает предопределёнными константами Py_True и Py_False. Эти константы являются неизменными экземплярами, создаваемые самой конкретизацией интерпретатора Python.

Внутри Objects/boolobject.c вы можете обнаружить вспомогательную функцию создания некого экземпляра bool из числа, строка 28 Objects/boolobject.c:

PyObject *PyBool_FromLong(long ok)
{
    PyObject *result;

    if (ok)
        result = Py_True;
    else
        result = Py_False;
    Py_INCREF(result);
    return result;
}

Эта функция применяет соответствующее вычисление C численного типа для назначения Py_True и Py_False результата и увеличения на единицу соответствующих счётчиков ссылок.

Реализуются численные функции для and, xor и or, однако, сложение, вычитание и деление разыменовываются из базового типа long, поскольку нет смысла с делении двух Булевых значений.

Реализация and для значения bool вначале проверяет что a и b являются Булевыми. Если это не так, они приводятся к целым и соответствующая операция and выполняется с двумя числами, строка 61 Objects/boolobject.c:

static PyObject *
bool_and(PyObject *a, PyObject *b)
{
    if (!PyBool_Check(a) || !PyBool_Check(b))
        return PyLong_Type.tp_as_number->nb_and(a, b);
    return PyBool_FromLong((a == Py_True) & (b == Py_True));
}

  Тип long

Тип long слегка сложнее чем bool. При переходе с Python 2 на Python 3, CPython отбросил поддержку для типа int и вместо него применяет тип long как самый первичный тип целого.

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

Структура данных long Python составляется из заголовка переменной PyObject и списка цифр. Этот список цифр, ob_digit, изначально устанавливается на наличие одной цифры, однако при инициализации он расширяется на большую длину, строка 85 Include/longintrepr.h:

struct _longobject {
    PyObject_VAR_HEAD
    digit ob_digit[1];
};

Например, число 1 имело бы ob_digit [1], а число 24601 обладало бы ob_digit [2, 4, 6, 0, 1].

Для нового long память выделяется посредством _PyLong_New(). Эта функция получает фиксированные длины и проверяет что они меньше чем MAX_LONG_DIGITS. Затем она перераспределяет необходимую память для ob_digit чтобы соответствовать необходимой длине.

Для преобразования типа long C в тип long Python, этот long C конвертируется в список цифр, выделяется память под соответствующий long Python и затем устанавливаются все цифры.

Для чисел с единственной цифрой, наш объект long инициализируется с ob_digit уже имеющей длину 1, затем его значение устанавливается без выделения дополнительной памяти, строка 297 Objects/longobject.c:

PyObject *
PyLong_FromLong(long ival)
{
    PyLongObject *v;
    unsigned long abs_ival;
    unsigned long t;  /* unsigned so >> doesn't propagate sign bit */
    int ndigits = 0;
    int sign;

    CHECK_SMALL_INT(ival);
...
    /* Fast path for single-digit ints */
    if (!(abs_ival >> PyLong_SHIFT)) {
        v = _PyLong_New(1);
        if (v) {
            Py_SIZE(v) = sign;
            v->ob_digit[0] = Py_SAFE_DOWNCAST(
                abs_ival, unsigned long, digit);
        }
        return (PyObject*)v;
    }
...
    /* Larger numbers: loop to determine number of digits */
    t = abs_ival;
    while (t) {
        ++ndigits;
        t >>= PyLong_SHIFT;
    }
    v = _PyLong_New(ndigits);
    if (v != NULL) {
        digit *p = v->ob_digit;
        Py_SIZE(v) = ndigits*sign;
        t = abs_ival;
        while (t) {
            *p++ = Py_SAFE_DOWNCAST(
                t & PyLong_MASK, unsigned long, digit);
            t >>= PyLong_SHIFT;
        }
    }
    return (PyObject *)v;
}

Для преобразования чисел двойной точности с плавающей запятой в long Python, PyLong_FromDouble() выполняет для вас необходимую математику.

Остаток реализации функций в Objects/longobject.c обладают утилитами, такими как преобразование строки Unicode в число при помощи PyLong_FromUnicodeObject().

  Пример

Всё богатство сравнений слота типов для long установлено в long_richcompare(). Эта функция обёртывает long_compare():

static PyObject *
long_richcompare(PyObject *self, PyObject *other, int op)
{
    Py_ssize_t result;
    CHECK_BINOP(self, other);
    if (self == other)
        result = 0;
    else
        result = long_compare((PyLongObject*)self, (PyLongObject*)other);
    Py_RETURN_RICHCOMPARE(result, 0, op);
}

long_compare() вначале проверяет значение длины (число цифр) двух переменных a и b. Если длины равны, тогда она проходит в цикле по всем цифрам на предмет равны ли они друг другу.

long_compare() возвращает одно из трёх типов значений:

Когда a < b, она возвращает отрицательное число.

Когда a == b, она возвращает ноль.

Когда a > b, она возвращает положительное число.

Например, когда вы вычисляете 1 == 5, result равен -4. Для 5 == 1 получается 4.

Вы можете реализовать следующий блок кода прежде чем макро Py_RETURN_RICHCOMPARE возвратит True при результате абсолютного значения <=1. Применяется соответствующий макро Py_ABS(), который возвращает абсолютное значение целого со знаком:

    if (op == Py_AlE) {
        if (Py_ABS(result) <= 1)
            Py_RETURN_TRUE;
        else
            Py_RETURN_FALSE;
    }
    Py_RETURN_RICHCOMPARE(result, 0, op);

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

>>> 2 == 1
False
>>> 2 ~= 1
True
>>> 2 ~= 10
False

Тип строки Unicode

Строки Python Unicode сложны. Кросс-платформенные типы Unicode сложны на любой платформе.

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

Строковый тип Python 2 хранился в C с применением типа char. Однобайтовый тип char в достаточной степени хранит всякий из символов ASCII (American Standard Code for Information Interchange) и применялся в программировании компьютеров с 1970-х.

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

Для решения этих проблем была введена консорциумом Unicode в 1991 году стандартная система кодирования и базы данных символов, носящая название стандарта Unicode. Современный стандарт Unicode содержит символы для всех языков с написанием, а также расширенные глифы и символы.

База данных символов Unicode (UCD, Unicode Character Database ) содержит 143 859 именованных символов в версии 13.0, по сравнению с всего лишь 128 в ASCII. Стандарт Unicode определяет эти символы в некой таблице символов с названием UCS (Universal Character Set, универсальный набор символов). Каждый символ обладает уникальным идентификатором, носящим название точки кодирования (code point).

Существует множество различных кодировок, которые пользуются стандартом Unicode и преобразовывают точки кодирования в двоичные значения.

Строки Unicode Python поддерживают три длины кодировок:

1- байтовую (8- бит)

2- байтовую (16- бит)

4- байтовую (32- бита)

Эти кодировки переменной длины внутри имеющейся реализации обладают следующими отражениями:

1- байтовый Py_UCS1, хранимый как 8- битовый тип int без знака uint8_t

2- байтовый Py_UCS2, хранимый как 16- битовый тип int без знака uint16_t

4- байтовый Py_UCS4, хранимый как 32- битовый тип int без знака uint32_t

  Относящиеся к делу исходные файлы

Вот те исходные файлы, которые относятся к строкам:

Таблица 11-5. Относящиеся к строкам исходные файлы

Файл
Назначение

Include/unicodeobject.h

Определение объекта строки Unicode

Include/cpython/unicodeobject.h

Определение объекта строки Unicode

Objects/unicodeobject.c

Реализация объекта строки Unicode

Lib/encodings

Пакет encodings, содержащий все возможные кодировки

Lib/codecs.py

Модуль кодеков

Modules/_codecsmodule.c

Модуль расширений C кодеков, реализация специфичных для ОС кодировок

Modules/_codecs

Реализации кодеков для диапазона альтернативных кодировок

  Обработка позиций кода Unicode

CPython не содержит копию собственно UCD (базы данных символов Unicode), также ему не приходится обновлять всякий раз сценарии и символы, добавляемые в сам стандарт Unicode.

Строки Unicode в CPython заботятся лишь о кодировании. Сами операционные системы обрабатывают основную задачу представления позиций кода в своём правильном сценарии.

Сам стандарт Unicode содержит UCD и регулярно обновляет её новыми сценариями, эмодзи и символами. Операционные системы берут эти обновления Unicode и обновляют своё программное обеспечение через исправления. Такие исправления содержат новые позиции кода UCD и поддерживают различные кодировки Unicode. Сама UCD расщепляется на разделы с названием кодовых блоков.

Таблицы кода Unicode публикуются на вебсайте Unicode.

Другим моментом поддержки Unicode выступает веб браузер. Веб браузеры декодируют двоичные данные HTML в соответствии с преобразованием кодировки заголовка HTTP. Когда вы работаете с CPython в качестве веб сервера, тогда ваше кодирование Unicode обязано соответствовать заголовкам HTTP, подлежащим отправке вашим пользователям.

  Сопоставление UTF-8 и UTF-16

Существуют две распространённые кодировки:

UTF-8 это 8- битная кодировка символов, которая поддерживает все возможные символы в имеющейся UCD с позициями кода от 1 до 4 байт

UTF-16 это 16- битная кодировка символов, аналогичная UTF-8, но не совместимая с 7- или 8- битными кодировками, такими как ASCII.

UTF-8 это наиболее распространённая кодировка Unicode.

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

U+00F7 для символа деления ('÷')

U+0107 для Латинской строчной c с острым ударением (с акутом, 'ć')

В Python, кодовые позиции Unicode могут кодироваться непосредственно в самом коде при помощи символа экранирования \u и соответствующего шестнадцатеричного значения его позиции кодирования:

>>> print("\u0107")
ć

CPython не пытается дополнять такие данные, поэтому если вы попробуете \u10, тогда он выдаст вам следующую исключительную ситуацию:

>>> print("\u107")
 File "<stdin>", line 1
SyntaxError: (unicode error) 'unicodeescape' codec can't decode
    bytes in position 0-4: truncated \uXXXX escape

И XML, и HTML поддерживают позиции кодирования Unicode со специальным символом экранирования &#val;, где val это десятичное значение позиции кодирования. Когда вам требуется преобразовывать позиции кодирования Unicode в XML или HTML, тогда вы можете применять обработчик xmlcharrefreplace из метода .encode():

>>> "\u0107".encode('ascii', 'xmlcharrefreplace')
b'&#263;'

Его вывод будет содержать позицию кодирования, экранированную HTML или XML. Все современные браузеры будут декодировать эти экранированные последовательности в правильные символы.

  Совместимость с ASCII

Когда вы работаете с кодированным ASCII текстом, тогда важно понимать основное отличие между UTF-8 и UTF-16. UTF-8 обладает большим преимуществом совместимости с кодированным ASCII текстом. Преобразование ASCII является кодированием 7- бит.

Самые первые 128 позиций кодирования стандарта Unicode представляют собой имеющиеся в стандарте ASCII 128 символы. Например, Латинская буква "a" это 97- й символ в ASCII и 97- й символ в Unicode. Десятичные 97 эквивалентны 61 в шестнадцатеричной системе счисления, следовательно позицией кода Unicode для "a" выступает U+0061.

В своём REPL вы можете создавать значение двоичного кода для буквы "a":

>>> letter_a = b'a'
>>> letter_a.decode('utf8')
'a'

Её можно правильно декодировать в UTF-8.

UTF-16 работает с кодовыми позициями от 2 до 4 байт. Соответствующее однобайтовое представление буквы "a" не будет декодировано:

>>> letter_a.decode('utf16')
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
UnicodeDecodeError: 'utf-16-le' codec can't decode
    byte 0x61 in position 0: truncated data

На это важно обращать внимание при выборе механизма кодирования. UTF-8 является более безопасным вариантом, когда вам требуется импортировать кодированные ASCII данные.

  Тип широкого символа

Когда вы обрабатываете входную строку Unicode с неизвестной кодировкой при помощи исходного кода CPython, тогда будет применяться тип C wchar_t.

wchar_t это стандарт C для строк с широкими символами и его достаточно для хранения в памяти строк Unicode. После PEP 393 именно тип wchar_t был выбран в качестве формата хранения Unicode. Сам объект строки Unicode предоставляет PyUnicode_FromWideChar(), функцию утилиты, которая будет преобразовывать константу wchar_t в объект строки.

Например, pymain_run_command(), применяемая python -c, преобразует значение аргумента -c в строку Unicode, строка 226 Modules/main.c:

static int
pymain_run_command(wchar_t *command, PyCompilerFlags *cf)
{
    PyObject *unicode, *bytes;
    int ret;

    unicode = PyUnicode_FromWideChar(command, -1);

  Маркеры порядка байт

При декодирования неких входных данных, таких как файл, CPython определяет значение порядка байт из маркера порядка байт (BOM, byte order mark). BOM это особые символы, которые появляются в самом начале потока байт Unicode. Они сообщают получателю в каком именно порядке сохранены их данные.

Различные вычислительные системы могут выполнять кодирование с различными порядками байт. Если вы воспользуетесь неверным порядком байт, даже при верном кодировании, тогда ваши данные будут искажёнными. Обратный (big-endian, "тупоголовый") порядок размещает наиболее значимый байт первым. Прямой (little-endian, "остроконечный") порядок помещает первым наименее значимый байт.

Спецификация UTF-8 поддерживает BOM, однако он не оказывает воздействия. UTF-8 BOM может появляться в самом начале последовательности кодированных данных, в представлении b'\xef\xbb\xbf' и будет указывать CPython что его поток данных, скорее всего, UTF-8. UTF-16 и UTF-32 поддерживают прямой и обратный BOM.

Значение по умолчанию порядка байт в CPython устанавливается глобальным значением sys.byteorder:

>>> import sys; print(sys.byteorder)
little

  Пакет encodings

Пакет encodings из Lib/encodings поставляется с более чем сотней встроенных поддерживаемых кодировок для CPython. Всякий раз когда вызывается метод .encode() или .decode() из строки или байтовой строки, соответствующая кодировка отыскивается в этом пакете.

Каждая кодировка определяется как некий отдельный модуль. Например, ISO2022_JP это широко применяемая кодировка для Японских систем электронной почты и она определяется в Lib/encodings/iso2022_jp.py.

Все модули кодировок будут определять функцию getregentry() и регистрировать следующие характеристики:

Её уникальное название

Её функции кодирования и декодирования из модуля кодека

Её классы приращения на единицу кодера и декодера

Её классы считывания из потока и записи в поток

Многие модули кодирования совместно используют одни и те же кодеки либо из модуля codecs, либо из модуля _mulitbytecodec. Неоторые модули кодировок применяют отдельный модуль кодека на C из Modules/cjkcodecs.

Например, модуль кодирования ISO2022_JP импортирует некий модуль расширения C, _codecs_iso2022.c, из Modules/cjkcodecs/_codecs_iso2022.c:

import _codecs_iso2022, codecs
import _multibytecodec as mbc

codec = _codecs_iso2022.getcodec('iso2022_jp')

class Codec(codecs.Codec):
    encode = codec.encode
    decode = codec.decode

class IncrementalEncoder(mbc.MultibyteIncrementalEncoder,
                         codecs.IncrementalEncoder):
    codec = codec

class IncrementalDecoder(mbc.MultibyteIncrementalDecoder,
                         codecs.IncrementalDecoder):
    codec = codec

Пакет encodings также обладает модулем Lib/encodings/aliases.py, который содержит некий словарь aliases. Этот словарь применяется для установки соответствия в реестре альтернативными названиями. К примеру, utf8, utf-8 и u8 вся являются псевдонимами кодировки utf_8.

  Модуль Codecs

Модуль codecs обрабатывает необходимую трансляцию данных со специфической кодировкой. Соответствующие функции кодирования и декодирования определённой кодировки могут достигаться при помощи getencoder() и getdecoder(), соответственно:

>>> iso2022_jp_encoder = codecs.getencoder('iso2022_jp')
>>> iso2022_jp_encoder('\u3072\u3068')  # hi-to
(b'\x1b$B$R$H\x1b(B', 2)

Соответствующая функция кодирования вернёт двоичный результат и значение числа байт в выводе в качестве кортежа. codecs также реализует встроенную функцию open() для открытия обработчиков файла из своей операционной системы.

  Реализации Codec

Реализация объекта Unicode (Objects/unicodeobject.c) содержит следующие модули кодировок:

Таблица 11-6. Методы кодировок объекта Unicode

Кодек
Кодировщик

ascii

PyUnicode_EncodeASCII()

latin1

PyUnicode_EncodeLatin1()

UTF7

PyUnicode_EncodeUTF7()

UTF8

PyUnicode_EncodeUTF8()

UTF16

PyUnicode_EncodeUTF16()

UTF32

PyUnicode_EncodeUTF32()

unicode_escape

PyUnicode_EncodeUnicodeEscape()

raw_unicode_escape

PyUnicode_EncodeRawUnicodeEscape()

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

Реализация прочих методов кодирования располагается внутри Modules/_codecs чтобы не загромождать реализацию основного объекта строк Unicode. Кодеки unicode_escape и raw_unicode_escape являются внутренними для CPython.

  Внутренние Codecs

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

Для любого входного и выходного текста могут использоваться такие кодировки текста:

Таблица 11-7. Кодеки CPython

Кодек
Назначение

idna

Реализует RFC 3490

mbcs

Кодирует в соответствии с кодовой страницей ANSI (только Windows)

raw_unicode_escape

Преобразовывает в строки для сырых литералов в исходном коде Python

string_escape

Преобразовывает в строчные литералы для исходного кода Python

undefined

Пробует установленную по умолчанию в системе кодировку

unicode_escape

Преобразовывает в литералы Unicode для исходного кода Python

unicode_internal

Возвращает внутреннее представление CPython

Также имеются некоторые кодировки только в двоичном виде, которые требуются для применения с codecs.encode() и codecs.decode() для входных байтовых строк, например таких:

>>> codecs.encode(b'hello world', 'base64')
b'aGVsbG8gd29ybGQ=\n'

Вот перечень таких кодировок только в двоичном представлении:

Таблица 11-8. Кодеки CPython исключительно для двоичного представления

Кодек
Псевдонимы
Назначение

base64_codec

base64, base-64

Преобразовывает в MIME base64

bz2_codec

bz2

Сжимает строку при помощи bz2

hex_codec

hex

Преобразовывает в шестнадцатеричное представление с двумя цифрами на байт

quopri_codec

quoted-printable

Преобразовывает в операнды в MIME в кавычках для вывода на печать

rot_13

rot13

Возвращает кодирование шифром Цезаря (сдвиг на 13 позиций)

uu_codec

uu

Преобразовывает с применением uuencode

zlib_codec

zip, zlib

Сжимает при помощи gzip

  Пример

Наш слот типа tp_richcompare выделен для PyUnicode_RichCompare() в PyUnicode_Type. Эта функция выполняет сравнение строк и может быть приспособлена для применения нашего оператора ~=. Это поведение вы реализуете в нечувствительном к регистру сравнению двух строк.

Прежде всего, добавим некое предложение варианта для проверки того когда левая и правая строки эквивалентны в двоичном виде, строка 11361 Objects/unicodeobject.c:

PyObject *
PyUnicode_RichCompare(PyObject *left, PyObject *right, int op)
{
    ...
    if (left == right) {
        switch (op) {
        case Py_EQ:
        case Py_LE:
>>>     case Py_AlE:
        case Py_GE:
            /* a string is equal to itself */
            Py_RETURN_TRUE;

Затем добавим новый блок else if для обработки своего оператора Py_AlE. Это осуществят такие наши действия:

Преобразовать левую строку в некую новую строку в верхнем регистре

Преобразовать правую строку в некую новую строку в верхнем регистре

Сравнить эти две строки

Разыменовать эти обе временные строки с тем чтобы вернуть память

Вернуть полученный результат

Ваш код должен выглядеть как- то так:

    else if (op == Py_EQ || op == Py_NE) {
        ...
    }
    /* Add these lines */
    else if (op == Py_AlE){
        PyObject* upper_left = case_operation(left, do_upper);
        PyObject* upper_right = case_operation(right, do_upper);
        result = unicode_compare_eq(upper_left, upper_right);
        Py_DECREF(upper_left);
        Py_DECREF(upper_right);
        return PyBool_FromLong(result);
    }

После выполнения вами повторной компиляции, ваше не чувствительное к регистру сопоставление строк должно в вашем REPL выдавать такие результаты:

>>> "hello" ~= "HEllO"
True
>>> "hello?" ~= "hello"
False

Тип словаря

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

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

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

  Хэширование

Все неизменные встроенные типы предоставляют некую функцию хэширования. Она определена в слоте типа tp_hash, или, для настраиваемых (индивидуальных) типов при помощи магического метода __hash__(). Хэш- значения имеют тот же размер что и указатель (64 бита для 64- битных систем, 32 бита для 32- битных систем), однако они не представляют своими величинами значения адреса в памяти.

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

>>> "hello".__hash__() == ("hel" + "lo").__hash__()
True

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

Некоторые хэш- значения просты, например, longs Python:

>>> (401).__hash__()
401

Для длинных значений хэширование long выдаётся более сложным:

>>> (401123124389798989898).__hash__()
2212283795829936375

Многие встроенные типы пользуются модулем Python/pyhash.c , который предоставляет следующие вспомогательные функции хэширования:

Байты: _Py_HashBytes(const void*, Py_ssize_t)

Числа двойной точности: _Py_HashDouble(double)

Указатели: _Py_HashPointer(void*)

Строки Unicode, к примеру, применяют для хэширования байтовых данных своих строк _Py_HashBytes():

>>> ("hello").__hash__()
4894421526362833592

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

class User:
    def __init__(self, id: int, name: str, address: str):
        self._id = id

    def __hash__(self):
        return hash(self._id)

    @property
    def id(self):
        return self._id

Экземпляры этого класса теперь можно хэшировать:

>>> bob = User(123884, "Bob Smith", "Townsville, QLD")
>>> hash(bob)
123884

Этот экземпляр теперь можно применять в качестве некого ключа словаря:

>>> sally = User(123823, "Sally Smith", "Cairns, QLD")
>>> near_reef = {bob: False, sally: True}
>>> near_reef[bob]
False

Множества снизят дублирование хэш- значений этого экземпляра:

>>>  {bob, bob}
{<__main__.User object at 0x10df244b0>}

  Относящиеся к делу исходные файлы

Вот те исходные файлы, которые относятся к словарям:

Таблица 11-9. Относящиеся к словарям исходные файлы

Файл
Назначение

Include/dictobject.h

Определение API объекта словаря

Include/cpython/dictobject.h

Определение типов объекта словаря

Objects/dictobject.c

Реализация объекта словаря

Objects/dict-common.h

Определение записи ключа и объектов ключа

Python/pyhash.c

Внутренний алгоритм хэширования

  Структура словаря

Некий объект словаря, PyDictObject составляется из таких элементов:

Свойств словаря объекта, содержащих его размер, тег версии и сами ключи и значения

Объект таблицы ключей словаря, PyDictKeysObject, содержащий сами ключи и хэш- значения всех записей

Рисунок 11-1

Наш PyDictObject обладает следующими свойствами:

Таблица 11-10. Свойства PyDictObject

Поле
Тип
Назначение

ma_keys

PyDictKeysObject*

Объект таблицы ключей словаря

ma_used

Py_ssize_t

Число содержащихся в этом словаре

ma_values

PyObject**

Не обязательный массив значений (см. Замечание)

ma_version_tag

uint64_t

Номер версии этого словаря

[Замечание]
Замечание

Словари способны обладать одним из двух состояний: расщеплённом или комбинированном. Когда словари комбинированы, соответствующие указатели на значении этого словаря хранятся в объектах их ключей.

Когда словарь расщеплён, его значения хранятся в неком отдельном свойстве, ma_values, в качестве таблицы значений PyObject*.

Наша таблица ключей словаря PyDictKeysObject обладает следующими свойствами:

Таблица 11-11. Свойства PyDictKeysObject

Поле
Тип
Назначение

dk_entries

PyDictKeyEntry[]

Размещение массива записей ключей словаря

dk_indices

char[]

Таблица хэш- значений и соответствий для dk_entries

dk_lookup

dict_lookup_func

Функция обнаружения (см. следующий раздел)

dk_nentries

Py_ssize_t

Значения числа используемых записей в общей таблице записей

dk_refcnt

Py_ssize_t

Счётчик ссылок

dk_size

Py_ssize_t

Значения размера самой таблицы хэширования

dk_usable

Py_ssize_t

Общее число используемых записей в общей таблице записей, при изменении размера словаря O

Запись ключа словаря PyDictKeyEntry обладает следующими свойствами:

O

Таблица 11-12. Свойства PyDictKeyEntry

Поле
Тип
Назначение

me_hash

Py_ssize_t

Кэшированный хэш код me_key

me_key

PyObject*

Указатель на сам объект ключа

me_value

PyObject*

Указатель на значение объекта

  Нахождение

Для заданного объекта ключа имеется некая общая функция поиска: lookdict().

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

Значение адреса памяти этого ключа имеется в общей таблице ключей.

Хэш- значение его объекта имеется в общей таблице ключей.

Сам ключ не имеется в его словаре.

[Совет]
Смотри также

Сама функция обнаружения основывается на знаменитой книге Дональда Кнута The Art of Computer Programming Смотрите Главу 6, раздел 4 по хэшиованию.

Вот сама последовательность поиска:

Получить хэш значение для ob.

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

Если ix пустой, тогда вернуть DKIX_EMPTY (не найден).

Получить запись ключа, ep, для определённого значения индекса.

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

Если эти хэш значения ключа совпадают по причине самого объекта, ob, разрешить то же самое значение хэша как ep->me_mash, затем вернуть полученный результат.

[Замечание]
Замечание

lookupdict() является одной из нескольких функций hot исходного кода CPython:

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

Документация GCC, “Common Function Attributes”.

Это особенность компиляторов GCC, однако при компиляции с помощью Проводимой профилем оптимизации, эта функция, скорее всего, будет автоматически оптимизирована компилятором.

Выводы

Теперь, когда вы рассмотрели реализацию встроенных типов, вы готовы изучать всё остальное.

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

Некоторые библиотеки обладают типами, написанными на C вместо наследования из имеющихся встроенных типов. Одним из примеров выступает NumPy, библиотека для числовых массивов. Её тип nparray написан на C и обладает высокими эффективностью и производительностью.

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

Глава 6. Лексический и синтаксический разбор при помощи синтаксических деревьев

Содержание

Глава 6. Лексический и синтаксический разбор при помощи синтаксических деревьев

Выработка реального синтаксического дерева

CPython Parser-Tokenizer

Относящиеся к делу файлы

Ввод данных в синтаксический анализатор из файла

Поток синтаксического анализатора и механизма разбора лексем

Абстрактные синтаксические деревья

Относящиеся к делу файлы

Применение Instaviz для представления деревьев абстрактного синтаксиса

Компиляция деревьев абстрактного синтаксиса

Важные термины для запоминания

Пример: Добавление оператора сравнения почти-равенства

Выводы

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

Этот этап носит название синтаксического разбора (parsing):

Рисунок 6-1

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

Для синтаксического анализа в CPython имеются две структуры, а именно, concrete syntax tree (CST, дерево реального синтаксиса) и abstract syntax tree (AST, абстрактное синтаксическое дерево)

Рисунок 6-2

Процесс синтаксического разбора обладает двумя частями:

Создание дерева реального синтаксиса при помощи синтаксического анализатора лексем (parser-tokenizer) или лексического анализатора (lexer)

Создание абстрактного синтаксического дерева из реального синтаксического дерева с применением синтаксического разбора (parser)

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

Выработка реального синтаксического дерева

Дерево реального синтаксиса (CST, concrete syntax tree), порой именуемое parse tree (деревом синтаксического анализа) является некой упорядоченной, направленной структурой, которая представляет код в контекстно- независимой (контекстно- свободной) грамматике.

CST создаётся из tokenizer (механизма определения лексем - токенов, символов) и parser (синтаксического анализатора). Генератор синтаксического анализатора вы изучили в Главе 4, Язык и грамматика Python. Выводом генератора синтаксического анализа является таблица синтаксического анализа DFA (deterministic finite automaton, конечно- детерменированного автомата), описывающего все возможные состояния контекстно- независимой грамматики.

[Совет]
Смотри также

Первоначальный автор Python, Гвидо ван Россум, разработал некую контекстную грамматику для применения в CPython 3.9, в качестве альтернативы LL(1), грамматике, применявшейся в предварительных версиях CPython. Эта новая грамматика носит название PEG (parser expression grammar, грамматика выражений синтаксического анализа).

Такой синтаксический анализатор PEG стал доступным в Python 3.9. В Python 3.10 старая грамматика LL(1) будет удалена окончательно.

В Главе 4, Язык и грамматика Python вы изучили некоторые типы выражений, например, if_stmt и with_stmt. Наше реальное синтаксическое дерево (CST) представляет подобные if_stmt символы, как ветвление с лексемами и терминальными символами в качестве листовых узлов.

К примеру, наше арифместическое выражение a + 1 превращается в такое реальное синтаксическое дерево (CST):

Рисунок 6-3

Некое арифметическое выражение представляется здесь с тремя основными ветвями: левой ветвью, ветвью оператора и правой ветвью.

Наш синтаксический анализатор проходит в итерациях по лексемам из некого потока на входе и устанавливает их соответствие относительно всех возможных состояний и лексем в своей грамматике для построеня реального синтаксического дерева (CST).

Все отражённые в нашем показаном выше реальном синтаксическом дереве символы определены в Grammar/Grammar:

arith_expr: term (('+'|'-') term)*
term: factor (('*'|'@'|'/'|'%'|'//') factor)*
factor: ('+'|'-'|'~') factor | power
power: atom_expr ['**' factor]
atom_expr: [AWAIT] atom trailer*
atom: ('(' [yield_expr|testlist_comp] ')' |
       '[' [testlist_comp] ']' |
       '{' [dictorsetmaker] '}' |
       NAME | NUMBER | STRING+ | '...' | 'None' | 'True' | 'False')

Все лексемы определены в Grammar/Tokens:

ENDMARKER
NAME
NUMBER
STRING
NEWLINE
INDENT
DEDENT

LPAR                    '('
RPAR                    ')'
LSQB                    '['
RSQB                    ']'
COLON                   ':'
COMMA                   ','
SEMI                    ';'
PLUS                    '+'
MINUS                   '-'
STAR                    '*'
...

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

Например, если вы попытаетесь определить некую функцию с названием 1, тогда Python возбудит некую SyntaxError:

>>> def 1():
  File "<stdin>", line 1
    def 1():
        ^
SyntaxError: invalid syntax

Токен NAME это определённый тип лексемы для представления одного из множества численных выражений Python. Python обладает особой грамматикой для чисел, включающей следующее:Code

Восьмеричные значения, такие как 0o20

Шестнадцателичные значения, такие как 0x10

Двоичные значения, такие как 0b10000

Комплексные числа, такие как 10j

Числа с плавающей запятой, такие как 1.01

С подчёркиванием в качестве разделителя, такие как 1_000_000

Вы можете увидеть скомпилированные символы и лексемы при помощи модулей symbol и token в Python:

$ ./python
>>> import symbol
>>> dir(symbol)
['__builtins__', '__cached__', '__doc__', '__file__', '__loader__',
 '__name__', '__package__', '__spec__', '_main', '_name', '_value',
 'and_expr', 'and_test', 'annassign', 'arglist', 'argument',
 'arith_expr', 'assert_stmt', 'async_funcdef', 'async_stmt',
 'atom', 'atom_expr',
...
>>> import token
>>> dir(token)
['AMPER', 'AMPEREQUAL', 'AT', 'ATEQUAL', 'CIRCUMFLEX',
 'CIRCUMFLEXEQUAL', 'COLON', 'COMMA', 'COMMENT', 'DEDENT', 'DOT',
 'DOUBLESLASH', 'DOUBLESLASHEQUAL', 'DOUBLESTAR', 'DOUBLESTAREQUAL',
...

CPython Parser-Tokenizer

Языки программирования обладают различными реализациями лексических анализаторов. Некоторые используют генераторы лексического анализа в качестве дополнения к генератору синтаксического анализа.

CPython обладает модулем синтаксического анализа и разбиения на лексемы (parser-tokenizer), написанном на C.

  Относящиеся к делу файлы

Вот связанные с модулем синтаксического анализа и разбиения на лексемы:

Таблица 6-1. Относящиеся к parser-tokenizer исходные файлы

Файл
Назначение

Python/pythonrun.c

Исполняет синтаксический анализ и компиляцию из входных данных

Parser/parsetok.c

Реализация синтаксического анализа и разбиения на лексемы

Parser/tokenizer.c

Реализация разбиения на лексемы

Parser/tokenizer.h

Файл заголовков для реализация разбиения на лексемы, который описывает такие модели данных как состояние лексемы

Include/token.h

Определение типов лексем, вырабатываемое Tools/scripts/generate_token.py

Include/node.h

Взаимодействие и макросы узла дерева синтаксического анализа для разбиения на лексемы

  Ввод данных в синтаксический анализатор из файла

Точка входа для синтаксического анализа и разбиения на лексемы, PyParser_ASTFromFileObject(), получает дескриптор файла, флаги компиляции и некий экземпляр PyArena, после чего преобразует соответствующий файловый объект в модуль.

Имеются два этапа:

Преобразование в реальное синтаксическое дерево (CRT) при помощи PyParser_ParseFileObject().

Преобразование в абстрактное синтаксическое дерево (AST) или модуль посредством функции AST PyAST_FromNodeObject().

Функция PyParser_ParseFileObject() обладает двумя важными задачами:

Создание экземпляра состояния разбиения на лексемы, tok_state, при помощи PyTokenizer_FromFile()

Преобразование лексем в реальное синтаксическое дерево (CST, список узлов) посредством parsetok()

  Поток синтаксического анализатора и механизма разбора лексем

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

Перед исполнением сам синтаксический анализатор и механизм разбиения на лексемы устанавливает tok_state, временную структуру данных для хранения всех состояний, вырабатываемых разбиением на лексемы. Конкретное состояние разбиения на лексемы содержит такие сведения, как текущие положения курсора и строки.

Для получения следующей лексемы синтаксический анализатор - механизм разбиения на лексемы вызывает tok_get(). Синтаксический анализатор - механизм разбиения на лексемы передаёт значение идентификатора результирующей лексемы в свой синтаксический анализатор, который использует конечно- детерминированный автомат (DFA) генератора синтаксического анализа для создания некого узла в соответствующем дереве реального синтаксиса.

tok_get() является одной из наиболее сложных функцмй во всём базовом коде CPython. Она обладает более чем 640 строками и содержит десятки наследований с конечными вариантами, новыми функциональными возможностями языка, а также синтаксисисом.

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

Рисунок 6-4

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

Значение типа узла определяется в Include/node.h:

typedef struct _node {
    short               n_type;
    char                *n_str;
    int                 n_lineno;
    int                 n_col_offset;
    int                 n_nchildren;
    struct _node        *n_child;
    int                 n_end_lineno;
    int                 n_end_col_offset;
} node;

Поскольку дерево реального синтаксиса (CST) является деревом синтаксиса, идентификаторами лексем и символами, его компилятору было бы трудно принимать быстрые решения на основе Python.

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

Его вывод будет в численном виде, с применением номеров лексем и символов , выработанных на этапе make regen-grammar и хранимых в Include/token.h:

>>> from pprint import pprint
>>> import parser
>>> st = parser.expr('a + 1')
>>> pprint(parser.st2list(st))
[258,
 [332,
  [306,
   [310,
    [311,
     [312,
      [313,
       [316,
        [317,
         [318,
          [319,
           [320,
            [321, [322, [323, [324, [325, [1, 'a']]]]]],
            [14, '+'],
            [321, [322, [323, [324, [325, [2, '1']]]]]]]]]]]]]]]]],
 [4, ''],
 [0, '']]

Чтобы быстрее с этим разобраться, вы можете взять все эти числа из своих модулей symbol и token, поместить их в некий словарь и рекурсивно заменять эти значения в своём выводе parser.st2list() значениями имён соответствующих лексем, cpython-book-samples/21/lex.py:

import symbol
import token
import parser

def lex(expression):
    symbols = {v: k for k, v in symbol.__dict__.items()
               if isinstance(v, int)}
    tokens = {v: k for k, v in token.__dict__.items()
              if isinstance(v, int)}
    lexicon = {**symbols, **tokens}
    st = parser.expr(expression)
    st_list = parser.st2list(st)

    def replace(l: list):
        r = []
        for i in l:
            if isinstance(i, list):
                r.append(replace(i))
            else:
                if i in lexicon:
                    r.append(lexicon[i])
                else:
                    r.append(i)
        return r

    return replace(st_list)

Чтобы представить как это выглядит в дереве синтаксического анализа, вы можете выполнить lex() с простым выражением, скажем, a + 1:

>>> from pprint import pprint
>>> pprint(lex('a + 1'))

['eval_input',
 ['testlist',
  ['test',
   ['or_test',
    ['and_test',
     ['not_test',
      ['comparison',
       ['expr',
        ['xor_expr',
         ['and_expr',
          ['shift_expr',
           ['arith_expr',
            ['term',
             ['factor', ['power', ['atom_expr', ['atom',
['NAME', 'a']]]]]],
            ['PLUS', '+'],
            ['term',
             ['factor',
              ['power', ['atom_expr', ['atom', ['NUMBER',
'1']]]]]]]]]]]]]]]]],
 ['NEWLINE', ''],
 ['ENDMARKER', '']]

В данном выводе вы можете наблюдать имеющиеся символы в нижнем регистре, например, 'arith_expr', а значения лексем в верхнем регистре, такие как 'NUMBER'.

Абстрактные синтаксические деревья

Следующий этап в интерпретаторе CPython состоит в преобразовании выработанное синтаксическим анализатором реальное дерево синтаксиса (CST) в нечто более логичное, что можно исполнить.

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

Прежде чем компилировать код, реальное дерево синтаксиса (CST) надлежит преобразовать в структуру более высокого уровня, которая представляет реальные конструкции Python. Такая структура является представлением CST, носящим название абстрактного синтаксического дерева (AST, abstract syntax tree).

В качестве примера рассмотрим бинарную операцию в AST с названием BinOp и определяемую как некий тип операции. Она обладает тремя компонентами:

left: Левая часть данной операции

op: сам оператор, например, +, - или *

right: правая часть операции

Соответствующее дерево абстрактного синтаксиса (AST) a + 1 может быть представлено так:

Рисунок 6-5

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

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

  Относящиеся к делу файлы

Ниже приводятся исходные файлы, связанные с деревьями абстрактного синтаксиса:

Таблица 6-2. Относящиеся к AST исходные файлы

Файл
Назначение

Include/Python-ast.h

Определение типов AST, вырабатываемое Parser/asdl_c.py

Parser/Python.asdl

Список типов и свойств узлов в специфичным для этой области языке, ASDL 5

Python/ast.c

Реализация AST

  Применение Instaviz для представления деревьев абстрактного синтаксиса

Instaviz это пакет Python, написанный для его применения в этой книге. Он отображает AST и скомпилированный код в неком веб- интерфейсе.

Для установки Instaviz, выполните её из pip для пакета instaviz:

$ pip install instaviz

Затем откройте REPL, запустив python в своей командной строке без аргументов.

Наша функция instaviz.show() получает отдельный аргумент с типом code object. Вы рассмотрите объект кода в нашей следующей главе. Для данного примера определим некую функцию и воспользуемся названием этой функции в качестве значения своего аргумента:

$ python
>>> import instaviz
>>> def example():
       a = 1
       b = a + 1
       return b

>>> instaviz.show(example)

Вы обнаружите в своей командной строке что был запущен веб сервер с портом 8080. Если вы используете этот порт, вы можете изменить его, вызвав instaviz.show (например, port=9090) или с иным номером порта.

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

Рисунок 6-6

График внизу слева это та функция, которую вы определили в своём REPL, представленное в качестве дерева абстрактного синтаксиса. Каждый узел в этом дереве представлен неким типом AST. Их все можно найти в модуле ast и унаследовать из _ast.AST.

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

Например, если вы кликните по своему узлу Assign в центре, тогда он будет связан со строкой b = a + 1:

Рисунок 6-7

Этот узел Assign обладает двумя свойствами:

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

value это подлежащее назначению значение, которое в данном случае является оператором BinOp, а именно a + 1.

Когда вы кликните по оператору BinOp, будут отражены относящиеся к нему свойства:

left находящийся слева от этого оператора узел

op собственно оператор, в нашем случае узел является оператором Add (+) для сложения.

right находящийся справа от этого оператора узел

Рисунок 6-8

  Компиляция деревьев абстрактного синтаксиса

Компиляция AST в C является непростой задачей. Соответствующий модуль Python ast.c имеет более 5 000 строк кода.

Существует несколько точек входа, формирующих общедоступную часть API дерева абстрактного синтаксиса (AST). Этот API AST получает некий узел дерева (реального синтаксиса, CST), название файла, значения флагов компиляции и область хранения в памяти.

Получаемым в результате типом является mod_ty, представляющий модуль Python, определяемый в Include Python-ast.h.

mod_ty является структурой контейнера для одного из четырёх типов в Python:

Module

Interactive

Expression

FunctionType

Все эти типы перечислены в Parser/Python.asdl. Вы обнаружите значение типов модулей, типов операторов (предложений), типов выражений, операций (операторов) и понимание того, что все они определены в этом файле.

Значения названий типов в Parser/Python.asdl соотносятся с теми классами, которые выработаны деревом абстрактного типа (AST) и с теми же самыми классами, получившими свои названия в модуле ast стандартной библиотеки:

-- ASDL's 4 builtin types are:
-- identifier, int, string, constant

module Python
{
    mod = Module(stmt* body, type_ignore *type_ignores)
        | Interactive(stmt* body)
        | Expression(expr body)
        | FunctionType(expr* argtypes, expr returns)

Модуль ast импортирует Include Python-ast.h, файл, автономно создаваемый из Parser/Python.asdl при повторной генерации грамматики. Все параметры и названия в Include Python-ast.h напрямую коррелируют с теми, что определены в Parser/Python.asdl.

Тип mod_ty вырабатывается в Include Python-ast.h из определения Module в Parser/Python.asdl:

enum _mod_kind {Module_kind=1, Interactive_kind=2, Expression_kind=3,
                 FunctionType_kind=4};
struct _mod {
    enum _mod_kind kind;
    union {
        struct {
            asdl_seq *body;
            asdl_seq *type_ignores;
        } Module;

        struct {
            asdl_seq *body;
        } Interactive;

        struct {
            expr_ty body;
        } Expression;

        struct {
            asdl_seq *argtypes;
            expr_ty returns;
        } FunctionType;

    } v;
};

Соответствующие файл заголовка и структур C применяются для того, чтобы программа Python/ast.c могла быстро выработать необходимые структуры с указателями на относящиеся к делу данные.

Сама точка входа AST, PyAST_FromNodeObject(), по существу, некий оператор switch по результатам от TYPE(n). TYPE() это макро, применяемое AST для определения значения типа узлов в дереве реального синтаксиса. Результатом этого TYPE() будет либо тип некого символа, либо лексемы.

При запуске в корневом узле он может быть только одним из типов модуля, определённого как Module, Interactive, Expression или FunctionType:

Для file_input значением типа должен быть Module.

Для eval_input, такого как из REPL, значением типа должен быть Expression

Для каждого типа оператора в Python/ast.c существует соответственная функция C ast_for_xxx, которая будет смотреть за узлами реального синтаксического дерева для завершения свойств такого оператора.

Одним из простейших примеров является оператор возведения в степень, например, 2 ** 4, или 2 в 4 степени. ast_for_power() возвратит некую BinOp с оператором Pow (возведение в степень), причём с левой стороны как e (2), а с правой стороны как f (4), строка 2717 Python/ast.c :

static expr_ty
ast_for_power(struct compiling *c, const node *n)
{
    /* power: atom trailer* ('**' factor)*
     */
    expr_ty e;
    REQ(n, power);
    e = ast_for_atom_expr(c, CHILD(n, 0));
    if (!e)
        return NULL;
    if (NCH(n) == 1)
        return e;
    if (TYPE(CHILD(n, NCH(n) - 1)) == factor) {
        expr_ty f = ast_for_expr(c, CHILD(n, NCH(n) - 1));
        if (!f)
            return NULL;
        e = BinOp(e, Pow, f, LINENO(n), n->n_col_offset,
                  n->n_end_lineno, n->n_end_col_offset, c->c_arena);
    }
    return e;
}

Вы можете увидеть результаты этого, отправив короткую функцию в модуль instaviz:

>>> def foo():
       2**4
>>> import instaviz
>>> instaviz.show(foo)

Рисунок 6-9

Вы также можете увидеть соответствующие свойства в своём интерфейсе пользователя:

Рисунок 6-10

Итак, суммируя, всякий тип оператора и выражения обладают соответствующей функцией ast_for_*() для их создания. Их аргументы определены в Parser/Python.asdl и выставляются через модуль ast в стандартной библиотеке.

Если некое выражение или оператор (предложение) обладают потомками, тогда они вызывают соответствующую дочернюю функцию ast_for_*() при обходе вглубь.

Важные термины для запоминания

Ниже приводятся некоторые ключевые для данной главы термины:

Abstract syntax tree (AST, дерево абстрактного синтаксиса): Некое контекстное дерево представления грамматики и синтаксиса Python

Concrete syntax tree (CST, дерево реального синтаксиса): Дерево представления лексем и символов без контекста

Parse tree (Дерево синтаксического анализа): Другой термин обозначения дерева реального синтаксиса

Token: Лексема, некий тип символа, например, +

Tokenization (разбор лексем): Процесс преобразования текста в лексемы

Parsing (синтаксический разбор): Процесс преобразования текста в CST или AST

Пример: Добавление оператора сравнения почти-равенства

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

Некое выражение сравнения сопоставляет два или более значений:

>>> a = 1
>>> b = 2
>>> a == b
False

Применяемые в выражениях сравнения операторы носят название операторов сравненияю Вот некоторые из тех, с которыми вы знакомы:

Меньше чем: <

Больше чем: >

Равенство: ==

Не равенство: !=

[Совет]
Смотри также

Для Python 2.1 в PEP 207 в его модели данных были предложены богатые сравнения. Этот PEP содержал контекст, историю и выравнивание для индивидуальных типов Python для реализации методов сравнения.

Теперь давайте добавим другой оператор сравнения с названием почти равны, который будет представляться как ~=. Он будет обладать следующим поведением:

Если вы сравниваете число с плавающей запятой и целое, тогда он выполнит приведение числа с плавающей запятой к целому и проведёт сравнение с целым для получения результата.

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

В REPL этот новый оператор должен возвращать следующее:

>>> 1 ~= 1
True
>>> 1 ~= 1.0
True
>>> 1 ~= 1.01
True
>>> 1 ~= 1.9
False

Для добавления нового оператора вам вначале следует обновить свою грамматику CPython. В Grammar/python.gram операторы сравнения определяются как символ comp_op:

comparison[expr_ty]:
    | a=bitwise_or b=compare_op_bitwise_or_pair+ ...
    | bitwise_or
compare_op_bitwise_or_pair[CmpopExprPair*]:
    | eq_bitwise_or
    | noteq_bitwise_or
    | lte_bitwise_or
    | lt_bitwise_or
    | gte_bitwise_or
    | gt_bitwise_or
    | notin_bitwise_or
    | in_bitwise_or
    | isnot_bitwise_or
    | is_bitwise_or
eq_bitwise_or[CmpopExprPair*]: '==' a=bitwise_or ...
noteq_bitwise_or[CmpopExprPair*]:
    | (tok='!=' {_PyPegen_check_barry_as_flufl(p) ? NULL : tok}) ...
lte_bitwise_or[CmpopExprPair*]: '<=' a=bitwise_or ...
lt_bitwise_or[CmpopExprPair*]: '<' a=bitwise_or ...
gte_bitwise_or[CmpopExprPair*]: '>=' a=bitwise_or ...
gt_bitwise_or[CmpopExprPair*]: '>' a=bitwise_or ...
notin_bitwise_or[CmpopExprPair*]: 'not' 'in' a=bitwise_or ...
in_bitwise_or[CmpopExprPair*]: 'in' a=bitwise_or ...
isnot_bitwise_or[CmpopExprPair*]: 'is' 'not' a=bitwise_or ...
is_bitwise_or[CmpopExprPair*]: 'is' a=bitwise_or ...

Изменим выражение compare_op_bitwise_or_pair чтобы разрешить новую пару ale_bitwise_or:

compare_op_bitwise_or_pair[CmpopExprPair*]:
    | eq_bitwise_or
...
    | ale_bitwise_or

Определим новое выражение ale_bitwise_or под уже имеющимся выражением is_bitwise_or:

...
is_bitwise_or[CmpopExprPair*]: 'is' a=bitwise_or ...
ale_bitwise_or[CmpopExprPair*]: '~=' a=bitwise_or
    { _PyPegen_cmpop_expr_pair(p, AlE, a) }

Этот новый тип определяет именованное выражение, ale_bitwise_or, которое содержит терминальный символ '~='.

Этот вызов функции _PyPegen_cmpop_expr_pair(p, AlE, a) является выражением для получения из дерева абстрактного синтаксиса (AST) узла cmpop. Его типом является AlE для Almost Equal.

Затем добавьте в Grammar/Tokens лексему:

ATEQUAL                 '@='
RARROW                  '->'
ELLIPSIS                '...'
COLONEQUAL              ':='
# Add this line
ALMOSTEQUAL             '~='

Для обновления имеющейся грамматики и лексем в C вам требуется повторно сгенерировать соответствующие заголовки.

В macOS или Linux воспользуйтесь следующей командой:

$ make regen-token regen-pegen

В Windows, находясь в каталоге PCBuild воспользуйтесь такой командой:

> build.bat --regen

Эти шаги автоматически обновят ваш механизм разбора лексем. Например, откройте исходный код Parser/token.c и вы обнаружите как изменился выбор (case) в функции PyToken_TwoChars():

    case '~':
        switch (c2) {
        case '=': return ALMOSTEQUAL;
        }
        break;
    }

Когда вы повторно скомпилируете CPython на этом этапе и откроете REPL, тогда вы обнаружите, что ваш механизм распознования лексем успешно распознает эту лексему, однако дерево абстрактного синтаксиса не знает как его обрабатывать:

$ ./python
>>> 1 ~= 2
SystemError: invalid comp_op: ~=

Эта исключительная ситуация возбуждена ast_for_comp_op() внутри Python/ast.c потому как она не распознала ALMOSTEQUAL как допустимый оператор для предложения сравнения.

Compare это тип выражения, определённый в Parser/Python.asdl. Он обладает свойствами для своего левого выражения; список операторов с названием ops, а также список выражений для сравнения с названием comparators:

Compare(expr left, cmpop* ops, expr* comparators)

Внутри определения Compare имеется справка для перечисления cmpop:

cmpop = Eq | NotEq | Lt | LtE | Gt | GtE | Is | IsNot | In | NotIn

Это перечень возможных листьевых узлов AST, которые способны действовать как операторы сравнения. Мы пропустили свою и нужно её добавить. Обновите этот список вариантов, чтобы он включал и новую, AlE:

cmpop = Eq | NotEq | Lt | LtE | Gt | GtE | Is | IsNot | In | NotIn | AlE

Затем снова повторно сгенерируйте дерево абстрактного синтаксиса чтобы обновить файлы заголовков AST:

$ make regen-ast

Это обновит перечисление нашего оператора сравнения (_cmpop) внутри Include/Python-ast.h, чтобы он включал и возможность AlE:

typedef enum _cmpop { Eq=1, NotEq=2, Lt=3, LtE=4, Gt=5, GtE=6, Is=7,
                      IsNot=8, In=9, NotIn=10, AlE=11 } cmpop_ty;

Наше дерево абстрактного синтаксиса не знает что лексема ALMOSTEQUAL эквивалентна оператору сравнения AlE. Поэтому нам необходимо обновить свой код C для AST.

Переместитесь к ast_for_comp_op() из Python/ast.c. Отыщите предложение переключателя для лексем этого оператора. Это вернёт вам одно из перечисляемых значений _cmpop.

Добавьте две строки чтобы отлавливать лексему ALMOSTEQUAL и возвращать оператор сравнения AlE, строка 1222 в Python/ast.c:

static cmpop_ty
ast_for_comp_op(struct compiling *c, const node *n)
{
    /* comp_op: '≪'|'>'|'=='|'>='|'≪='|'!='|'in'|'not' 'in'|'is'
               |'is' 'not'
    */
    REQ(n, comp_op);
    if (NCH(n) == 1) {
        n = CHILD(n, 0);
        switch (TYPE(n)) {
            case LESS:
                return Lt;
            case GREATER:
                return Gt;
            case ALMOSTEQUAL: // Add this line to catch the token
                return AlE;    // And this one to return the AST node

На этом этапе наши механизм разбора лексем и его дерево абстрактного синтаксиса способны выполнять синтаксический анализ данного кода, однако наш компилятор не может знать как обрабатывать такой оператор. Чтобы проверить представительство нашего AST воспользуйтесь ast.parse() и изучите первый оператор в полученном выражении:

>>> import ast
>>> m = ast.parse('1 ~= 2')
>>> m.body[0].value.ops[0]
<_ast.AlE object at 0x10a8d7ee0>

Это некий экземпляр нашего типа оператора AlE, а потому наше дерево абстрактного синтаксиса правильно провело синтаксический разбор кода.

В своей следующей главе вы изучите как работает компилятор CPython и повторно посетите наш оператор почти- равно для окончательной сборки его поведения.

Выводы

Многосторонность и выполнение API нижнего уровня CPythonпревращает его в идеального претендента на встроенный механизм сценариев. Вы обнаружите, что CPython применяется во множестве приложений пользовательского интерфейса, например, в проектировании игр, 3D графике, и автоматизации систем.

Его процесс интерпретации гибкий и действенный. Теперь, когда вы понимаете то, как он работает, вы готовы разобраться с компилятором.

Глава 7. Собственно компилятор

Содержание

Глава 7. Собственно компилятор

Относящиеся к делу исходные файлы

Важные термины

Создание экземпляра компилятора

Флаги фьючерсов и флаги компилятора

Флаги фьючерсов

Справка по флагам фьючерсов для 3.9

Флаги компилятора

Символьные таблицы

Относящиеся к делу исходные файлы

Структура данных символьной таблицы

Применение стандартного библиотечного модуля symtable

Реализация символьной таблицы

Ядро процесса компиляции

Доступ к компилятору из Python

API компилятора C

Инструкции

Тип инструкции

Инструкции безусловного перехода

Блоки базового кадра

Операции и аргументы

Сборка

Структура данных ассемблера

Алгоритм ассемблера поиска в глубину

API C ассемблера

Поиск в глубину

Создание объекта кода

Применение Instaviz для отображения объекта кода

Пример: Реализация оператора почти-равенства

Выводы

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

Основное задание компилятора состоит в преобразовании имеющегося дерева абстрактного синтаксиса в те инструкции, которые способно понимать ЦПУ:

Рисунок 7-1

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

Компилятор: Проходит по дереву абстрактного синтаксиса и создаёт CFG (control сow graph), граф управления потоком, который представляет логическую последовательность для исполнения

Ассемблер: Преобразует все узлы CFG в последовательные исполняемые предложения, носящие название байтового кода (bytecode)

Вот визуальное представление процесса компиляции:

Рисунок 7-2

[Замечание]
Важно

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

В этой главе вы сосредоточитесь на компиляции дерева абстрактного синтаксиса модуля в кодовый объект.

PyAST_CompileObject() является основной точкой входа для компилятора CPython. Она берёт дерево абстрактного синтаксиса модуля в качестве своего первичного аргумента, совместно с названием его файла, а также всеми теми глобальными, локальными и PyArena, созданными ранее в процессе интерпретации.

[Замечание]
Замечание

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

Относящиеся к делу исходные файлы

Вот те исходные файлы, которые относятся к компилятору:

Таблица 7-1. Относящиеся к компилятору исходные файлы

Файл
Назначение

Python/compile.c

Реалиация компилятора

Include/compile.h

Определение API и типов компилятора

Важные термины

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

Значение состояния компилятора реализуется в виде некого типа контейнера, который содержит одну символическую таблицу.

Эта символическая таблица содержит множество имён переменных и необязательно может содержать дочерние символические таблицы

Определённый тип компилятора содержит большое число элеменов компилятора.

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

Элемент компилятора содержит большое число блоков базовых кадров

Блоки базовых кадров содержат множество инструкций байтового кода

Контейнер состояния компилятора и его составляющие могут быть проиллюстрированы следующим образом:

Рисунок 7-3

Создание экземпляра компилятора

Прежде чем начать, создаётся некое глобальное состояние компилятора. Такая структура состояния компилятора (с типом compiler) содержит применяемые компилятором свойства, такие как флаги компилятора, стек и PyArena. Она также содержит ссылки на иные структуры данных, например, символьную таблицу.

Вот основные поля в состоянии компилятора:

Таблица 7-2. Основные поля состояния компилятора

Поле
Тип
Назначение

c_arena

PyArena *

Указатель на арену выделения памяти

c_const_cache

PyObject * (dict)

dict Python, удерживающий все константы, в том числе кортежи names

c_do_not_emit_bytecode

int

Флаг для отключения компиляции байтового кода

c_filename

PyObject * (str)

Название подлежащего компиляции файла

c_flags

PyCompilerFlags *

Унаследованные флаги компиляции, см. раздел Флаги компилятора

c_future

int

Указатель на __future__ модуля

c_interactive

int

Флаг для интерактивного режима

c_nestlevel

int

Текущий уровень вложения

c_optimize

PyObject * (dict)

Уровень оптимизации

c_st

symtable *

Символьная таблица компилятора

c_stack

PyObject * (list)

Список Python удерживающий указатели compiler_unit

u

compiler_unit*

Состояние компилятора для текущего блока

Конкретный экземпляр состояния компилятора создаётся внутри PyAST_CompileObject():

Если не обладает свойством строки документирования (__doc__), тогда здесь создаётся пустое, как и в случае со свойством __annotations__

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

Арена выделения памяти для компилятора устанавливается в ту, которая используется самим компилятором. За дополнительными сведениями относительно выделения памяти обратитесь к разделу Персональные распределители доменов Главы 9, Управление памятью.

Все последующие флаги настраиваются перед компиляцией кода.

Флаги фьючерсов и флаги компилятора

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

В самом состоянии компилятора, которое содержит переменные среды и флаги командной строки

Внутри своего исходного кода модулей посредством использования предложений __future__

Для получения дополнительных сведений относительно состояния конфигурации обратитесь к разделу Состояние конфигурации в Главе 5, Конфигурация и входные данные.

  Флаги фьючерсов

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

from __future__ import annotations

Код данного предложения может применять подсказки ещё не разрешённого типа, поэтому требуется данное предложение __future__. В противном случае этот модуль невозможно было бы импортировать.

  Справка по флагам фьючерсов для 3.9

Начиная с версии 3.9 все флаги, за исключением двух (annotations и barry_as_FLUFL) являются обязательными и разрешены автоматически:

Таблица 7-3. Флаги фьючерсов для 3.9

Импорт
Назначение

absolute_import

Включает абсолютное импортирование (PEP 328)

annotations

Отложить оценку аннотаций типов (PEP 563)

barry_as_FLUFL

Вложить Пасхальное яйцо (PEP 401)

division

Применять оператор истинного деления (PEP 238)

generator_stop

Разрешить StopIteration внутри генераторов (PEP 479)

generators

Ввести простые генераторы (PEP 255)

nested_scopes

Добавить статически вложенные области действия (PEP 227)

print_function

Превратить print в функцию (PEP 3105)

unicode_literals

Превратить литералы str в Unicode вместо byte (PEP 3112)

annotations

Разрешить предложение with (PEP 343)

[Замечание]
Замечание

Основная масса флагов __future__ были применены с целью переносимости между Python 2 и Python 3. Что касается подходов Python 4, вы можете обнаружить ещё большее число добавленных флагов.

  Флаги компилятора

Флаги компилятора специфичны для установленного окружения, поэтому они способны изменять сам ход исполнения кода или тот способ, которым выполняется компилятор, однако они не должны соединяться к определённому источнику, подобно тому как это выполняют предложения __future__.

Одним из примеров флага компиляции мог бы служить -O flag для оптимизации использования предложений assert. Этот флаг отключает все предложения assert, которые могли бы быть помещены в код для целей отладки. Его также можно разрешить при помощи настройки переменной среды PYTHONOPTIMIZE=1.

Символьные таблицы

Прежде чем компилировать код, API PySymtable_BuildObject() создаёт символьную таблицу.

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

  Относящиеся к делу исходные файлы

Вот те исходные файлы, которые относсятся к символьным таблицам:

Таблица 7-4. Относящиеся к компилятору исходные файлы

Файл
Назначение

Python/symtable.c

Реализация символьной таблицы

Include/symtable.h

Определение API и типов символьной таблицы

Lib/symtable.py

Модуль стандартной библиотеки symtable

  Структура данных символьной таблицы

Сама структура symtable должна быть одним из экземпляров symtable для компилятора, поэтому существенным становится пространство имён.

К примеру, если вы создаёте метод с названием resolve_names() в одном классе и объявляете другой метод с тем же самым названием в другом классе, тогда вы желали бы знать какой именно метод вы вызываете внутри их модуля.

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

Структура такой символьной таблицы, symtable, обладает следующими полями:

Таблица 7-5. Поля символьной таблицы

Поле
Тип
Назначение

recursion_depth

int

Текущая глубина рекурсии

recursion_limit

int

Предел рекурсии до возбуждения RecursionError

st_blocks

PyObject * (dict)

Соответствие адресов узлов дерева абстрактного синтаксиса записям символьной таблицы

st_cur

_symtable_entry

Текущая запись символьной таблицы

st_filename

PyObject * (str)

Название подлежащего компиляции файла

st_future

PyFutureFeatures

Фьючерсные функциональные возможности модуля, оказывающие влияние на эту символьную таблицу

st_global

PyObject * (dict)

Ссылки на соответствующие символы из st_top

st_nblocks

int

Значение числа задействованных блоков

st_private

PyObject * (str)

Название текущего класса (необязательно)

st_stack

PyObject * (list)

Стек сведений о пространствах имён

st_top

_symtable_entry

Запись символьной таблицы для данного модуля

  Применение стандартного библиотечного модуля symtable

Некоторые API C символьных таблиц выставляются в Python через модуль symtable из стандартной библиотеки.

При помощи другого модуля с названием tabulate (доступного через PyPI) вы можете создать сценарий для вывода на печать символьной таблицы.

Символьные таблицы могут быть вложенными, поэтому если некий модуль содержит функцию или класс, он также будет обладать и символьной таблицей.

Создайте сценарий с названием symviz.py, содержащий рекурсивную функцию show() , cpython-book-samples/30/symviz.py:

import tabulate
import symtable

code = """
def calc_pow(a, b):
    return a ** b
a = 1
b = 2
c = calc_pow(a,b)
"""

_st = symtable.symtable(code, "example.py", "exec")

def show(table):
    print("Symtable {0} ({1})".format(table.get_name(),
                                      table.get_type()))
    print(
        tabulate.tabulate(
            [
                (
                    symbol.get_name(),
                    symbol.is_global(),
                    symbol.is_local(),
                    symbol.get_namespaces(),
                )
                for symbol in table.get_symbols()
            ],
            headers=["name", "global", "local", "namespaces"],
            tablefmt="grid",
        )
    )
    if table.has_children():
        [show(child) for child in table.get_children()]

show(_st)

Запустите symviz.py в своей командной строке чтобы рассмотреть символьную таблицу для примера кода:

Рисунок 7-4

  Реализация символьной таблицы

Реализация символьной таблицы пребывает в Python/symtable.c и её первичным интерфейсом выступает PySymtable_BuildObject().

Аналогично рассмотренной в нашей предыдущей главе компиляции дерева абстрактного синтаксиса (AST), PySymtable_BuildObject() выполняет переключения между всеми возможными типами mod_ty (Module, Interactive, Expression и FunctionType) и посещает каждое из предложений в них.

Установленная символьная таблица рекурсивно исследует все узлы и ветви внутри своего дерева абстрактного синтаксиса (с типом mod_ty) и добавляет записи в свою symtable, строка 261 Python/symtable.c:

struct symtable *
PySymtable_BuildObject(mod_ty mod, PyObject *filename,
                       PyFutureFeatures *future)
{
    struct symtable *st = symtable_new();
    asdl_seq *seq;
    int i;
    PyThreadState *tstate;
    int recursion_limit = Py_GetRecursionLimit();
...
    st->st_top = st->st_cur;
    switch (mod->kind) {
    case Module_kind:
        seq = mod->v.Module.body;
        for (i = 0; i < asdl_seq_LEN(seq); i++)
            if (!symtable_visit_stmt(st,
                        (stmt_ty)asdl_seq_GET(seq, i)))
                goto error;
        break;
    case Expression_kind:
        ...
    case Interactive_kind:
        ...
    case FunctionType_kind:
        ...
    }
    ...
}

Для некоторого модуля PySymtable_BuildObject() обходит в цикле все предложения этого модуля и вызывает symtable_visit_stmt(), которая представляет собой гигантское предложение switch с case для каждого типа (определяемого в Parser/Python.asdl).

Каждый тип оператора обладает соответствующей функцией для разрешения символов. Например, тип предложения определения функции (FunctionDef_kind ) обладает определённой логикой для следующих действий:

Проверки текущей глубины рекурсии на предмет превышения установленного предела рекурсии

Добавляет соответствующее название функции с свою символьную таблицу с тем, чтобы она была способна вызываться в качестве объекта функции

Осуществляет разрешение определённых по умолчанию не литеральных аргументов из своей символьной таблицы

Выполняет разрешение типа аннотаций

Предпринимает разрешение декораторов функций

Наконец, symtable_enter_block() посещает сам блок с содержимым этой функции. Затем выполняется посещение и разрешение всех аргументов, а также осуществляется обход и разрешение самой функции.

[Замечание]
Замечание

Если вы всё ещё удивлены почему изменчивы установленные по умолчанию аргументы Python, основная причина заключается в symtable_visit_stmt(). Установленные по умолчанию аргументы ссылаются на соответствующую переменную из symtable.

Для изменчивости типа никакая дополнительная работа по копированию не производится.

В качестве некого предварительного просмотра вот соответствующий код C для этих этапов построения symtable для некой функции из symtable_visit_stmt(), строка 1171 в Python/symtable.c:

static int
symtable_visit_stmt(struct symtable *st, stmt_ty s)
{
    if (++st->recursion_depth > st->recursion_limit) {
        PyErr_SetString(PyExc_RecursionError,
           "maximum recursion depth exceeded during compilation");
        VISIT_QUIT(st, 0);
    }
    switch (s->kind) {
    case FunctionDef_kind:
        if (!symtable_add_def(st, s->v.FunctionDef.name, DEF_LOCAL))
            VISIT_QUIT(st, 0);
        if (s->v.FunctionDef.args->defaults)
            VISIT_SEQ(st, expr, s->v.FunctionDef.args->defaults);
        if (s->v.FunctionDef.args->kw_defaults)
            VISIT_SEQ_WITH_NULL(st, expr,
             s->v.FunctionDef.args->kw_defaults);
        if (!symtable_visit_annotations(st, s, s->v.FunctionDef.args,
                                        s->v.FunctionDef.returns))
            VISIT_QUIT(st, 0);
        if (s->v.FunctionDef.decorator_list)
            VISIT_SEQ(st, expr, s->v.FunctionDef.decorator_list);
        if (!symtable_enter_block(st, s->v.FunctionDef.name,
                                  FunctionBlock, (void *)s, s->lineno,
                                  s->col_offset))
            VISIT_QUIT(st, 0);
        VISIT(st, arguments, s->v.FunctionDef.args);
        VISIT_SEQ(st, stmt, s->v.FunctionDef.body);
        if (!symtable_exit_block(st, s))
            VISIT_QUIT(st, 0);
        break;
    case ClassDef_kind: {
        ...
    }
    case Return_kind:
        ...
    case Delete_kind:
        ...
    case Assign_kind:
        ...
    case AnnAssign_kind:

После создания результирующей символьной таблицы, она ппередаётся в компилятор.

Ядро процесса компиляции

Теперь, когда PyAST_CompileObject() обладает состоянием компиляции, symtable и модулем в виде дерева абстрактного синтаксиса (AST), можно приступать к реальной компиляции.

Ядро компилятора имеет две цели:

Преобразовать имеющееся состояние, symtable и дерево абстрактного синтаксиса в control flow graph (CFG) (граф управления потоком)

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

  Доступ к компилятору из Python

Вы можете вызвать свой компилятор в Python через вызов встроеной функции compile(). Она возвращает code object:

>>> co = compile("b+1", "test.py", mode="eval")
>>> co
<code object <module> at 0x10f222780, file "test.py", line 1>

Как и в случае с API symtable(), некое простое выражение должно обладать режимом "eval", а модуль, функция или класс иметь режимом "exec".

Собственно откомпилированный код можно обнаружить в свойстве co_code полученного кодового объекта:

>>> co.co_code
b'e\x00d\x00\x17\x00S\x00'

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

[Замечание]
Замечание

Тип Instruction в млдуле dis является отражением типа instr из API C.

Когда вы импортируете dis и придадите свойство кодового объекта co_code dis(), тогда эта функция выполнит его дизассемблирование и выведет в REPL на печать его инструкции:

>>> import dis
>>> dis.dis(co.co_code)
          0 LOAD_NAME                0 (0)
          2 LOAD_CONST               0 (0)
          4 BINARY_ADD
          6 RETURN_VALUE

LOAD_NAME, LOAD_CONST, BINARY_ADD и RETURN_VALUE, всё это инструкции байтового кода. Они носят название байтового кода (bytecode) по той причине, что все они обладают длиной в один байт. Тем не менее, начиная с Python 3.6, их формат хранения был изменён на word (слово, два байта), поэтому технически более правильно теперь о них говорить как о &qout;коде слова&qout;, а не о байтовом коде.

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

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

Снова исполните instaviz, чтобы увидеть объект кода и байтовый код для определённой в REPL функции:

>>> import instaviz
>>> def example():
       a = 1
       b = a + 1
       return b
>>> instaviz.show(example)

  API компилятора C

Точка входа для модуля компиляции дерева абстрактного синтаксиса (AST), compiler_mod(), переключает в различные функции компилятора в зависимости от типа модуля. Если вы предполагаете что mod установлен в Module, тогда в качестве элементов комиляции этот модуль компилируется в свойство c_stack. Затем запускается assemble() для создания PyCodeObject из стека элементов компилятора.

Возвращается полученный кодовый объект и отправляется на исполнение компилятором или сохраняется на диске в виде файла .pyc, в строке 1820 Python|compile.c:

tatic PyCodeObject *
compiler_mod(struct compiler *c, mod_ty mod)
{
    PyCodeObject *co;
    int addNone = 1;
    static PyObject *module;
    ...
    switch (mod->kind) {
    case Module_kind:
        if (!compiler_body(c, mod->v.Module.body)) {
            compiler_exit_scope(c);
            return 0;
        }
        break;
    case Interactive_kind:
        ...
    case Expression_kind:
        ...
    ...
    co = assemble(c, addNone);
    compiler_exit_scope(c);
    return co;
}

compiler_body() в цикле обходит все предложения из модуля и посещает их, строка 1782 Python/compile.c:

static int
compiler_body(struct compiler *c, asdl_seq *stmts)
{
    int i = 0;
    stmt_ty st;
    PyObject *docstring;
    ...
    for (; i < asdl_seq_LEN(stmts); i++)
        VISIT(c, stmt, (stmt_ty)asdl_seq_GET(stmts, i));
    return 1;
}

Значение типа предложения определяется посредством вызова asdl_seq_GET(), которое отыскивается по типу узла AST.

Для каждого типа оператора VISIT посредством макро вызывает функцию из Python/compile.c:

#define VISIT(C, TYPE, V) {\
    if (!compiler_visit_ ## TYPE((C), (V))) \
        return 0; \
}

Для stmt (общего типа предложения, statement), наш компилятор далее вызывает compiler_visit_stmt() и выполняет переключение по всем имеющимся потенциально типам, обнаруживаемым в Parser/Python.asdl, строка 3375 из Python/compile.c:

static int
compiler_visit_stmt(struct compiler *c, stmt_ty s)
{
    Py_ssize_t i, n;

    /* Always assign a lineno to the next instruction for a stmt. */
    SET_LOC(c, s);

    switch (s->kind) {
    case FunctionDef_kind:
        return compiler_function(c, s, 0);
    case ClassDef_kind:
        return compiler_class(c, s);
    ...
    case For_kind:
        return compiler_for(c, s);
    ...
    }

    return 1;
}

В качестве примера вот представление предложения for в Python:

for i in iterable:
    # block
else:  # optional if iterable is False
    # block

Вы можете визуализировать это предложение for при помощи железнодорожной схемы:

Рисунок 7-5

Когда данное предложение является предложением for, compiler_visit_stmt() вызывает compiler_for(). Для всех имеющихся предложений и выражений имеется некий эквивалент функции compiler_*(). Наиболее непосредственные типы создают идущие походу инструкции байтового кода, хотя некоторые более сложные типы предложений вызывают прочие функции.

  Инструкции

Многие предложения могут обладать подчинёнными предложениями. Цикл for обладает неким телом, но вы также можете иметь более сложные выражения в неком присваивании и итераторе.

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

  Тип инструкции

Собственно тип инструкции, instr, обладает следующими полями:

Таблица 7-6. Поля типа инструкции

Поле
Тип
Назначение

i_jabs

unsigned

Флаг, предписывающий что это инструкция абсолютного безусловного перехода

i_jrel

unsigned

Флаг, предписывающий что это инструкция относительного безусловного перехода

i_lineno

int

Номер строки, для которой создана эта инструкция

i_opcode

unsigned char

Номер кода операции, представляемый данной инструкцией (см. Include/Opcode.h)

i_oparg

int

Аргумент кода операции

i_target

basicblock*

Указатель на целевой basicblock, когда выполняется i_jrel

  Инструкции безусловного перехода

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

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

  Блоки базового кадра

Блок базового кода (с типом basicblock) содержит следующие поля:

Таблица 7-7. Поля блока базового кода

Поле
Тип
Назначение

b_ialloc

int

Длина массива инструкций (b_instr)

b_instr

instr *

Указатель на массив инструкций

b_iused

int

Число используемых инструкций (b_instr)

b_list

basicblock *

Список блоков в этом элементе компиляции (в обратном порядке)

b_next

basicblock*

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

b_offset

int*

Смещение инструкции для данного блока, вычисляемое assemble_jump_offsets()

b_return

unsigned

Истинно, когда вставлен код операции RETURN_VALUE

b_seen

unsigned

Применяется для выполнения глубинного поиска (DFS, см. раздел Сборка)

b_startdepth

int

Глубина стека начиная со входа в этот блок, вычисляемая stackdepth()

  Операции и аргументы

Для различных типов операций требуются различные аргументы. Например, ADDOP_JREL и ADDOP_JABS соответственно ссылаются на "add operation with jump to a relative position" (добавить операцию с безусловным переходом по относительному положению), а "add operation with jump to an absolute position" (добавить операцию с безусловным переходом по абсолютному положению), соответственно.

Существуют и другие макросы: ADDOP_I вызывает compiler_addop_i(), который добавляет операцию с целочисленным агрументом. ADDOP_O вызывает compiler_addop_o(), который добавляет операцию с аргументом PyObject.

Сборка

После того как эти этапы компиляции выполнены, наш компилятор обладает списком кадровых блоков, причём каждый содержит перечень инструкций и некий указатель на следующий блок. Ассемблер выполняет поиск в глубину (DFS, deep- first search) этих кдровых блоков и сливает все инструкции в единую последовательность байтовых кодов.

  Структура данных ассемблера

Структур состояния ассемблера, assembler, определена в Python/compile.c и обладает следующими полями:

Таблица 7-8. Поля структуры состояния ассемблера

Поле
Тип
Назначение

a_bytecode

PyObject * (str)

Содержащая байтовый код строка

a_lineno

int

Последний lineno эмитированной инструкции

a_lineno_off

int

Смещение байтового кода последней lineno

a_lnotab

PyObject * (str)

Содержащая lnotab строка

a_lnotab_off

int

Смещение в lnotab

a_nblocks

int

Число достигаемых блоков

a_offset

int

Смещение в байтовом коде

a_postorder

basicblock **

Список блоков в обратном порядке глубинного обхода (DFS)

  Алгоритм ассемблера поиска в глубину

Для обхода узлов в базовом графе кадровых блоков ассемблер пользуется глубинным поиском (DFS, deep- first search). Такой алгоритм DFS не является чем- то особенным в CPython и обычно применяется при обходе графов.

В то время как деревья реального и абстрактного синтаксисов (CST и AST), оба, являются древовидными структурами, состояние компилятора представляет собой структуру графа , в которой её узлами выступают базовые кадровые блоки, содержащие инструкции.

Такие базовые кадровые блоки связаны двумя графами. Один представлен в обратном порядке создания на основании свойства b_list каждого блока. Последовательность пронумерованных алфавитно- цифровым образом от A до O базовых кадровых блоков выглядела бы примерно так:

Рисунок 7-6

Соответствующий создаваемый по b_list граф применяется для последовательного посещения всех блоков в неком скомпилированном элементе.

Второй граф для каждого из блоков применяет свойство b_next. Данный список представляет собой поток управления. Вершины в этом графе создаются вызовами compiler_use_next_block(c, next), где next это следующий блок для вычерчивания вершины из нашего текущего блока (c->u->u_curblock).

Граф узлов цикла for может выглядеть примерно следующим образом:

Рисунок 7-7

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

  API C ассемблера

API ассемблера имеет точку входа, assemble(), которая обладает следующими зонами ответственности:

Вычисляет значение числа блоков для выделения памяти.

Гарантирует, что всякий поступающий с конца блок возвращает значение none.

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

Для осуществления поиска в глубину в соответствующем блоке вызывает dfs().

Эмитирует все имеющиеся инструкции в свой компилятор.

Для выработки PyCodeObject вызывает makecode()/

Строка 6010 Python/compile.c:

{
    ...
    if (!c->u->u_curblock->b_return) {
        NEXT_BLOCK(c);
        if (addNone)
            ADDOP_LOAD_CONST(c, Py_None);
        ADDOP(c, RETURN_VALUE);
    }
    ...
    dfs(c, entryblock, &a, nblocks);

    /* Can't modify the bytecode after computing jump offsets. */
    assemble_jump_offsets(&a, c);

    /* Emit code in reverse postorder from dfs. */
    for (i = a.a_nblocks - 1; i >= 0; i--) {
        b = a.a_postorder[i];
        for (j = 0; j < b->b_iused; j++)
            if (!assemble_emit(&a, &b->b_instr[j]))
                goto error;
    }
    ...

    co = makecode(c, &a);
 error:
    assemble_free(&a);
    return co;
}

  Поиск в глубину

Поиск в глубину выполняется dfs() из Python/compile.c, которая следует указателям b_next в каждом из имеющихся блоков, помечает их как просмотренные посредством b_seen, и затем добавляет их в перечень a_postorder ассемблера в обратном порядке.

Эта функция выполняет цикл назад по обратному списку ассемблера и, если она обладает операцией безусловного перехода, рекурсивно вызывает dfs() для такого безусловного перехода, строка 5441 Python/compile.c:

static void
dfs(struct compiler *c, basicblock *b, struct assembler *a, int end)
{
    int i, j;

    /* Get rid of recursion for normal control flow.
       Since the number of blocks is limited, unused space in a_postorder
       (from a_nblocks to end) can be used as a stack for still not ordered
       blocks. */
    for (j = end; b && !b->b_seen; b = b->b_next) {
        b->b_seen = 1;
        assert(a->a_nblocks < j);
        a->a_postorder[--j] = b;
    }
    while (j < end) {
        b = a->a_postorder[j++];
        for (i = 0; i < b->b_iused; i++) {
            struct instr *instr = &b->b_instr[i];
            if (instr->i_jrel || instr->i_jabs)
                dfs(c, instr->i_target, a, j);
        }
        assert(a->a_nblocks < j);
        a->a_postorder[a->a_nblocks++] = b;
    }
}

После того, как ассемблер создал свой граф в граф управления потоком (CFG) при помощи поиска в глубину, может быть создан кодовый объект.

Создание объекта кода

Задача makecode() состоит в проходе по состоянию компилятора и некоторым свойствам ассемблера и и помещении всего этого в PyCodeObject через вызов PyCode_New().

Значения имён переменных и констант помещаются в качестве свойств этого кодового объекта, строка 5893 Python/compile.c:

static PyCodeObject *
makecode(struct compiler *c, struct assembler *a)
{
...

    consts = consts_dict_keys_inorder(c->u->u_consts);
    names = dict_keys_inorder(c->u->u_names, 0);
    varnames = dict_keys_inorder(c->u->u_varnames, 0);
...
    cellvars = dict_keys_inorder(c->u->u_cellvars, 0);
...
    freevars = dict_keys_inorder(c->u->u_freevars,
                                 PyTuple_GET_SIZE(cellvars));
...
    flags = compute_code_flags(c);
    if (flags < 0)
        goto error;

    bytecode = PyCode_Optimize(a->a_bytecode, consts,
                               names, a->a_lnotab);
...
    co = PyCode_NewWithPosOnlyArgs(
        posonlyargcount+posorkeywordargcount,
        posonlyargcount, kwonlyargcount, nlocals_int,
        maxdepth, flags, bytecode, consts, names,
        varnames, freevars, cellvars, c->c_filename,
        c->u->u_name, c->u->u_firstlineno, a->a_lnotab);
...
    return co;
}

Вы также можете обратить внимание на то, что этот байтовый код отправляется в PyCode_Optimize() до того как он будет отправлен в PyCode_NewWithPosOnlyArgs(). Эта функция является частью процесса оптимизации байтового кода в Python/peephole.c.

Оптимизатор peephole (смотрового окна) проходит по всем инструкциям байтового кода и, в определённых ситуациях, выполняет в нём замены на иные инструкции. Например, имеется некая оптимизация, которая удаляет все недостижимые инструкции, следующие за предложением return.

Применение Instaviz для отображения объекта кода

При помощи модуля instaviz вы можете собрать воедино все этапы вашего компилятора:

import instaviz

def foo():
    a = 2**4
    b = 1 + 5
    c = [1, 4, 6]
    for i in c:
        print(i)
    else:
        print(a)
    return c

instaviz.show(foo)

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

Рисунок 7-8

Вот объект кода со всеми названиями переменных, констант и исполняемого кода co_code:

Рисунок 7-9

Испытайте его с чем- нибудь иным, более сложным кодом, чтобы дополнительно изучить компилятор CPython и объекты кода.

Пример: Реализация оператора почти-равенства

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

Прежде всего вам придётся добавить внутреннее #define для своего оператора Py_AlE с тем, чтобы на него можно было бы ссылаться внутри всего богатства функций сравнения для PyObject.

Откройте Include/object.h и отыщите следующие предложения #define:

/* Rich comparison opcodes */
#define Py_LT 0
#define Py_LE 1
#define Py_EQ 2
#define Py_NE 3
#define Py_GT 4
#define Py_GE 5

Добавьте дополнительное значение, Py_AlE, со значением 6:

/* New almost-equal comparator */
#define Py_AlE 6

Сразу под этим выражением имеется макро, Py_RETURN_RICHCOMPARE. Обновите этот макро с неким предложением выбора для Py_AlE:

/*
 * Macro for implementing rich comparisons
 *
 * Needs to be a macro because any C-comparable type can be used
 */
#define Py_RETURN_RICHCOMPARE(val1, val2, op)                              \
    do {                                                                   \
        switch (op) {                                                      \
        case Py_EQ: if ((val1) == (val2)) Py_RETURN_TRUE; Py_RETURN_FALSE; \
        case Py_NE: if ((val1) != (val2)) Py_RETURN_TRUE; Py_RETURN_FALSE; \
        case Py_LT: if ((val1) < (val2)) Py_RETURN_TRUE; Py_RETURN_FALSE;  \
        case Py_GT: if ((val1) > (val2)) Py_RETURN_TRUE; Py_RETURN_FALSE;  \
        case Py_LE: if ((val1) <= (val2)) Py_RETURN_TRUE; Py_RETURN_FALSE; \
        case Py_GE: if ((val1) >= (val2)) Py_RETURN_TRUE; Py_RETURN_FALSE; \
/* + */ case Py_AlE: if ((val1) == (val2)) Py_RETURN_TRUE; Py_RETURN_FALSE;\
        default:                                                           \
            Py_UNREACHABLE();                                              \
        }                                                                  \
    } while (0)

Внутри Objects/object.c имеется защита, которая выполняет проверку того что данный оператор находится между 0 и 5. Поскольку вы добавили значение 6, вам придётся обновить и это утверждение, строка 709 /Objects/object.c:

PyObject *
PyObject_RichCompare(PyObject *v, PyObject *w, int op)
{
    PyThreadState *tstate = _PyThreadState_GET();

    assert(Py_LT <= op && op <= Py_GE);
Change that last line to the following:

assert(Py_LT <= op && op <= Py_AlE);

Измените здесь последнюю строку следующим образом:

assert(Py_LT <= op && op <= Py_AlE);

Далее вам требуется обновить код операции COMPARE_OP на поддержку Py_AlE в качестве значения для данного типа оператора.

Прежде всего, измените /Objects/object.c и добавьте Py_AlE в список _Py_SwappedOp. Данный перечень применяется для установки соответствия на тот факт, имеет ли данный класс один метод dunder (осадка) и при этом нет иного.

К примеру, когда вы определили некий класс, Coordinate, вы можете определить некий оператор равенства реализовав магический метод __eq__:

class Coordinate:
    def __init__(self, x, y):
        self.x = x
        self.y = y

    def __eq__(self, other):
        if isinstance(other, Coordinate):
            return (self.x == other.x and self.y == other.y)
        return super(self, other).__eq__(other)

Даже хотя вы и не реализовали __ne__ (не равны) для Coordinate, CPython предполагает, что может применяться противоположность __eq__.

>>> Coordinate(1, 100) != Coordinate(2, 400)
True

Внутри Objects/object.c отыщите список _Py_SwappedOp и добавьте в его конец Py_AlE. Затем добавьте "~=" в самый конец перечня opstrings:

int _Py_SwappedOp[] = {Py_GT, Py_GE, Py_EQ, Py_NE, Py_LT, Py_LE, Py_AlE};

static const char * const opstrings[]
 = {"<", "<=", "==", "!=", ">", ">=", "~="};

Откройте Lib/opcode.py и список богатства операторов сравнения:

cmp_op = ("<", "<=", "==", "!=", ">", ">=");

В конец этого кортежа включите наш новый оператор:

cmp_op = ("<", "<=", "==", "!=", ">", ">=", "~=");

Список opstrings используется используется для сообщений об ошибках, когда богатство операторов сравнения не реализовано в неком классе.

Затем вы можете обновить свой компилятор на поддержку того случая, когда свойство PyCmp_AlE пребывает в узле BinOp. Откройте Python/compile.c и отыщите compiler_addcompare(), строка 2479 /Python/compile.c:

static int compiler_addcompare(struct compiler *c, cmpop_ty op)
{
    int cmp;
    switch (op) {
    case Eq:
        cmp = Py_EQ;
        break;
    case NotEq:
        cmp = Py_NE;
        break;
    case Lt:
        cmp = Py_LT;
        break;
    case LtE:
        cmp = Py_LE;
        break;
    case Gt:
        cmp = Py_GT;
        break;
    case GtE:
        cmp = Py_GE;
        break;

Затем добавьте другой case для переключения предложения на пару перечисления comp_op дерева абстрактного синтаксиса AlE с перечислением кода операции сравнения PyCmp_AlE:

..
    case AlE:
        cmp = Py_AlE;
        break;

Теперь вы способны программировать поведение нашего оператора почти- равенства чтобы он отвечал следующему сценарию:

1 ~= 2 это False.

1 ~= 1.01 это True с применением округления вниз.

Вы можете достичь этого неким дополнительным кодом. Теперь же вы осуществите приведение обоих чисел с плавающей точкой к целым и их сравнением.

API CPython обладает большим числом функций для обработки типов PyLong (int) и PyFloat (float). Это мы рассмотрим в Главе 11, Объекты и типы.

В Objects/floatobject.c отыщите float_richcompare() и под определением Compare: goto добавьте следующий вариант, строка 358 /Objects/floatobject.c:

static PyObject*
float_richcompare(PyObject *v, PyObject *w, int op)
{
 ...
    case Py_GT:
        r = i > j;
        break;
    /* New Code START */
    case Py_AlE: {
        double diff = fabs(i - j);
        double rel_tol = 1e-9; // relative tolerance
        double abs_tol = 0.1;  // absolute tolerance
        r = (((diff <= fabs(rel_tol * j))  ||
              (diff <= fabs(rel_tol * i))) ||
              (diff <= abs_tol));
        }
        break;
    }
    /* New Code END */
    return PyBool_FromLong(r);

Этот код будет обрабатывать сравнение чисел с плавающей точкой при применении оператора почти- равенства. Он применяет логику, аналогичную math.isclose(), определяемой в PEP 485, но с жёстко запрограммированным допуском в 0.1.

Другим защитником, который вам надлежит изменить является цикл оценки, Python/ceval.c. Вы изучите цикл оценки в нашей следующей главе.

Отыщите такой фрагмент кода:

...
        case TARGET(COMPARE_OP): {
            assert(oparg <= Py_GE);

Замените эту установку следующим:

assert(oparg <= Py_AlE);

После повторной компиляции CPythonоткройте REPL и проверьте:

$ ./python
>>> 1.0 ~= 1.01
True
>>> 1.02 ~= 1.01
True
>>> 1.02 ~= 2.01
False
>>> 1 ~= 1.01
True
>>> 1 ~= 1
True
>>> 1 ~= 2
False
>>> 1 ~= 1.9
False
>>> 1 ~= 2.0
False
>>> 1.1 ~= 1.101
True

В своей следующей главе мы расширим эту реализацию на прочие типы.

Выводы

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

Рисунок 7-10

Теперь основное задание цикла расчёта ядром интерпретатора CPython для исполнения этих модулей. В своей следующей главе мы исследуем как выполняются объекты кода.

Глава 9. Управление памятью

Содержание

Глава 9. Управление памятью

Выделение памяти в C

Статическое выделение памяти в C

Автоматическое выделение памяти в C

Динамическое выделение памяти в C

Проектирование системы управления памятью Python

Выделение областей

Распределители памяти

Распределитель памяти CPython

Относящиеся к делу исходные файлы

Важные термины

Блоки, пулы и арены

Арены

Пулы

Таблицы пулов

Блоки

API выделения блоков

Применение API отладки Python

Области распределителя памяти объекта и PyMem

Применение модуля tracemalloc

Область распределителя сырой памяти

Персональные распределители доменов

Персональные уборщики выделенной памяти

AddressSanitizer

MemorySanitizer

UndeрnedBehaviorSanitizer

Арена памяти PyArena

Относящиеся к делу файлы

Подсчёт ссылок

Создание переменных в Python

Инкрементальные ссылки

Понижаемые ссылки

Подсчёт ссылок в операциях байтового кода

Преимущества подсчёта ссылок CPython

Сборка мусора

Относящиеся к делу исходные файлы

Проектирование сборки мусора

Включённые в сборщик мусора типы контейнеров

Не отслеживаемые объекты и изменяемость

Алгоритм сборки мусора

Выводы

Двумя наиболее важными частями вашего компьютера выступают его оперативная память и ЦПУ. Ни одна из них не может работать без другой. Они должны использоваться должным образом и они должны быть действенными.

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

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

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

Подсчёт ссылок

Сборка мусора

К концу этой главы вы получите понимание того, как CPython выделяет память в своей операционной системе, как выделяется и освобождается память объектов и как CPython управляет утечками памяти.

Выделение памяти в C

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

Статическое выделение памяти: Требования к памяти вычисляются во время калькуляции и выделяются самим исполняемым кодом после его запуска.

Автоматическое выделение памяти: Требования к памяти для некой сферы действий выделяются в рамках стека вызова при входе кадра и высвобождаются когда этот кадр завершается.

Динамическое выделение памяти: Память может быть запрошена и выделяется динамически во время выполнения через вызовы API выделения памяти.

  Статическое выделение памяти в C

Типы в C обладают фиксированным размером. Сам компилятор вычисляет требования к памяти для всех статических и глобальных переменных и затем вычисляет эти требования в приложении:

static int number = 0;

Вы можете увидеть размер типа в C, воспользовавшись sizeof(). В моей системе, 64-х битной macOS с исполняемым GCC, int это 4 байта. Базовые типы в C могут обладать различными размерами в зависимости от архитектуры.

Массивы определены статически. Рассмотрим такой массив из 10 целых:

static int numbers[10] = {0,1,2,3,4,5,6,7,8,9};

Компилятор C преобразует это предложение в выделение sizeof(int) * 10 байт памяти.

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

  Автоматическое выделение памяти в C

Аналогично статическому выделению памяти, автоматическое выделение памяти вычисляет требования к выделению памяти во время компиляции.

Данный пример демонстрирует преобразование 100 градусов по Фаренгейту в градусы по Цельсию, cpython-book-samples/32/automatic.c:

#include <stdio.h>

static const double five_ninths = 5.0/9.0;

double celsius(double fahrenheit) {
    double c = (fahrenheit - 32) * five_ninths;
    return c;
}

int main() {
    double f = 100;
    printf("%f F is %f C\n", f, celsius(f));
    return 0;
}

Данный пример применяет как статическое, так и автоматическое выделение памяти:

Значение const five_ninths выделяется статически так как обладает ключевыс словом static.

Значение переменной c внутри celsius() выделяется автоматически при вызове celsius() и высвобождается по завершению celsius().

Значение переменной f внутри main() выделяется автоматически при вызове main() и освобождается по завершению main().

Значение результата celsius(f) подразумевает автоматическое выделение.

Все автоматические требования памяти со стороны main() освобождаются когда эта функция выполнена.

  Динамическое выделение памяти в C

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

В таких случаях память выделяется динамически. Динамическое выделение памяти работает через вызовы API выделения памяти C. Операционные системы резервируют некий раздел системной памяти для обработки динамического выделения памяти. Этот раздел памяти носит название кучи (heap).

В своём следующем примере вы динамически выделяете память некому массиву значений градусов по Фаренгейту и по Цельсию. Это приложение вычисляет величины значений градусов по Цельсию соотносясь с специфичными для пользователя числами значений градусов по Фаренгейту, cpython-book-samples/32/dynamic.c:

#include <stdio.h>
#include <stdlib.h>

static const double five_ninths = 5.0/9.0;

double celsius(double fahrenheit) {
    double c = (fahrenheit - 32) * five_ninths;
    return c;
}

int main(int argc, char** argv) {
    if (argc != 2)
        return -1;
    int number = atoi(argv[1]);
    double* c_values = (double*)calloc(number, sizeof(double));
    double* f_values = (double*)calloc(number, sizeof(double));
    for (int i = 0 ; i < number ; i++ ){
        f_values[i] = (i + 10) * 10.0 ;
        c_values[i] = celsius((double)f_values[i]);
    }
    for (int i = 0 ; i < number ; i++ ){
        printf("%f F is %f C\n", f_values[i], c_values[i]);
    }
    free(c_values);
    free(f_values);

    return 0;
}

Если вы выполните эту программу со значением аргумента 4, тогда она выведет на печать следующие результаты:

100.000000 F is 37.777778 C
110.000000 F is 43.333334 C
120.000000 F is 48.888888 C
130.000000 F is 54.444444 C

Этот пример использует динамическое выделение памяти для выделения некого блока памяти из имеющейся кучи, которые затем возвращаются после того как становятся не нужными. Если какая- то динамическая память не возвращена, это вызовет утечку памяти (memory leak).

Проектирование системы управления памятью Python

Будучи построенным поверх C, CPython обязан применять ограничения статического, динамического и автоматического выделения памяти. Некоторые стороны проектирования языка программирования Python превращают эти ограничения в ещё более вызывающие:

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

Большинство типов ядра Python обладают динамическими размерами. Тип list может иметь любой размер, dict может обладать любым числом ключей, и даже int является динамическим. Сам пользователь никогда не должен определять значение размера этих типов.

Имена в Python могут повторно применяться для значений различных типов:

>>> a_value = 1
>>> a_value = "Now I'm a string"
>>> a_value = ["Now" , "I'm", "a", "list"]

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

Вместо того чтобы разработчику Python приходилось выделять память, память объекта Python выделяется автоматически, отдельным, унифицированным API. Такая архитектура требует, чтобы стандартная библиотека CPython и его модули ядра (написанные на C), все они целиком, применяли этот API.

  Выделение областей

CPython поступает с тремя областями динамического выделения памяти:

Сырая область применяется для выделения из системной кучи и большой, не относящейся к объектам памяти.

Область объектов используется для выделения всех связанных с объектами Phyton памяти.

Область PyMem, то же самое что PYMEM_DOMAIN_OBJ. Она присутствует для целей наследования API.

Каждый домен реализует один и тот же интерфейс функций:

_Alloc(size_t size) выделяет память размером size байт и возвращает указатель.

_Calloc(size_t nelem, size_t elsize) выделяет nelem элементов, причём каждый с размером elsize и возвращает указатель.

_Realloc(void *ptr, size_t new_size) изменяет выделение памяти до размера new_size.

_Free(void *ptr) высвобождает память с указателем ptr обратно в кучу.

Перечисление PyMemAllocatorDomain представляет эти три области в CPython как PYMEM_DOMAIN_RAW, PYMEM_DOMAIN_OBJ и PYMEM_DOMAIN_MEM.

  Распределители памяти

CPython использует два распределителя памяти:

malloc: распределитель самой операционной системы для области сырой (raw) памяти.

pymalloc: распределитель памяти CPython для доменов PyMem и памяти объектов.

[Замечание]
Замечание

Распределитель CPython, PyMem по умолчанию скомпилирован вовнутрь CPython. Вы можете удалить его, повторно скомпилировав CPython после установки WITH_PYMALLOC = 0 в pyconfig.h. Если вы удалите его, API областей PyMem и памяти объектов будут применять системный распределитель памяти.

Когда вы компилируете CPython с отладкой (применяя --with-pydebug в macOS или Linux, либо цель Debug в Windows), тогда всякая функция выделения памяти пройдёт в реализацию Debug. Например, при включённой отладке, ваше выделение взывает вместо _PyMem_Alloc() _PyMem_DebugAlloc().

Распределитель памяти CPython

Распределитель памяти CPython расположен поверх системного распределителя памяти и обладает своим алгоритмом для выделения. Этот алгоритм аналогичен системному выделению памяти, за исключением того, что он индивидуализирован под CPython:

Большинство запросов на выделение памяти обладает малым размером и обладают фиксированным размером, ибо PyObject это 16 байт, PyASCIIObject это 42 байта, PyCompactUnicodeObject занимает 72 байта, а PyLongObject составляет 32 байта.

Распределитель памяти pymalloc выделяет блоки памяти только по 256 кБ, нечто большего размера отправляется в системный распределитель памяти.

Распределитель памяти pymalloc пользуется GIL вместо системной проверки безопасности потоков.

Чтобы помочь прояснить это положение вещей, вы можете представить себе в качестве аналогии спортивный стадион, домашний для футбольного клуба CPython. В помощь управления толпой, ФК CPython реализует некое системное разбиение своего стадиона на секторы с A по E, причём с сидениями по рядам с 1 по 40:

Рисунок 9-1

По фронту стадиона, ряды с 1 по 10 представляют собой просторные премиальные сидения с 80 местами в каждом ряду. Позади, ряды с 31 по 40 являются экономичными сидениями со 150 мест в каждом ряду.

Алгоритм выделения памяти Python обладает аналогичными характеристиками:

В точности как стадион обладает сидениями, алгоритм pymalloc обладает блоками памяти.

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

Ровно так же, как сидения одного размера помещены в ряды, блоки одного и того же размера помещаются в пулы.

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

Для такой стратегии имеется ряд преимуществ:

Такой алгоритм более производителен для основных вариантов применения в CPython: небольшие объекты с коротким периодом жизни.

Этот алгоритм применяет GIL вместо системного выявления блокировок потоков.

Данный алгоритм использует установку соответствия в памяти (mmap()) вместо выделения памяти в куче.

  Относящиеся к делу исходные файлы

Вот те исходные файлы, которые относятся к распределению памяти:

Таблица 9-1. Относящиеся к выделению памяти исходные файлы

Файл
Назначение

Include/pymem.h

API выделения памяти PyMem

Include/cpython/pymem.h

API настроек выделения памяти PyMem

Include/internal/pycore_mem.h

Структура данных сборщика мусора и внутренние API

Objects/obmalloc.c

Реализация выделения памяти области и реализация pymalloc

  Важные термины

Ниже приводятся некоторые важные термины, с которыми вы встретитесь в этой главе:

Запрашиваемая память соответствует размеру некого блока.

Все блоки одного и того же размера помещаются в один и тот же пул памяти.

Пулы группируются по аренам.

  Блоки, пулы и арены

Самой крупной группой памяти выступает арена. CPython создаёт арены по 256 кБ для выравнивая на границы системных страниц памяти. Границы системных страниц это непрерывные участки памяти фиксированной длины.

Даже для современной высокоскоростной памяти непрерывная память будет загружаться быстрее фрагментированной. Обладание непрерывной памятью является преимуществом.

  Арены

Арены выделяются из системной кучи и при помощи mmap() в поддерживающих анонимное отображение памяти системах. Установка соответствия в памяти помогает снижать в аренах фрагментацию памяти.

Вот некое визуальное представление для четырёх арен внутри кучи системной памяти:

Рисунок 9-2

Арены обладают структурой данных arenaobject:

Таблица 9-2. Структура данных arenaobject

Поле
Тип
Назначение

address

uintptr_t

Адрес памяти арены

pool_address

block *

Указатель на следующий подлежащий отрезанию для выделения памяти пул

nfreepools

uint

Значение числа пулов в этой арене (свободные пулы плюс никогда не выделяемые пулы)

ntotalpools

uint

Общее количество пулов в арене, вне зависимости от того доступны они или нет

freepools

pool_header*

Односвязный список доступных пулов

nextarena

arena_object*

Следующая арена (см. замечание)

prevarena arena_object*

PyObject *

Предыдущая арена (см. замечание)

[Замечание]
Замечание

Арены связываются между собой в двусвязный список внутри структуры данных арены при помощи указателей nextarena и prevarena.

Если данная арена не выделена, используется следующий участник nextarena. Участник nextarena связывает все невзаимодействующие арены в односвязной глобальной переменной unused_arena_objects.

Когда данная арена взаимодействует с некой выделенной ареной по крайней мере одним доступным пулом, для использования в двусвязном списке usable_arenas используются и nextarena, и prevarena. Этот список сопровождается в увеличивающемся порядке значениями nfreepools.

  Пулы

Внутри некой арены для блоков размером до 512 байт создаются пулы. Для 32- битных систем значением шага является 8 байт, поэтому имеется 64 класса:

Таблица 9-3. Классы пулов 32- битных систем

Запрос в байтах
Размер выделяемого блока
Индекс размера класса

1-8

8

0

9-16

16

1

17-24

24

2

25-32

32

3

...

...

...

497-504

504

62

505-512

512

64

В 64- битных системах шаг составляет 16 байт, поэтому имеется 32 класса:

Таблица 9-4. Классы пулов 64- битных систем

Запрос в байтах
Размер выделяемого блока
Индекс размера класса

1-16

16

0

17-32

32

1

33-48

48

2

49-64

64

3

...

...

...

480-496

496

30

497-512

512

31

Все пулы имеют 4096 байт (4кБ), поэтому в арене всегда имеется 64 пула:

Рисунок 9-3

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

Пулы обладают тремя возможными состояниями:

Заполненный (Full): В этом пуле выделены все блоки.

Используемый (Used): Этот пул выделен и некоторые блоки были установлены, но он всё ещё обладает пространством.

Пустой (Empty): Этот пул выделен, но никакие блоки не были установлены.

В некой арене отметка верхнего уровня вмещает пулы, которые по крайней мере выделены.

Рисунок 9-4

Пулы обладают структурой данных poolp, которая является статическим выделением в памяти структуры pool_header. Тип pool_header обладает следующими свойствами:

Таблица 9-5. Свойства типа pool_header

Поле
Тип
Назначение

ref

uint

Число выделенных в настоящее время блоков в пуле

freeblock

block *

Указатель на заголовок свободного списка этого пула

nextpool

pool_header*

Указатель на следующий пул этого класса размера

prevpool

pool_header*

Указатель на предыдущий пул этого класса размера

arenaindex

uint

Односвязный список доступных пулов

szidx

uint

Индекс размера класса данного пула

nextoffset

uint

Число байт неиспользуемых блоков

maxnextoffset

uint

Максимальное число, которым может быть nextoffset пока блок не заполнится

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

  Таблицы пулов

Реестр всех пулов внутри некой арены носит название таблицы пулов. Таблица пулов представляет собой двусвязный циклический список с заголовком частично используемых пулов.

Такая таблица пулов сегментирована по индексу класса размера, i. Для индекса i, usedpools[i + i] указывает на заголовок списка всех частично используемых пулов, которые обладают значением индекса размера для данного класса размера.

Таблицы пулов обладают некоторыми существенными характеристиками:

Когда пул становится заполненным, ссылка на него изымается из перечня usedpools[].

Если в заполненном пуле освобождается некий блок, тогда этот пул помещается обратно в состояние используемого. Вновь высвобожденный пул привязывается в начало списка usedpools[] с тем, чтобы следующее выделение памяти для его класса размера использовало бы этот освобождённый блок.

При переходе в состояние пустого, пул отсоединяется из своего списка usedpools[] и присоединяется к передней части односвязного списка freepools в его арене.

  Блоки

Внутри пулов память выделяется блоками. Блоки обладают следующими характеристиками:

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

Доступные блоки внутри некого пула перечислены в односвязном списке freeblock.

Когда блок освобождается, он вставляется спереди списка freeblock.

При инициализации некого пула внутри списка freeblock связаны только самые первые два блока.

До тех пор пока пул пребывает в состоянии используемого, будут иметься доступными блоки для их выделения.

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

Рисунок 9-5

  API выделения блоков

Когда блок памяти запрашивается в области памяти, которая применяет pymalloc, вызывается pymalloc_alloc(). Эта функция хорошее место для вставки точки прерывания и шаг по вашему коду чтобы проверить ваши знания блоков, пулов и арен, строка 1590 /Objects/obmalloc.c:

static inline void*
pymalloc_alloc(void *ctx, size_t nbytes)
{
...

Запрос nbytes = 30 не равен ни нулю, не больше значения SMALL_REQUEST_THRESHOLD в 512 байт:

SMALL_REQUEST_THRESHOLD of 512:

    if (UNLIKELY(nbytes == 0)) {
        return NULL;
    }
    if (UNLIKELY(nbytes > SMALL_REQUEST_THRESHOLD)) {
        return NULL;
    }

Для 64- битной системы значение индекса класса размера вычисляется как 1. Это коррелирует со вторым индексом класса размера (17 - 32 байт).

Целевой пул затем это usedpools[1 + 1] (usedpools[2]):

    uint size = (uint)(nbytes - 1) >> ALIGNMENT_SHIFT;
    poolp pool = usedpools[size + size];
    block *bp;

Затем выполняется проверка, чтобы что имеется доступным ('used') некий пул для этого индекса класса размера. Когда список freeblock пребывает в конце своего пула, тогда всё в этом пуле ещё имеются доступными чистые блоки.

Вызывается pymalloc_pool_extend() для расширения списка freeblock:

    if (LIKELY(pool != pool->nextpool)) {
        /*
         * There is a used pool for this size class.
         * Pick up the head block of its free list.
         */
        ++pool->ref.count;
        bp = pool->freeblock;
        assert(bp != NULL);

        if (UNLIKELY((pool->freeblock = *(block **)bp) == NULL)) {
            // Reached the end of the free list. Try to extend it.
            pymalloc_pool_extend(pool, size);
        }
    }

Когда нет доступных пулов, создаётся новый пул и возвращается его первый блок. allocate_from_new_pool() автоматически добавляет такой новый пул в список usedpools:

	else {
			/* There isn't a pool of the right size class immediately
			* available. Use a free pool.
			*/
			bp = allocate_from_new_pool(size);
		}

		return (void *)bp;
	}

Наконец, возвращается значение адреса нового блока.

  Применение API отладки Python

Модуль sys содержит некую внутреннюю функцию, _debugmallocstats(), для получения значения числа используемых блоков для каждого значения пула класса размера. Он также выводит на печать значение числа выделенных и возвращённых арен совместно со значением числа используемых блоков.

Вы можете воспользоваться этой функцией для просмотра использования памяти времени выполнения:

$ ./python -c "import sys; sys._debugmallocstats()"

Small block threshold = 512, in 32 size classes.

class   size   num pools   blocks in use  avail blocks
-----   ----   ---------   -------------  ------------
    0     16           1             181            72
    1     32           6             675            81
    2     48          18            1441            71
 ...
 2 free 18-sized PyTupleObjects * 168 bytes each =                  336
 3 free 19-sized PyTupleObjects * 176 bytes each =                  528

Этот вывод отображает значения таблицы индексов классов размера, имеющиеся выделения памяти и некоторые дополнительные статистические сведения.

Области распределителя памяти объекта и PyMem

Распределитель памяти объектов CPython это первая из трёх областей, которые вы изучите. Основная цель распределителя памяти объектов состоит в выделении памяти, связанной с объектами Python, например, новые заголовки объектов и данные объектов, такие как ключи и значения словарей или списки элементов.

Этот распределитель памяти также применяется самими компилятором, деревом абстрактного синтаксиса (AST), синтаксическим анализатором, а также циклом вычислений. Исключительным образцом выделения в использование памяти объектам является конструктор типа PyLongObject (int), PyLong_New():

Когда строится новый int, память выделяется под конструкцию создания переменной в динамической памяти (allocator, распределитель) необходимого объекта.

Запрашиваемая величина размера это значение размера структуры PyLongObject плюс то количество памяти, которое требуется для хранения цифр.

Python long не является эквивалентом типу long в C. Он выступает неким списком цифр. Число 12378562834 в Python будет представлено как список цифр [1,2,3,7,8,5,6,2,8,3,4]. Именно с такой структурой Python способен работать с гигантскими числами, не заботясь об ограничениях 32- или 64- битных целых.

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

PyLongObject *
_PyLong_New(Py_ssize_t size)
{
    PyLongObject *result;
    ...
    if (size > (Py_ssize_t)MAX_LONG_DIGITS) {
        PyErr_SetString(PyExc_OverflowError,
                        "too many digits in integer");
        return NULL;
    }
    result = PyObject_MALLOC(offsetof(PyLongObject, ob_digit) +
                             size*sizeof(digit));
    if (!result) {
        PyErr_NoMemory();
        return NULL;
    }
    return (PyLongObject*)PyObject_INIT_VAR(result, &PyLong_Type, size);
}

Если вы вызовете _PyLong_New(2), он высчитает значение size_t подобным образом:

Таблица 9-6. Вычисление размера PyLong

Значение
Байты

sizeof(digit)

4

размер

2

смещение заголовка

26

Итого

32

Вызов PyObject_MALLOC() должен бы быть выполнен со значением size_t равным 32.

В моей системе значение максимального числа цифр в long, MAX_LONG_DIGITS, равно 2305843009213693945 (очень, очень большое число). Если вы исполните _PyLong_New(2305843009213693945), то это вызвало бы PyObject_MALLOC() для 9223372036854775804, или 8,589,934,592 гигабайт (больше чем я имею в доступности оперативной памяти).

  Применение модуля tracemalloc

Модуль tracemalloc из стандартной библиотеки можно применять для отладки выделения памяти через распределитель памяти объектам. Он предоставляет сведения где размещён некий объект и значение числа выделенных блоков памяти. В качестве отладочного средства tracemalloc может помогать вам вычислять значение объёма потребляемой памяти запуская ваш код и выявляя утечки памяти.

Чтобы включить отслеживание памяти вы можете запустить Python с -X tracemalloc=1, где 1 это значение числа глубины кадров, которое вы желаете отслеживать. В качестве альтернативы вы можете воспользоваться переменной среды PYTHONTRACEMALLOC=1. Вы можете предписать сколько кадров в глубину отслеживать, заменив 1 любым целым.

Вы можете воспользоваться take_snapshot() для создания экземпляра моментального снимка, а затем сопоставлять множество моментальных снимков при помощи compare_to(). Создадим некий файл примера tracedemo.py чтобы увидеть это в действии, cpython-book-samples/32/tracedemo.py:

import tracemalloc

tracemalloc.start()

def to_celsius(fahrenheit, /, options=None):
    return (fahrenheit-32)*5/9

values = range(0, 100, 10)  # values 0, 10, 20, ... 90

for v in values:
    c = to_celsius(v)

after = tracemalloc.take_snapshot()

tracemalloc.stop()
after = after.filter_traces([tracemalloc.Filter(True, '**/tracedemo.py')])
stats = after.statistics('lineno')

for stat in stats:
    print(stat)

Его исполнение выводит на печать построчный список используемой памяти с наивысших к наинисшим значениям:

$ ./python -X tracemalloc=2 tracedemo.py

/Users/.../tracedemo.py:5: size=712 B, count=2, average=356 B
/Users/.../tracedemo.py:13: size=512 B, count=1, average=512 B
/Users/.../tracedemo.py:11: size=480 B, count=1, average=480 B
/Users/.../tracedemo.py:8: size=112 B, count=2, average=56 B
/Users/.../tracedemo.py:6: size=24 B, count=1, average=24 B

Строкой с наивысшим потреблением памяти была return (fahrenheit-32)*5/9, которая и выполняла реальные вычисления.

Область распределителя сырой памяти

Область выделения сырой памяти используется либо напрямую, либо когда две прочих области вызываются с запросом памяти, превышающим 512кБ. Он получает величину запрашиваемого размера, в байтах, и вызывает malloc(size). Когда значением аргумента является 0, тогда некоторые системы вернут для malloc(0) NULL, что будет воспринято как некая ошибка. Некоторые платформы вернут какой- то указатель без стоящей за ним памяти, что вызвало бы прерывание pymalloc.

Для решения этой проблемы _PyMem_RawMalloc() добавляет некий дополнительный байт перед вызовом malloc().

[Замечание]
Замечание

По умолчанию, распределители памяти области PyMem применяют выделение памяти объектам. PyMem_Malloc() и PyObject_Malloc() имеют один и тот же путь исполнения.

Персональные распределители доменов

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

PyMemAllocatorEx это typedef struct с участниками для всех методов, которые вам потребуется реализовать для перекрытия имеющегося распределителя памяти:

typedef struct {
    /* User context passed as the first argument to the four functions */
    void *ctx;

    /* Allocate a memory block */
    void* (*malloc) (void *ctx, size_t size);

    /* Allocate a memory block initialized by zeros */
    void* (*calloc) (void *ctx, size_t nelem, size_t elsize);

    /* Allocate or resize a memory block */
    void* (*realloc) (void *ctx, void *ptr, size_t new_size);

    /* Release a memory block */
    void (*free) (void *ctx, void *ptr);
} PyMemAllocatorEx;

Для получения имеющейся реализации доступен метод API PyMem_GetAllocator():

PyMemAllocatorEx * existing_obj;
PyMem_GetAllocator(PYMEM_DOMAIN_OBJ, existing_obj);

[Предостережение]
Важно

Существуют некоторые важные тесты архитектуры для индивидуальных распределителей памяти:

Новый распределитель памяти обязан возвращать отличающийся, не-NULL указатель при запросе нуля байт.

Для области PYMEM_DOMAIN_RAW этот распределитель должен обладать безопасным потоком.

Если вы реализовали необходимые функции My_Malloc(), My_Calloc(), My_Realloc() и My_Free(), при помощи подписей PyMemAllocatorEx, тогда вы можете перекрыть имеющийся распределитель для каждой из областей, например, для PYMEM_DOMAIN_OBJ:

PyMemAllocatorEx my_allocators =
    {NULL, My_Malloc, My_Calloc, My_Realloc, My_Free};
PyMem_SetAllocator(PYMEM_DOMAIN_OBJ, &my_allocators);

Персональные уборщики выделенной памяти

Уборщики выделенной памяти (Memory allocation sanitizers) это дополнительные алгоритмы, помещаемые между имеющимися системными вызовами для выделения памяти и собственно функциями ядра для распределения памяти в вашей системе. Они применяются для сред, которые требуют особых ограничений стабильности или очень высокой безопасности или же для отладки ошибок выделения памяти.

CPython может быть скомпилирован с применением различных уборщиков памяти. Они являются частью библиотек компилятора, а не чем- то разработанным под CPython. Обычно они значительно замедляют CPython и не могут комбинироваться. В основном они предназначены для применения в сценариях отладки или в системах, в которых критически важным является предотвращение порчи памяти.

  AddressSanitizer

AddressSanitizer это быстрый определитель ошибок в памяти. Он способен выявлять большое число относящихся к памяти ошибок времени исполнения:

Выход за границы кучи, стека и глобальных элементов

Подлежащая использованию память после её высвобождения

Двойное освобождение и неверное высвобождение

Вы можете включить AddressSanitizer выполнив следующее:

$ ./configure --with-address-sanitizer ...

[Предостережение]
Важно

AddressSanitizer способен замедлять приложения вплоть до двух раз и потреблять в три раза больше памяти.

AddressSanitizer поддерживается в следующих операционных системах:

Linux

macOS

NetBSD

FreeBSD

Для получения дополнительных сведений обратитесь к официальной документации

  MemorySanitizer

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

Вы можете включить MemorySanitizerвыполнив следующее:

$ ./configure --with-memory-sanitizer ...

[Предостережение]
Важно

MemorySanitizer способен замедлять приложения вплоть до двух раз и потреблять в два раза больше памяти.

MemorySanitizer поддерживается в следующих операционных системах:

Linux

NetBSD

FreeBSD

Для получения дополнительных сведений обратитесь к официальной документации

  UndeрnedBehaviorSanitizer

UndefinedBehaviorSanitizer (UBSan) это средство быстрого выявления не предопределённого поведения. В процессе выполнения он способен выявлять различные виды не заданного поведения:

Неправильно выровненный или нулевой указатель

переполнение целого со знаком

Преобразовани в, из или между типами с плавающей запятой

Вы можете включить UndefinedBehaviorSanitizer выполнив следующее:

$ ./configure --with-undefined-behavior-sanitizer ...

UndefinedBehaviorSanitizer поддерживается в следующих операционных системах:

Linux

macOS

NetBSD

FreeBSD

Для получения дополнительных сведений обратитесь к официальной документации

UBSan обладает множеством конфигураций. При помощи --with-undefined-behavior-sanitizer будет установлен профиль undefined. Для применения иного профиля, например, nullability, запустите ./configure с индивидуальным CFLAGS:

$ ./configure CFLAGS="-fsanitize=nullability" \
  LDFLAGS="-fsanitize=nullability"

После того как вы повторно скомпилируете CPython? эта конфигурация воспроизведёт исполняемый файл CPython с применением UndefinedBehaviorSanitizer.

Арена памяти PyArena

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

PyArena также обладает своим собственным списком выделенных объектов внутри данной структуры арены. Выделяемая PyArena память не является целью сборщика мусора.

Когда память выделяется в экземпляре PyArena, будет перехвачено запущенного общего числа выделенных блоков, после чего вызывается PyMem_Alloc. Запросы на выделение памяти к PyArena применяют распределитель памяти объектов для блоков с размером меньшим или равным 512кБ и распределитель сырой памяти для блоков большего размера.

  Относящиеся к делу файлы

Вот связанные с PyArena файлы:

Таблица 9-7. Относящиеся к PyArena файлы

Файл
Назначение

Include/pyarena.h

API PyArena и определения типа

Include/pyarena.c

Реализация PyArena

Подсчёт ссылок

Как вы до сих пор наблюдали в этой главе, CPython собирается в системе динамического распределения памяти C. Требования к памяти определяются во время исполнения и память выделяется вашей системе при помощи API PyMem.

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

Для упрощения управления памятью Python приспособил две стратегии по управлению распределением под объекты памяти:

Подсчёт ссылок

Сборка мусора

Далее мы рассмотрим их более подробно.

  Создание переменных в Python

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

my_variable = ["a", "b", "c"]

При присвоении некого значения какой-то переменной в Python, её название проверяется внутри локальной и глобальной сфер действия на предмет того не существует ли уже такое.

В нашем примере выше my_variable ещё пока нет ни в каких словарях locals() или globals(). Создаётся новый объект list и указатель на него сохраняется в словаре locals().

Теперь имеется одна ссылка на my_variable. Список объектов памяти не должен освобождаться пока имеются допустимые ссылки на него. Если эта память была освобождена, тогда данный указатель на my_variable указывал бы на недопустимое пространство памяти и CPython испытал бы крушение.

На протяжении кода C CPython вы будете наблюдать вызовы Py_INCREF() и Py_DECREF(). Эти макросы в первую очередь выступают API для инкрементального увеличения и уменьшения ссылок на объекты Python. Всякий раз когда нечто полагается на некое значение, значение счётчика ссылок увеличивается на 1. Когда эта зависимость больше не имеет места, значение этого счётчика уменьшается на 1.

Когда значение счётчика достигает нуля, это предполагает, что данная память больше не требуется и она автоматически освобождается.

  Инкрементальные ссылки

Всякий экземпляр PyObject обладает свойством ob_refcnt. Это свойство выступает счётчиком значения числа ссылок на данный объект.

Ссылки на некий объект прирастают на единицу во многих случаях. В базовом коде CPython имеется более 3000 вызовов Py_INCREF(). Наиболее распространены вызовы огда его объект:

Назначается некой переменной с названием

Служит ссылкой в качестве аргумента функции или метода

Возвращается или выдаётся из функции

Сама стоящая за макро Py_INCREF логика обладает лишь одним шагом. Она увеличивает величину значения ob_refcnt на единицу:

static inline void _Py_INCREF(PyObject *op)
{
    _Py_INC_REFTOTAL;
    op->ob_refcnt++;
}

Когда CPython компилируется в режиме отладки, тогда _Py_INC_REFTOTAL будет увеличивать на единицу счётчик глобальных ссылок, _Py_RefTotal.

[Замечание]
Замечание

Вы можете наблюдать глобальный счётчик ссылок добавляя флаг -X showrefcount при запуске некой отладочной сборки CPython:

$ ./python -X showrefcount -c "x=1; x+=1; print(f'x is {x}')"
x is 2
[18497 refs, 6470 blocks]

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

  Понижаемые ссылки

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

Py_DECREF() более сложный чем Py_INCREF(), потому как он также управляет логикой ссылки по достижению 0, которая требует освобождения памяти данного объекта:

static inline void _Py_DECREF(
#ifdef Py_REF_DEBUG
    const char *filename, int lineno,
#endif
    PyObject *op)
{
    _Py_DEC_REFTOTAL;
    if (--op->ob_refcnt != 0) {
#ifdef Py_REF_DEBUG
        if (op->ob_refcnt < 0) {
            _Py_NegativeRefcount(filename, lineno, op);
        }
#endif
    }
    else {
        _Py_Dealloc(op);
    }
}

Внутри Py_DECREF(), когда значение счётчика его ссылки (ob_refcnt) становится равным 0, вызывается деструктор _Py_Dealloc(op) и вся выделенная память освобождается.

Как и в случае Py_INCREF(), имеются некоторые дополнительные функции когда CPython скомпилирован в режиме отладки.

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

<file>:<line>: _Py_NegativeRefcount: Assertion failed:
    object has negative ref count
Enable tracemalloc to get the memory block allocation traceback

object address  : 0x109eaac50
object refcount : -1
object type     : 0x109cadf60
object type name: <type>
object repr     : <refcnt -1 at 0x109eaac50>

При внесении изменений в поведение некой операции, языка программирования Python или его компилятора, следует аккуратно рассматривать воздействие ссылок на объекты.

  Подсчёт ссылок в операциях байтового кода

Большая часть подсчёта числа ссылок Python происходит внутри операций байтового кода в Python/ceval.c.

Подсчитаем число ссылок на переменную у в этом примере:

y = "hello"

def greet(message=y):
    print(message.capitalize() + " " + y)

messages = [y]

greet(*messages)

На первый взгляд имеется четыре ссылки на у:

В качестве переменной в самой верхней области действия

В качестве значения по умолчанию для аргумента ключевого слова message

Внутри greet()

Как элемент внутри списка message

Выполните этот код со следующей дополнительной вставкой:

import sys
print(sys.getrefcount(y))

В действительности имеется в общей сложности шесть ссылок на у.

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

Операции байтового кода обязаны обладать детерменированным воздействием на счётчик ссылок для тех объектов, которые он получает в качестве аргументов.

Например, в соответствующем кадре цикла вычислений, операция LOAD_FAST загружает свой объект с заданным названием и помещает его в вершину своего стека значений. Поскольку это название переменной, которое представлено в oparg было разрешено при помощи GETLOCAL(), значение счётчика ссылок было увеличено на единицу:

...
    case TARGET(LOAD_FAST): {
        PyObject *value = GETLOCAL(oparg);
        if (value == NULL) {
            format_exc_check_arg(tstate, PyExc_UnboundLocalError,
                                 UNBOUNDLOCAL_ERROR_MSG,
                                 PyTuple_GetItem(co->co_varnames, oparg));
            goto error;
        }
        Py_INCREF(value);
        PUSH(value);
        FAST_DISPATCH();
    }

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

Например, допустим, вы выполняете назначение двум переменным, a и b, затем создаёте третью, c, на основе произведения a и b:

a = 10
b = 20
c = a * b

В нашей третьей операции, c = a * b, выражение с правой стороны, a * b, будет ассемблировано в три операции:

LOAD_FAST, разрешение значения переменной a и её помещение в стек значений, с последующим увеличением значения числа ссылок на a на единицу

LOAD_FAST, разрешение значения переменной b и её помещение в стек значений, с последующим увеличением значения числа ссылок на b на единицу

BINARY_MULTIPLY, умножение значений переменных слева и справа и помещение их результата в стек значений

Оператор бинарного умножения, BINARY_MULTIPLY, знает, что ссылки на левую и правую переменную в его операции были загружены в первую и вторую позицию в стеке значений. Он также подразумевает, что сама операция LOAD_FAST прирастила на единицу их счётчики ссылок.

В реализации этой операции BINARY_MULTIPLY значения этих ссылок для обоих, и a (левого), и b (правого) уменьшаются на единицу после вычисления соответствующего результата:

        case TARGET(BINARY_MULTIPLY): {
            PyObject *right = POP();
            PyObject *left = TOP();
            PyObject *res = PyNumber_Multiply(left, right);
            Py_DECREF(left);
            Py_DECREF(right);
            SET_TOP(res);
            if (res == NULL)
                goto error;
            DISPATCH();
        }

Получаемое в результате число, res, будет иметь счётчик ссылок равным 1 прежде чем оно будет установлено в вершине стека значений.

  Преимущества подсчёта ссылок CPython

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

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

Всё что обсуждалось до сих пор относится к сфере исполнения CPython, Разработчик Python практически не способен контролировать это поведение.

Существует ещё один существенный недостаток в подходе с подсчётом ссылок: циклические ссылки.

Воспользуемся таким примером на Python:

x = []
x.append(x)
del x

Значения счётчика ссылок для x всё ещё 1, поскольку он ссылается сам на себя.

В угоду этой сложности и для устранения таких типов утечек памяти CPython обладает вторым механизмом управления памяти, носящим название Сборки мусора (garbage collection).

Сборка мусора

Как часто вы занимаетесь уборкой мусора? Каждую неделю или раз в две недели?

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

CPython пользуется тем же самым принципом для алгоритма сборки мусора. Сборщик мусора CPython работает над возвратом которая была задействована более не существующими объектами. По умолчанию он включён и работает в фоновом режиме.

Поскольку алгоритм сборки мусора намного сложнее подсчёта ссылок, он не происходит всё время. Когда он в деле, он будет потреблять большой объём ресурсов ЦПУ. Сборка мусора работает периодически после некоторого набора операций.

  Относящиеся к делу исходные файлы

Вот связанные со сборкой мусора файлы:

Таблица 9-8. Относящиеся к сборке мусора файлы

Файл
Назначение

Modules/gcmodule.c

Реализация модуля и алгоритмя сборки мусора

Include/internal/pycore_mem.h

Реализация PyArenaСтруктура данных и внутреннее API сборки мусора

  Проектирование сборки мусора

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

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

Хотя и следует избегать создания циклических ссылок в контейнерах, внутри стандартной библиотеки и ядра интерпретатора имеется множество примеров таковых. Вот ещё один распространённый пример в котором тип контейнера (class) может ссылаться сам на себя, cpython-book-samples/32/user.py:

__all__ = ["User"]

class User(BaseUser):
    name: 'str' = ""
    login: 'str' = ""

    def __init__(self, name, login):
        self.name = name
        self.login = login
        super(User).__init__()

    def __repr__(self):
        return ""

class BaseUser:
    def __repr__(self):
        # This creates a cyclical reference
        return User.__repr__(self)

В данном примере экземпляр User ссылается на тип BaseUser, который обратно ссылается на этот экземпляр User. Основная цель сборки мусора состоит в поиске недостижимых объектов и их пометке как мусора.

Некоторые алгоритмы сборки мусора, такие как пометить и вымести (mark and sweep) или остановиться и скопировать (stop and copy), стартуют в самом корне и изучают все достижимые объекты. Это сложно выполнять в CPython, ибо модули расширения C могут определять и сохранять свои собственные объекты. У вас нет возможности запросто определять все объекты просто просматривая их в locals() и globals().

Для процессов с длительным временем работы и большими задачами обработки данных истощение памяти может вызывать значительные проблемы.

Вместо этого сборщик мусора CPython отыскивает критически важные ссылки в определённых типах контейнеров.

  Включённые в сборщик мусора типы контейнеров

Сборщик мусора ищет типы, обладающие в своём определении типа установленным флагом Py_TPFLAGS_HAVE_GC. Вы рассмотрите определения типов в Главе 11, Объекты и типы.

Вот те типы, которые помечаются для сборки мусора:

Объекты класса, метода и функции

Объекты ячейки

Массивы байтов, строки байтов и Unicode

Словари

Применяемые в атрибутах объекты дескриптора

Перечисляемые объекты

Исключительные ситуации

Объекты кадров

Списки, кортежи, именованные кортежи и наборы

Объекты памяти

Модули и пространства имён

Объекты типов и слабых ссылок

Итераторы и генераторы

Буферы маринования

Интересуетесь что пропущено? Не помечены для сборки мусора числа с плавающей запятой, целые, Булевы, и NoneType.

Индивидуальные типы, написанные при помощи модулей расширения C могут помечаться как требующие сборки мусора при помощи API C сборщика мусора.

  Не отслеживаемые объекты и изменяемость

Сборщик мусора будет отслеживать определённые типы на предмет изменения их свойств для выявления того какие из них не достижимы.

Некоторые экземпляры контейнеров не являются предметом изменения, потому как они непреложные, поэтому данный API предоставляет механизм для избавления от отслеживания (untracking). Чем меньше имеется объектов для отслеживания сборщиком мусора, тем быстрее и действеннее оказывается сборщик мусора.

Исключительным примером не отслеживаемых объектов являются кортежи. Кортежи неизменны. После их создания вы не можете их изменять. Тем не менее, кортежи могут содержать изменяемые типы, такие как списки и словари.

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

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

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

Вы можете определять подлежит ли какой нибудь объект отслеживанию через вызов gc.is_tracked(obj).

  Алгоритм сборки мусора

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

Инициализация

Для запуска и останова сборщика мусора его точка входа PyGC_Collect() следует процессом из пяти шагов:

Получить из своего интерпретатора состояние сборщика мусора, Code.

Убедиться что сборщик мусора включён.

Убедиться в том что сборщик мусора уже исполняется.

Запустить функцию сборки, collect(), с продвижением по обратным вызовам.

Пометить сборщик мусора как выполненный.

Когда сборщик мусора был исполнен и завершён, вы можете определить методы обратных вызовов при помощи списка gc.callbacks. Обратные вызовы обязаны обладать подписью соответствующего метода f(stage: str, info: dict):

Python 3.9 (tags/v3.9:9cf67522, Oct 5 2020, 10:00:00)
[Clang 6.0 (clang-600.0.57)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import gc
>>> def gc_callback(phase, info):
...  print(f"GC phase:{phase} with info:{info}")
...
>>> gc.callbacks.append(gc_callback)
>>> x = []
>>> x.append(x)
>>> del x
>>> gc.collect()
GC phase:start with info:{'generation': 2,'collected': 0,'uncollectable': 0}
GC phase:stop with info:{'generation': 2,'collected': 1,'uncollectable': 0}
1

Стадия уборки

В главной функции сборки мусора, collect() имеет целью три поколения в CPython. Прежде чем вы ознакомитесь с поколениями, вначале важно понять сам алгоритм сборки.

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

Когда создаётся один из таких типов контейнеров, он сам добавляет себя в этот список, а когда он уничтожается, он сам удаляет себя. Вы можете рассмотреть некий образец в типе cellobject.c, строка 7 Objects/cellobject.c:

PyObject *
PyCell_New(PyObject *obj)
{
    PyCellObject *op;

    op = (PyCellObject *)PyObject_GC_New(PyCellObject, &PyCell_Type);
    if (op == NULL)
        return NULL;
    op->ob_ref = obj;
    Py_XINCREF(obj);

>>  _PyObject_GC_TRACK(op);
    return (PyObject *)op;
}

Поскольку ячейки изменяемые, этот объект помечается как отслеживаемый через вызов _PyObject_GC_TRACK().

Когда объекты ячеек удаляются, вызывается cell_dealloc(), эта функция предпринимает три шага:

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

Py_XDECREF это стандартный вызов во всяком деструкторе для уменьшения на единицу счётчика его ссылок. Значение счётчика ссылок инициализируется со значением 1, поэтому он обсчитывает эту операцию.

PyObject_GC_Del() удаляет данный объект из связного списка сборки мусора через вызов gc_list_remove(), а затем освобождает память при помощи PyObject_FREE().

Вот исходный код cell_dealloc(), строка 79 /Objects/cellobject.c:

static void
cell_dealloc(PyCellObject *op)
{
    _PyObject_GC_UNTRACK(op);
    Py_XDECREF(op->ob_ref);
    PyObject_GC_Del(op);
}

Когда запускается уборка, она сливает более молодые поколения в екущее поколение. Например, если вы убираете своё второе поколение, тогда когда оно запускает уборку, оно будет слито с объектами первого поколения в соответствующий список сборщика мусора при помощи gc_list_merge(). Сборщик мусора затем выявит недостижимые объекты в своём young (выступающем текущей целью) поколении.

Сама логика для выявления недостижимых объектов располагается в deduce_unreachable(). Она следует такими этапами:

Для каждого объекта в соответствующем поколении копируется значение счётчика ob->ob_refcnt в ob->gc_ref.

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

Создаётся список недостижимых объектов и добавляются все объекты, которые соответствуют указанному на этапе 2 критерию.

Не существует единственного метода для выявления циклических ссылок. Каждый тип должен определять индивидуальную функцию с подписью traverseproc в слоте tp_traverse. Для осуществления указанного выше этапа 2, deduce_unreachable() вызывает функцию обхода для каждого объекта внутри subtract_refs(). Эта функция обхода должна выполнять обратный вызов visit_decref() для каждого содержащего ею элемента, строка 462 /Modules/gcmodule.c:

static void
subtract_refs(PyGC_Head *containers)
{
    traverseproc traverse;
    PyGC_Head *gc = GC_NEXT(containers);
    for (; gc != containers; gc = GC_NEXT(gc)) {
        PyObject *op = FROM_GC(gc);
        traverse = Py_TYPE(op)->tp_traverse;
        (void) traverse(FROM_GC(gc),
                       (visitproc)visit_decref,
                       op);
    }
}

Такие функции обхода удерживаются внутри исходного кода каждого объекта в Objects. Например, функция обхода tuple, tupletraverse(), для всех своих элементов вызывает visit_decref(). Аналог в типе словаря будет вызывать visit_decref() для всех своих ключей и значений.

Всякий объект, который не завершён, подлежит перемещению в список unreachable, выпускаемый для следующего поколения.

Высвобождение объектов

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

Когда объект обладает завершителем в наследуемом слоте tp_del, тогда он не способен безопасно удаляться и помечается как не подлежащий уборке. Они добавляются в список gc.garbage с тем, чтобы их разработчик уничтожал их вручную.

Когда некий объект имеет завершитель в слоте tp_finalize, тогда он помечается как завершённый во избежание повторного вызова.

Если некий объект на этапе 2 был воскрешён повторной инициализацией, тогда сборщик мусора повторно выполняет свой цикл уборки.

Для всех объектов вызывается слот tp_clear. Этот слот изменяет значение счётчика ссылок, ob_refcnt, в 0, включая освобождение памяти.

Сборка мусора по поколениям

Сборка мусора по поколениям это техника, основанная на том, наблюдении, что большинство объектов (80 процентов и более) уничтожаются сразу после их создания.

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

В функции уборки целью выступает отдельное поколение и она сливает более юные поколения в себя перед исполнением. По этой причине, когда вы запускаете collect() для поколения 1, затем она будет прибирать и поколение 0. Аналогично, запуск collect() на поколении 2 будет убираться и в поколениях 0 и 1.

Когда создаются экземпляры объекта, счётчики поколения увеличиваются на единицу. Когда этот счётчик достигает порогового значения, collect() запускается автоматически.

Применение API сборки мусора из Python

Стандартная библиотека CPython поставляется с модулем Python gc для взаимодействия с конкретной ареной и сборщиком мусора. Вот как пользоваться модулем gc в режиме отладки:

>>> import gc
>>> gc.set_debug(gc.DEBUG_STATS)

Это будет выдавать на печать статистические значения при каждом запуске сборщика мусора:

gc: collecting generation 2...
gc: objects in each generation: 3 0 4477
gc: objects in permanent generation: 0
gc: done, 0 unreachable, 0 uncollectable, 0.0008s elapsed

Вы применяете gc.DEBUG_COLLECTABLE для изучения когда элементы собраны под мусор. Когда вы сочетаете это с флагом отладки gc.DEBUG_SAVEALL, это переместит элементы в перечень gc.garbage, после того как они будут прибраны:

>>> import gc
>>> gc.set_debug(gc.DEBUG_COLLECTABLE | gc.DEBUG_SAVEALL)
>>> z = [0, 1, 2, 3]
>>> z.append(z)
>>> del z
>>> gc.collect()
gc: collectable <list 0x10d594a00>
>>> gc.garbage
[[0, 1, 2, 3, [...]]]

Вы можете получить величину порогового значения, после которого запускается сборщик мусора, вызвав get_threshold():

>>> gc.get_threshold()
(700, 10, 10)

Вы также можете получить значение текущего счётчика порогового значения:

>>> gc.get_count()
(688, 1, 1)

Наконец, вы можете запускать для некого поколения алгоритм сборки мусора вручную и затем возвращать собранное итоговое значение:

>>> gc.collect(0)
24

Если вы не предписываете поколение, тогда по умолчанию им будет 2, что сольёт вместе также и поколения 0 и 1.

Выводы

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

Системы выделения памяти объектам и сырой памяти, которые были показаны в этой главе, превратятся в полезные если вы разрабатываете модули расширения C. Модули расширения C требуют некого интимного знакомства с системой управления памятью CPython. Даже единственный пропущенный Py_INCREF() способен вызывать утечки памяти или крушение системы.

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

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

Глава 8. Цикл расчёта

Содержание

Глава 8. Цикл расчёта

Относящиеся к делу исходные файлы

Важные термины

Построение состояния потока

Тип состояния потока

Относящиеся к делу исходные файлы

Построение объектов кадров

Тип объекта кадра

Относящиеся к делу исходные файлы

API инициализации объекта кадра

Преобразование параметров ключевых слов в словарь

Преобразование позиционных аргументов в переменные

Упаковка позиционных аргументов в *args

Загрузка аргументов ключевых слов

Добавление пропущенных позиционных аргументов

Добавление пропущенных аргументов ключевых слов

Свёртывание замыканий

Создание генераторов, сопрограмм и асинхронных генераторов

Исполнение кадра

Отслеживание выполнения кадра

Стек значений

Пример операции байтового кода: BINARY_OR

Имитация стека значений

Действия стека

Пример: Добавление элемента в список

Выводы

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

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

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

Выполнение кода в CPython происходит внутри центрального цикла с названием цикла расчёта (evaluation loop). Интерпретатор CPython будет рассчитывать и выполнять кодовый объект, выбираемый из либо выстроенного файла .pyc или из самого компилятора:

Рисунок 8-1

В цикле расчёта каждая из инструкций байтового кода берётся и исполняется при помощи стековой системы на основе кадров.

[Замечание]
Замечание

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

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

Traceback (most recent call last):
  File "example_stack.py", line 8, in <module>  <--- Frame
    function1()
  File "example_stack.py", line 5, in function1 <--- Frame
    function2()
  File "example_stack.py", line 2, in function2 <--- Frame
    raise RuntimeError
RuntimeError

Относящиеся к делу исходные файлы

Вот те исходные файлы, которые относятся к циклу расчёта:

Таблица 8-1. Относящиеся к расчёту исходные файлы

Файл
Назначение

Python/ceval.c

Реалиация ядра цикла вычислений

Python/ceval-gil.h

Определение GIL и алгоритма управления

Важные термины

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

Цикл вычислений будет получать объект кода и преобразовывать его в последовательность объектов кадров.

Сам интерпертатор обладает по крайней мере одним потоком.

Каждый поток имеет состояние потока

Кадровые объекты выполняются в стеке, носящем название стека кадров.

Стек значений содержит ссылки на переменные.

Построение состояния потока

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

CPython всегда обладает хотя бы одним потоком, причём каждый поток обладает своим собственным состоянием.

[Совет]
Смотри также

Более подробно построение потоков обсуждается в Главе 10, Параллельность и одновременность.

  Тип состояния потока

Тип состояния потока обладает тридцатью свойствами, включая следующие:

Уникальный идентификатор

Присоединённый список на прочие состояния потока

Состояние интерпертатора, из которого о был порождён

Исполняемый в данный момент кадр

Текущая глубина рекурсии

Не обязательные функции отслеживания

Обрабатываемая прямо сейчас исключительная ситуация

Все подлежащие в данный момент асинхронные исключительные ситуации

Некий стек исключительных ситуаций, возбуждённых при возведении множественных исключительных ситуаций (например, внутри блока except)

Счётчик GIL

Асинхронный генератор счётчиков

  Относящиеся к делу исходные файлы

Относящиеся к состоянию потока исходные файлы разбросаны по множеству файлов:

Таблица 8-2. Относящиеся к состоянию потока исходные файлы

Файл
Назначение

Python/thread.c

Реализация API потока

Include/threadstate.h

Некоторые API состояния и определения типов

Include/pystate.h

API состояния интерпретатора и определения типов

Include/pythread.h

API построения потоков

Include/cpython/pystate.h

Некоторые API состояния и потока и интерпретатора

Построение объектов кадров

Скомпилированные объекты кодов вставляются в объекты кадров. Объекты кадров являются типом Python, а потому на них можно ссылаться как из C, так и из Python.

Объекты кадров также содержат прочие данные времени исполнения, необходимые для исполнения всех инструкций в соответствующих кодовых объектах. Эти данные содержат локальные переменные, глобальные переменные и встроенные модули.

  Тип объекта кадра

Типом объекта кадра выступает PyObject со следующими дополнительными свойствами:

Таблица 8-3. Относящиеся к состоянию потока исходные файлы

Поле
Тип
Назначение

f_back

PyFrameObject *

Указатель на предыдущий кадр в стеке или NULL для самого первого кадра

f_blockstack

PyTryBlock[]

Последовательность блоков for, try и loop

f_builtins

PyObject * (dict)

Символьная таблица для модуля builtin

f_code

PyCodeObject *

Подлежащий исполнению кодовый объект

f_executing

char

Флаг того исполняется ли всё ещё данный кадр

f_gen

PyObject *

Заимствованная ссылка на генератор, или NULL

f_globals

PyObject * (dict)

Глобальная символьная таблица (PyDictObject)

f_iblock

int

Индекс этого кадра в f_blockstack

f_lasti

int

Последняя инструкция, если она вызывается

f_lineno

int

Текущий номер строки

f_locals

PyObject *

Локальная символьная таблица (все соответствия)

f_localsplus

PyObject *[]

Соединение locals плюс stack

f_stacktop

PyObject **

Следующий свободный слот в f_valuestack

f_trace

PyObject *

Указатель на некую индивидуальную функцию отслеживания (см. раздел Отслеживание выполнения кадра)

f_trace_lines

char

Включает пользовательскую функцию отслеживания для трассировки на уровне строки

f_trace_opcodes

char

Включает пользовательскую функцию отслеживания для трассировки на уровне кода операции

f_valuestack

PyObject **

Указатель на самый последний локальный элемент

  Относящиеся к делу исходные файлы

Вот те исходные файлы, которые относятся к объектам кадра:

Таблица 8-4. Относящиеся к объектам кадров исходные файлы

Файл
Назначение

Objects/frameobject.c

Реалиация и API Python кадра объекта

Include/frameobject.h

API объекта кадра и определение типа

  API инициализации объекта кадра

API для инициализации объекта кадра, PyEval_EvalCode(), выступает точкой входа для вычисления объекта кода. PyEval_EvalCode() является обёрткой вокруг внутренней функции _PyEval\_EvalCode().

[Замечание]
Замечание

PyEval_EvalCode() это сложная функция, которая определяет большое число поведений как объектов кадров, так и самого цикла интерпретатора. Именно она очень важна для понимания, ибо она также способна обучить вас некоторым принципам проектирования интерпретатора CPython.

В этм разделе вы пошагово пройдёте логикой _PyEval\_EvalCode().

PyEval_EvalCode() определяет большое число аргументов:

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

_co: PyCodeObject* содержит тот код, который должен быть помещён в объект кадра

globals: PyObject* (dict) с названиями ключей в качестве ключей и их значениями

locals: PyObject* (dict) с названиями ключей в качестве ключей и их значениями

[Замечание]
Замечание

В Python локальные и глобальные переменные хранятся в каче тве словарей. Вы можете получать доступ к этим словарям при помощи встроенных функций cals() и globals():

>>> a = 1
>>> print(locals()["a"])
1

Все прочие аргументы не обязательные и не применяются в базовом API:

argcount: Значение числа позиционных аргументов

args: PyObject* (tuple) с со значениями позиционных аргументов по порядку

closure: Кортеж со строками для слияния с полем co_freevars соответствующего кодового объекта

defcount: Значение длины установленных по умолчанию значений для позиционных аргументов

defs: Перечень значений по умолчанию для позиционных аргументов

kwargs: Список значений аргументов кодового слова

kwcount: Значение числа аргументов кодового слова

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

kwnames: Список имён аргументов кодового слова

name: Название для данного вычисляемого предложения в виде строки

qualname: Полностью определённое название для данного вычисляемого предложения в виде строки

Вызов _PyFrame_New_NoTrack() создаёт новый кадр. Этот API также доступен из API C через PyFrame_New(). _PyFrame_New_NoTrack() создаст новый PyFrameObject следуя таким шагам:

Установит значение свойства f_back равным состоянию потока последнего кадра.

Загрузит необходимые текущие встроенные функции через установку свойства f_builtins и загрузит сам модуль builtins при помощи PyModule_GetDict().

Установит значение свойства f_code на подлежащий вычислению кодовый блок.

Установит значение свойства f_valuestack на некий пустой стек значений

Установит значение указателя f_stacktop на f_valuestack

Установит значение глобального свойства, f_globals, на значение аргумента globals

Установит значение локального свойства, f_locals, на некий новый словарь

Установит значение f_lineno равным свойству co_firstlineno кодового объекта с тем, чтобы трассировщик содержал номера строк

Установит значения всех остающихся свойств в их значения по умолчанию

Обладая новым экземпляром PyFrameObject, могут быть построены все аргументы для его кодового объекта:

Рисунок 8-2

  Преобразование параметров ключевых слов в словарь

Определения функций могут содержать вместилище всего **kwargs в качестве аргументов ключевых слов:

def example(arg, arg2=None, **kwargs):
    print(kwargs["x"], kwargs["y"])  # resolves to a dictionary key
example(1, x=2, y=3)  # 2 3

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

  Преобразование позиционных аргументов в переменные

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

def example(arg1, arg2):
    print(arg1, arg2)
example(1, 2)  # 1 2

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

  Упаковка позиционных аргументов в *args

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

def example(arg, *args):
    print(arg, args[0], args[1])

example(1, 2, 3)  # 1 2 3

  Загрузка аргументов ключевых слов

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

Например, когда аргумент e является ни позиционным, не именованным, он добавляется в **remaining:

>>> def my_function(a, b, c=None, d=None, **remaining):
       print(a, b, c, d, remaining)

>>> my_function(a=1, b=2, c=3, d=4, e=5)
(1, 2, 3, 4, {"e": 5})

[Замечание]
Замечание

Исключительно позиционные аргументы (Positional-only arguments) являются новой функциональной возможностью в Python 3.8. Введённые в PEP 570, исключительно позиционные аргументы представляют собой способ прекращения пользователями вашего API применения позиционных аргументов в синтаксисе ключевых слов.

Например, эта простая функция преобразовывает градусы по Фаренгейту в градусы по Цельсию. Обратите внимание на использование прямого слэша (/) в качестве особого аргумента, который отделяет исключительно позиционные аргументы от прочих аргументов:

def to_celsius(fahrenheit, /, options=None):
    return (fahrenheit-32)*5/9

Все аргументы слева от / обязаны вызываться исключительно как позиционные аргументы. Аргументы справа могут вызываться и как позиционные, и как аргументы с ключевыми словами:

>>> to_celsius(110)

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

>>> to_celsius(fahrenheit=110)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: to_celsius() got some positional-only arguments
 passed as keyword arguments: 'fahrenheit'

Собственно разрешение значений словаря аргументов ключевых слов происходит после распаковки всех аргументов. PEP 570 исключительно позиционные аргументы отображаются запуском цикла аргументов ключевых слов начиная с co_posonlyargcount. Если символ / применялся в качестве третьего аргумента, тогда значением co_posonlyargcount будет равен 2.

PyDict_SetItem() вызывается для каждого из оставшихся аргументов для их добавления в словарь locals. После его исполнения все аргументы ключевых слов войдут в сферу действия локальных переменных.

Когда аргумент ключевого слова определяется с неким значением, тогда и оно доступно внутри этой сферы:

def example(arg1, arg2, example_kwarg=None):
    print(example_kwarg)  # example_kwarg is already a local variable.

  Добавление пропущенных позиционных аргументов

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

  Добавление пропущенных аргументов ключевых слов

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

  Свёртывание замыканий

Все названия замыканий добавляются в список свободных имён переменных объекта кода.

  Создание генераторов, сопрограмм и асинхронных генераторов

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

[Совет]
Смотри также

API и реализация кадров генераторов, сопрограмм и асинхронных кадров обсуждаются в Главе 10, Параллельность и одновременность.

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

Наконец, с новым кадром вызывается _PyEval_EvalFrame().

Исполнение кадра

Как уже обсуждалось ранее в Главе 6, Лексический и синтаксический разбор при помощи синтаксических деревьев и в Главе 7, Собственно компилятор объект кода содержит бинарное кодирование подлежащих исполнению байтовых кодов. Он также содержит перечень переменных и и символическую таблицу.

Значения локальных и глобальных переменных определяются во время исполнения на основании того как вызывались функции, модуль или блок. Эти сведения добавляются в соответствующий кадр через _PyEval_EvalCode().

Имеются и иные применения кадров, такие как декоратор сопрограммы, который динамически вырабатывает некий кадр с соответствующей целью в качестве переменной.

Общедоступный API, PyEval_EvalFrameEx(), вызывает настроенный интерпретатором кадр вычисляемой функции в свойстве eval_frame. Вычисление кадра было сделано подключаемым в Python 3.7 через PEP 523.

_PyEval_EvalFrameDefault() является установленной по умолчанию функцией вычисления и единственным поставляемым с CPython вариантом.

Эта центральная функция объединяет всё воедино и воспроизводит ваш код к жизни. Она содержит десятилетия оптимизации, ибо даже одна строка кода способна оказать значительное воздействие на на производительность CPython в целом.

Всё что подлежит исполнению через CPython проходит через эту функцию вычисления.

[Замечание]
Замечание

Некоторые из вас при чтении Python/ceval.c могли заметить насколько много раз применялись макросы C..

Макросы C это некий способ обладания повторно применяемым кодом без накладных расходов выполнения вызова функции..

В Visual Studio Code идущие по ходу дела расширения макро показываются после того как вы установили официальное расширение C/C++:

Рисунок 8-3

В CLion выберите макро и нажмите Alt + Space, чтобы подцепить его определение.

  Отслеживание выполнения кадра

Вы можете пошагово проследовать за исполнением Python 3.7 и выше включив атрибут отслеживания в своём текущем потоке. Тип PyFrameObject содержит свойство f_trace с типом PyObject *. Ожидается, что это значение указывает на некую функцию Python.

Этот код примера устанавливает глобальную функцию отслеживания в функцию с названием my_trace(), которая получает в качестве стека стек из текущего кадра, выводит на печать в экране дизассемблированные коды операций и добавляет некоторые дополнительные сведения для отладки, cpython-book-samples/31/my_trace.py:

import sys
import dis
import traceback
import io

def my_trace(frame, event, args):
   frame.f_trace_opcodes = True
   stack = traceback.extract_stack(frame)
   pad = "   "*len(stack) + "|"
   if event == "opcode":
      with io.StringIO() as out:
         dis.disco(frame.f_code, frame.f_lasti, file=out)
         lines = out.getvalue().split("\n")
         [print(f"{pad}{l}") for l in lines]
   elif event == "call":
      print(f"{pad}Calling {frame.f_code}")
   elif event == "return":
      print(f"{pad}Returning {args}")
   elif event == "line":
      print(f"{pad}Changing line to {frame.f_lineno}")
   else:
      print(f"{pad}{frame} ({event} - {args})")
   print(f"{pad}----------------------------------")
   return my_trace
sys.settrace(my_trace)

# Run some code for a demo
eval('"-".join([letter for letter in "hello"])')

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

Этот фрагмент кода выводит на печать сам код внутри каждого стека и перед тем как выполнить её, указывает следующую операцию. После того как кадр возвращает значение, выводится на печать это возвращаемое предложение:

Рисунок 8-4

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

Стек значений

Внутри ядра цикла вычисления создаётся стек значений. Этот стек является списком указателей на экземпляры PyObject. Они могут быть такими значениями как переменные, ссылки на функции (которые являются объектами Python) или иные прочие объекты Python.

Инструкции байтового кода в цикле вычислений будут получать входные данные из этого стека значений.

  Пример операции байтового кода: BINARY_OR

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

Например, допустим мы вставили некую операцию or в Python:

if left or right:
    pass

Наш компилятор скомпилирует эту операцию or в инструкцию BINARY_OR:

static int
binop(struct compiler *c, operator_ty op)
{
    switch (op) {
    case Add:
        return BINARY_ADD;
    ...
    case BitOr:
        return BINARY_OR;

В нашем цикле вычислений вариант для BINARY_OR получит два значения из стека значений, операции left и right, а затем вызовет PyNumber_Or для этих двух объектов:

...
    case TARGET(BINARY_OR): {
        PyObject *right = POP();
        PyObject *left = TOP();
        PyObject *res = PyNumber_Or(left, right);
        Py_DECREF(left);
        Py_DECREF(right);
        SET_TOP(res);
        if (res == NULL)
            goto error;
        DISPATCH();
    }

Полученный результат, res, затем устанавливается вершиной этого стека, перекрывая текущее значение вершины.

  Имитация стека значений

Чтобы понять цикл вычислений, вам придётся разобраться c его стеком значений.

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

В CPython вы можете вы можете добавлять объекты в стек значений при помощи макро PUSH(a), где a это указатель на PyObject.

Например, допустим, вы создали PyLong со значением 10 и поместите его в свой стек значений:

PyObject *a = PyLong_FromLong(10);
PUSH(a);

Это действие имело бы такой эффект:

Рисунок 8-5

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

PyObject *a = POP(); // a is PyLongObject with a value of 10

Это действие вернёт верхнее значение и завершится опустошением стека:

Рисунок 8-6

Теперь, допустим, вы добавили в стек два значения:

yObject *a = PyLong_FromLong(10);
PyObject *b = PyLong_FromLong(20);
PUSH(a);
PUSH(b);

Это бы завершилось в порядке такого добавления, а потому a оказалось бы помещённым на второй позиции в стеке:

Рисунок 8-7

Если бы вы выполнили выборку с вершины стека, вы бы получили указатель на b, потому что он находился в вершине:

PyObject *val = POP(); // returns ptr to b

Рисунок 8-8

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

PyObject *val = POP(); // returns ptr to b

0 представляет вершину стека, 1 будет представлять вторую позицию:

Рисунок 8-9

Для клонирования своего значения в вершине стека вы можете применять макро DUP_TOP():

DUP_TOP();

Это действие скопирует значение в вершине для формирования двух указателей на один и тот же объект:

Рисунок 8-10

Макро обращения ROT_TWO поменяет местами первое и второе значения:

ROT_TWO();

Это действие переключит порядок первого и второго значений:

Рисунок 8-11

  Действия стека

Каждая из этих кодовых операций обладает предопределённым воздействием на стек, вычисляемым stack_effect() внутри Python/compile.c. Эта функция возвращает значение приращения в числе значений внутри стека значений для каждого кода операции.

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

Пример: Добавление элемента в список

В Python, когда вы создаёте некий список, в этом перечне объектов доступен метод append():

my_list = []
my_list.append(obj)

В данном примере obj выступает неким объектом, который вы желаете добавить в конец своего списка.

В это действие вовлечены две операции:

LOAD_FAST для загрузки obj в вершину стека значений из списка locals данного кадра

LIST_APPEND для добавления этого объекта

В LOAD_FAST вовлечено пять шагов:

Указатель на obj загружается из GETLOCAL(), где значение загружаемой переменной это аргумент операции. Весь список указателей на переменные хранится в fastlocals, которая является копией атрибута PyFrame из f_localsplus. Аргументом этой операции выступает число указаний на значение индекса в самом массиве указателей fastlocals. Это означает, что Python загружает локальные элементы как копии самих указателей, вместо того чтобы принимать во внимание значения имён переменных.

Если такая переменная более не существует, возбуждается ошибочная ситуация несвязанной локальной переменной.

Значения счётчика ссылок для value (в нашем случае для obj) увеличиваются на единицу.

Значение указателя на obj помещается в вершину стека значений.

Вызывается макро FAST_DISPATCH. Если включено отслеживание, тогда цикл выполняется ещё раз для всех отслеживаний. Когда трассировка отключена, для fast_next_opcode вызывается goto. Этот goto выполняет безусловный переход обратно в вершину нашего цикла за следующей инструкцией.

Вот эти пять шагов в LOAD_FAST:

...
    case TARGET(LOAD_FAST): {
        PyObject *value = GETLOCAL(oparg);                 // 1.
        if (value == NULL) {
            format_exc_check_arg(
                PyExc_UnboundLocalError,
                UNBOUNDLOCAL_ERROR_MSG,
                PyTuple_GetItem(co->co_varnames, oparg));
            goto error;                                    // 2.
        }
        Py_INCREF(value);                                  // 3.
        PUSH(value);                                       // 4.
        FAST_DISPATCH();                                   // 5.
    }
 ...

Наш указатель на obj теперь в вершине стека значений и исполняется следующая инструкция, LIST_APPEND.

Многие операции байтового кода ссылаются на базовые типы, такие как PyUnicode или PyNumber. Например, LIST_APPEND добавляет в конец списка некий объект. Для достижения этого он вытаскивает значение указателя из стека значений и возвращает значение указателя на самый последний объект в своём стеке.

Такой макро является ярлыком для следующего:

PyObject *v = (*--stack_pointer);

Теперь указатель на obj хранится как obj. Полный список указателей загружается из PEEK(oparg).

Затем для list и v вызываются списки API C для Python. Соответствующий код для этого находится внутри Objects/listobject.c, который вы изучите в Главе 11, Объекты и типы.

Затем делается вызов PREDICT, который предвидит, что следующей операцией будет JUMP_ABSOLUTE. Макро PREDICT обладает вырабатываемым компилятором goto для каждого из потенциальных операций предложения case.

Это означает, что наш ЦПУ способен выполнить безусловный переход к таким инструкциям и ему нет надобности снова выполнять проход по этому циклу:

...
        case TARGET(LIST_APPEND): {
            PyObject *v = POP();
            PyObject *list = PEEK(oparg);
            int err;
            err = PyList_Append(list, v);
            Py_DECREF(v);
            if (err != 0)
                goto error;
            PREDICT(JUMP_ABSOLUTE);
            DISPATCH();
        }
 ...

[Замечание]
Замечание

Некоторые коды операций ходят парами, чт делает возможным предсказание второй операции при исполнении первой. К примеру, за COMPARE_OP зачастую следует POP_JUMP_IF_FALSE или POP_JUMP_IF_TRUE.

Если вы собираете статистические значения кодов операций, тогда у вас имеются два выбора:

Придерживаться включённых предсказаний и воспринимать эти результаты как если бы некие коды операций сочетались.

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

Для кода при многопоточности предсказание операций отключено, так как многопоточность позволяет самому ЦПУ записывать отдельные сведения предсказания ветвлений для каждого кода операции.

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

При каждом создании нового кадра и его вставке в стек, соответствующее значение f_back этого кадра устанавливается на значение текущего кадра перед тем как будет создан новый кадр. Такое вложение кадров становится понятным когда вы просматриваете отслеживание стека, cpython-book-samples/31/example_stack.py:

def function2():
  raise RuntimeError

def function1():
  function2()

if __name__ == "__main__":
  function1()

Вызов этого из командной строки выдаст вам следующее:

$ ./python example_stack.py

Traceback (most recent call last):
  File "example_stack.py", line 8, in
    function1()
  File "example_stack.py", line 5, in function1
    function2()
  File "example_stack.py", line 2, in function2
    raise RuntimeError
RuntimeError

В Lib/traceback.py для отслеживания вы можете воспользоваться walk_stack():

def walk_stack(f):
    """Walk a stack yielding the frame and line number for each frame.

    This will follow f.f_back from the given frame. If no frame is given,
    the current stack is used. Usually used with StackSummary.extract.
    """
    if f is None:
        f = sys._getframe().f_back.f_back
    while f is not None:
        yield f, f.f_lineno
        f = f.f_back

Значение родителя предка (sys._getframe().f_back.f_back) устанавливается в качестве кадра, поскольку вы не желаете в своём отслеживании видеть вызов walk_stack() или print_trace(). Указатель f_back перемещается в самый верх вызовов стека.

sys._getframe() является API Python для получения значения атрибута frame текущего потока.

Вот как мог бы выглядит стек кадров с тремя кадрами, причём каждый со своим объектом кода, а состояние потока указывает на текущий кадр:

Рисунок 8-12

Выводы

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

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

CPython может обладать большим числом циклов вычислений, исполняющих множество кадров в системе. В последующей главе Главе 10, Параллельность и одновременность как используется система стека кадров для того, чтобы CPython работал на множестве ядер или ЦПУ. Кроме того, API объекта кадра CPython позволяет приостанавливать кадры и возобновлять их в виде асинхронного программирования.

Загрузка переменных при помощи стек значений требует выделения памяти и управления ею. Для действенной работы CPython ему требуется основательный процесс управления памятью. В нашей следующей главе вы изучите как работает управление памятью и как она соотносится с теми указателями PyObject, которые используются циклом вычислений.

Глава 10. Параллельность и одновременность

Содержание

Глава 10. Параллельность и одновременность

Модели параллельности и одновременности

Структура процесса

Многопроцессный параллелизм

Ветвление процесса в POSIX

Многопроцессность в Windows

Пакет multiprocessing

Относящиеся к делу файлы

Порождение и ветвление процессов

Создание дочернего процесса

Конвейеризация данных дочернему процессу

Исполнение дочернего процесса

Обмен данными при помощи очередей и конвейеров

Очереди

Конвейеры

Семафоры

Разделяемые состояния между процессами

Пример приложения

Аннотация мультипроцессности

Многопоточность

GIL

Относящиеся к делу исходные файлы

Старт потоков в Python

Состояние потока

Потоки POSIX

Потоки Windows

Аннотация многопоточности

Асинхронное программирование

Генераторы

Структура генератора

Относящиеся к делу исходные файлы

Создание генераторов

Выполнение генераторов

Сопрограммы

Относящиеся к делу исходные файлы

Циклы событий

Пример

Асинхронные генераторы

Подчинённые интерпретаторы

Относящиеся к делу исходные файлы

Пример

Выводы

Самые первые компьютеры проектировались для выполнения одной вещи за раз. Бо́льшая часть их работы состояла в области вычислительной математики. Время шло, и компьютерам потребовалось обрабатывать входные данные из разнообразных источников, некоторые из которых настолько далеки, как расстояния галактик.

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

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

Многозадачность была хорошо востребована до наступления современных многоядерных ЦПУ, поэтому операционная система долгое время была способна разделять совместные ресурсы между множеством процессов.

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

Для некого ЦПУ соответствующий процесс запрашивает время ЦПУ в виде подлежащих исполнению операций. Он делает это распределяя время ЦПУ и планируя процессы по приоритетам:

Рисунок 10-1

Одновременная модель

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

Одновременность (concurrency) это исключительное решение для ведения дел с многозадачностью, однако ЦПУ обладают своими собственными пределами. Некоторые высокопроизводительные компьютеры для растягивания задач применяют развёртывание либо множества ЦПУ, либо множества ядер. Операционные системы предоставляют некий способ планирования процессов по множеству ЦПУ:

Рисунок 10-2

Параллельная модель

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

Чтобы обладать параллельностью, вам требуется множество вычислиьельных элементов. Вычислительные элементы могут быть центральными процессорами ил ядрами.

Для получения одновременности, вам нужен некий способ планирования задач с тем, чтобы простой одной не блокировал имеющиеся ресурсы.

Многие части архитектуры CPhyton абстрагируют сложность операционных систем чтобы предоставить разработчикам простой API. Подход CPhyton к параллельности и одновременности не является исключением.

Модели параллельности и одновременности

CPython предлагает большое число подходов к параллельности и одновременности. Ваш выбор зависит от нескольких факторов. Между моделями, в которые вовлечён CPython также имеются перекрытия.

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

Существует четыре модели, которыми снабжается CPython:

Таблица 10-1. Модели одновременности и параллельности CPython

Подход
Модуль
Одновременность
Параллельность

Обработка в потоках

threading

Да

Нет

Множество процессов

multiprocessing

Да

Да

Асинхронная обработка

asyncio

Да

Нет

Подчинённые интерпретаторы

subinterpreters

Да

Да

Структура процесса

Одна из основных задач операционной системы, такой как Windows, macOS или Linux является контроль над исполняемыми процессами. Эти процессы могут быть приложениями с пользовательским интерфейсом, такие как браузер или IDE. Это также могут быть фоновые процессы, такие как сетевые службы или службы операционной системы.

Для управления этими процессами соответствующая операционная система предоставляет некий API для старта нового процесса. Когда процесс создан, он регистрируется этой операционной системой с тем, чтобы она знала какие процессы выполняются. Процессам придаётся некий уникальный идентификатор (PID). В зависимости от самой операционной системы они могут обладать некоторыми прочими свойствами.

Процессы POSIX обладают минимальным набором свойств, регистрируемых в их операционной системе:

Управляющий терминал

Текущий рабочий каталог

Эффективный групповой идентификатор и эффективный идентификатор пользователя

Дескрипторы файлов и маска режима создания файла

Групповой идентификатор процесса и идентификатор процесса

Идентификатор реальной группы и идентификатор реального пользователя

Корневой каталог

Вы можете видеть эти атрибуты для запущенных процессов в macOS или Linux, выполняя команду ps.

[Совет]
Смотри также

Стандарт IEEE POSIX (1003.1-2017) определяет собственно взаимодействие и стандартное поведение для процессов и потоков.

Windows обладает аналогичным перечнем свойств, однако устанавливает свой собственный стандарт. Файловые полномочия, структуры каталогов и реестр процессов Windows сильно отличается от POSIX.

Представленные Win32_Process процессы Windows могут запрашиваться в WMI, Windows Management Instrumentation (инструментарии управления Windows) времени выполнения, или при помощи Диспетчера задач (Task Manager).

После того как процесс стартовал в некой операционной системе, придаются:

Стек памяти для вызова подпрограмм

Куча (см. раздел Динамическое выделение памяти в C)

Доступ к файлам, блокировкам и сокетам в его операционной системе

Сам ЦПУ вашего компьютера также удерживает некоторые дополнительные сведения при выполнении такого процесса, например:

Некий регистр, содержащий текущую подлежащую исполнению инструкцию или иные прочие данные, необходимые данному процессу для данной инструкции

Указатель инструкций или счётчике программы, указывающий какая инструкция в данной последовательности программы подлежит исполнению

Процесс CPython содержит в себе скомпилированный интерпретатор CPython и скомпилированные модули. Эти модули загружаются во время выполнения и преобразуются в инструкции циклом вычислений CPython:

Рисунок 10-3

Указанные регистр программы и счётчик программы указывают на единственную инструкцию в этом процессе. Это означает, что за один раз может исполняться лишь одна инструкция. Для CPython это означает, что в некий заданный момент времени может исполняться лишь одна инструкция байтового кода Python.

Существует два основных подхода для допуска параллельного выполнения инструкций в неком процессе:

Ответсвить (fork) другой процесс

Породить (spawn) некий поток

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

Многопроцессный параллелизм

Системы POSIX предоставляют некий API для того чтобы всякий процесс мог ответвлять (fork) дочерний процесс. Ветвление процесса это API вызова нижнего уровня к операционной системе, который может делаться любым исполняемым процессом.

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

  Ветвление процесса в POSIX

В качестве некого образца воспользуемся примером приложения преобразования градусов по Фаренгейту в градусы по Цельсию., применявшееся в самом начале раздела Динамическое выделение памяти в C. Вы можете приспособить его для порождения некого дочернего процесса под каждое значение градуса по Фаренгейту вместо вместо вычисления их в последовательности при помощи fork(). Каждый дочерний процесс продолжит работу с этого момента, cpython-book-samples/33/thread_celsius.c:

#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>

static const double five_ninths = 5.0/9.0;

double celsius(double fahrenheit){
    return (fahrenheit - 32) * five_ninths;
}

int main(int argc, char** argv) {
    if (argc != 2)
        return -1;
    int number = atoi(argv[1]);
    for (int i = 1 ; i <= number ; i++ ) {
        double f_value = 100 + (i*10);
        pid_t child = fork();
        if (child == 0) { // Is child process
            double c_value = celsius(f_value);
            printf("%f F is %f C (pid %d)\n", f_value, c_value, getpid());
            exit(0);
        }
    }
    printf("Spawned %d processes from %d\n", number, getpid());
    return 0;
}

Исполнение этой программы из командной строки предоставит вывод подобный следующему:

$ ./thread_celsius 4
110.000000 F is 43.333333 C (pid 57179)
120.000000 F is 48.888889 C (pid 57180)
Spawned 4 processes from 57178
130.000000 F is 54.444444 C (pid 57181)
140.000000 F is 60.000000 C (pid 57182)

Наш родительский процесс (57718) порождает четыре процесса. Для каждого дочернего процесса наша программа продолжается со строки child = fork(), где результирующим значением для child выступает 0. Она затем выполняет свои вычисления, выводит на печать полученное значение и выходит из своего процесса. Наконец, их родительский процесс выводит сколько процессов он породил и свой собственный PID.

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

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

Рисунок 10-4

Самым крупным негативным последствием такого подхода к параллельности является то, что дочерний процесс является точной копией своего родительского процесса.

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

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

Когда такой дочерний процесс создаётся, куча его предка становится доступной этому дочернему процессу. Для отправки сведений обратно своему предку должна применяться некая форма межпроцессного взаимодействия (IPC, interprocess communication).

[Замечание]
Замечание

Модуль os предлагает некую обёртку вокруг fork().

  Многопроцессность в Windows

До сих пор вы знакомились с моделью POSIX. Windows не предоставляет эквивалента fork() и Python обязан (настолько хорошо, насколько это возможно), обладать одним и тем же API для Linux, macOS и Windows.

Чтобы преодолеть это, для порождения другого процесса python.exe с аргументом командной строки -c используется API CreateProcessW(). Этот этап носит название порождения (spawn) процесса и он также возможен в POSIX. На протяжении данной главы вы найдёте ссылку на это.

  Пакет multiprocessing

CPython предоставляет некий API поверх работы с ветвлением процессов в операционной системе, которое делает простым создания параллельности со множеством процессов в Python.

Этот API доступен из пакета multiprocessing, который предоставляет расширенные возможности для сбора в пулы процессов, очередей, ветвлений, создания куч разделяемой памяти, соединения процессов между собой и многое иное.

  Относящиеся к делу файлы

Вот те исходные файлы, которые относятся к многопроцессности:

Таблица 10-2. Относящиеся к множеству процессов исходные файлы

Файл
Назначение

Lib/multiprocessing

Исходный код Python для пакета multiprocessing

Modules/_posixsubprocess.c

Модуль расширения C обёртывающий системный вызов fork()

Modules/_winapi.c

Модуль расширения C обёртывающий API ядра Windows

Modules/_multiprocessing

Модуль расширения C используемый пакетом multiprocessing

PC/msvcrtmodule.c

Интерфейс Python для библиотеки времени исполнения Microsoft Visual C

  Порождение и ветвление процессов

Пакет multiprocessing предлагает три метода для запуска новых параллельных процессов:

Ветвление интерпретатора (только для POSIX)

Порождение нового процесса интерпретатора (POSIX и Windows)

Запуск некого сервера ветвлений, в котором создаётся новый процесс, который затем ответвляет любое число процессов (только POSIX)

[Замечание]
Замечание

Для Windows и macOS, установленным по умолчанию методом является порождение. В Linux по умолчанию установлено ветвление. Вы можете перекрывать установленный по умолчанию метод при помощи multiprocessing.set_start_method().

API Python для запуска нового процесса, принимает вызываемую, target, и кортеж аргументов, args.

Воспользуемся этим примером порождения нового процесса для преобразования градусов Фаренгейта в градусы Цельсия, cpython-book-samples/33/spawn_process_celsius.py:

import multiprocessing as mp
import os

def to_celsius(f):
    c = (f - 32) * (5/9)
    pid = os.getpid()
    print(f"{f}F is {c}C (pid {pid})")

if __name__ == '__main__':
    mp.set_start_method('spawn')
    p = mp.Process(target=to_celsius, args=(110,))
    p.start()

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

Наш предыдущий пример может быть расширен для вычисления диапазона значений в отдельных интерпретаторах Python, cpython-book-samples/33/pool_process_celsius.py:

import multiprocessing as mp
import os

def to_celsius(f):
    c = (f - 32) * (5/9)
    pid = os.getpid()
    print(f"{f}F is {c}C (pid {pid})")

if __name__ == '__main__':
    mp.set_start_method('spawn')
    with mp.Pool(4) as pool:
        pool.map(to_celsius, range(110, 150, 10))

Обратите внимание, что наш вывод показывает тот же самый PID. По той причине что процесс интерпретатора CPython обладает значительными накладными расходами, Pool будет рассматривать всякий процесс в пуле неким исполнителем (worker). Если исполнитель выполнен, он будет применяться повторно.

Вы можете изменить это установив следующую замену эту строку:

with mp.Pool(4) as pool:

Изменяя её следующим кодом:

with mp.Pool(4, maxtasksperchild=1) as pool:

Теперь наш пример многозадачности будет выводить на печать нечто аналогичное такому:

$ python pool_process_celsius.py
110F is 43.333333333333336C (pid 5654)
120F is 48.88888888888889C (pid 5653)
130F is 54.44444444444445C (pid 5652)
140F is 60.0C (pid 5655)

Данный вывод отображает значения идентификаторов процессов вновь порождённых процессов и вычисленных значений.

  Создание дочернего процесса

Оба этих сценария создадут новый процесс интерпретатора Python и передадут ему данные при помощи pickle.

[Совет]
Совет

Этот модуль pickle является пакетом упорядочивания, применяемого для преобразования преобразования в упорядоченную последовательность объектов Python. Для дополнительных сведений отправляем вас к книге Real Python The Python pickle Module: How to Persist Objects in Python.

Для систем POSIX создание соответствующего подчинённого процесса модуля multiprocessing является эквивалентом данной команды, где <i> это описатель файлового дескриптора, а <j> это описатель дескриптора конвейера:

$ python -c 'from multiprocessing.spawn import spawn_main; \
  spawn_main(tracker_fd=<i>, pipe_handle=<j>)' --multiprocessing-fork

Для систем Windows вместо файлового дескриптороа отслеживания применяется значение родительского PID, как в той команде, где <k> это значение родительского PID, а <j> это значение описания дескриптора конвейера:

> python.exe -c 'from multiprocessing.spawn import spawn_main; \
  spawn_main(parent_pid=<k>, pipe_handle=<j>)' --multiprocessing-fork

  Конвейеризация данных дочернему процессу

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

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

Самый первый записанный родительским процессом объект это объект данных подготовки. Этот объект является словарём, содержащим некие сведения относительно своего родителя, такие как значение каталога исполнения, метод запуска, все специальные аргументы командной строки, а также sys.path.

Вы можете посмотреть некий пример того что выработано запуском multiprocessing.spawn.get_preparation_data(name):

>>> import multiprocessing.spawn
>>> import pprint
>>> pprint.pprint(multiprocessing.spawn.get_preparation_data("example"))
{'authkey': b'\x90\xaa_\x22[\x18\ri\xbcag]\x93\xfe\xf5\xe5@[wJ\x99p#\x00'
            b'\xce\xd4)1j.\xc3c',
 'dir': '/Users/anthonyshaw',
 'log_to_stderr': False,
 'name': 'example',
 'orig_dir': '/Users/anthonyshaw',
 'start_method': 'spawn',
 'sys_argv': [''],
 'sys_path': [
    '/Users/anthonyshaw',
    ]}

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

Оба объекта данных подготовки и объект процесса упорядочены при помощи модуля pickle и записаны в поток конвейера своего родительского процесса:

Рисунок 10-5

[Замечание]
Замечание

Реализация порождения дочернего процесса и упорядочение процесса POSIX расположены в Lib/multiprocessing/popen_spawn_posix.py.

Аналогичная реализация Windows находится в Lib/multiprocessing/popen_spawn_win32.py.

  Исполнение дочернего процесса

Точка входа дочернего процесса, multiprocessing.spawn.spawn_main(), получает значене аргумента pipe_handle, а также либо parent_pid для Windows, либо tracked_fd для POSIX:

def spawn_main(pipe_handle, parent_pid=None, tracker_fd=None):
    '''
    Run code specified by data received over pipe
    '''
    assert is_forking(sys.argv), "Not forking"

Для Windows соответствующая функция вызовет API OpenProcess значения родительского PID. Это используется для создания файлового дескриптора, fd, конвейера своего родительского процесса:

    if sys.platform == 'win32':
        import msvcrt
        import _winapi

        if parent_pid is not None:
            source_process = _winapi.OpenProcess(
                _winapi.SYNCHRONIZE | _winapi.PROCESS_DUP_HANDLE,
                False, parent_pid)
        else:
            source_process = None
        new_handle = reduction.duplicate(pipe_handle,
                                         source_process=source_process)
        fd = msvcrt.open_osfhandle(new_handle, os.O_RDONLY)
        parent_sentinel = source_process

Для POSIX файловым дескриптором, fd, становится pipe_handle и он дублируется чтобы стать значением parent_sentinel:

    else:
        from . import resource_tracker
        resource_tracker._resource_tracker._fd = tracker_fd
        fd = pipe_handle
        parent_sentinel = os.dup(pipe_handle)

Затем вызывается _main() со значением файлового дескриптора родительского конвейера, fd, а также сигнальной меткой родительского процесса, parent_sentinel. Возвращаемое значение _main() становится кодом выхода для данного процесса и его интерпретатор прекращается:

exitcode = _main(fd, parent_sentinel)
sys.exit(exitcode)

_main() вызывается с fd и parent_sentinel для проверки того что из родительского процесса был выполнен выход в процессе исполненния его потомка.

_main() преобразует упорядоченные двоичные данные байтового потока fd в параллельные . Помните, это файловый дескриптор соответствующего конвейера. Этот процесс применяет ту же самую библиотеку pickle, которой пользуется его родительский процесс:

Рисунок 10-6

Самым первым значением является dict, содержащий соответствующие данные подгоовки. Вторым значением выступает некий экземпляр SpawnProcess, который затем применяется как некий экземпляр для _bootstrap() при:

def _main(fd, parent_sentinel):
    with os.fdopen(fd, 'rb', closefd=True) as from_parent:
        process.current_process()._inheriting = True
        try:
            preparation_data = reduction.pickle.load(from_parent)
            prepare(preparation_data)
            self = reduction.pickle.load(from_parent)
        finally:
            del process.current_process()._inheriting
    return self._bootstrap(parent_sentinel)

_bootstrap() обрабатывает конкретизацию экземпляра BaseProcess из превращённых в параллельные из последовательных данных, а затем вызывается целевая функция с соответствующими аргументами и аргументами ключевых слов. Эта завершающая задача выполняется BaseProcess.run():

    def run(self):
        '''
        Method to be run in subprocess; can be overridden in subclass
        '''
        if self._target:
            self._target(*self._args, **self._kwargs)

Код выхода, self._bootstrap() устанавливается как код выхода и этот дочерний процесс прекращается.

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

Это не позволяет обмениваться данными после запуска соответствующего дочернего процесса. Такая задача выполняется с применением расширений Queue и Pipe объектов.

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

Рисунок 10-7

Следующий исполнитель получает свои данные и инициализирует своё состояние и запускает свою целевую функцию:

Рисунок 10-8

Для совместного использования данных вне инициализации следует применять очереди и конвейеры.

  Обмен данными при помощи очередей и конвейеров

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

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

Семафоры

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

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

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

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

Рисунок 10-9

В операционных системах API семафора отличаются, поэтому имеется некий класс абстракции, multiprocessing.synchronize.Semaphore

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

Реализация такий функций API семафоров располагается в модуле расширения C Modules/_multiprocessing/semaphore.c. Этот модуль расширения предлагаеи метод сигналов для создания, блокирования и высвобождения семафоров с прочими операциями.

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

Для Windows этот макрос пользуется API функциями <winbase.h> для семафоров:

#define SEM_CREATE(name, val, max) CreateSemaphore(NULL, val, max, NULL)
#define SEM_CLOSE(sem) (CloseHandle(sem) ? 0 : -1)
#define SEM_GETVALUE(sem, pval) _GetSemaphoreValue(sem, pval)
#define SEM_UNLINK(name) 0

Для POSIX соответствующий макрос применяет API <semaphore.h>:

#define SEM_CREATE(name, val, max) sem_open(name, O_CREAT | O_EXCL, 0600,...
#define SEM_CLOSE(sem) sem_close(sem)
#define SEM_GETVALUE(sem, pval) sem_getvalue(sem, pval)
#define SEM_UNLINK(name) sem_unlink(name)

Очереди

Очереди это великолепный способ отправки небольших данных от или к множеству процессов.

Вы можете приспособить наш более ранний пример со множеством процессов для применения экземпляра multiprocessing Manager() и создания двух очередей:

inputs для удержания входных значений градусов по Фаренгейту

outputs для удержания получаемых в результате значений градусов по Цельсию

Измените значение размера пула на 2, чтобы у вас было два исполнителя, cpython-book-samples/33/pool_queue_celsius.py:

import multiprocessing as mp

def to_celsius(input: mp.Queue, output: mp.Queue):
    f = input.get()
    # Time-consuming task ...
    c = (f - 32) * (5/9)
    output.put(c)

if __name__ == '__main__':
    mp.set_start_method('spawn')
    pool_manager = mp.Manager()
    with mp.Pool(2) as pool:
        inputs = pool_manager.Queue()
        outputs = pool_manager.Queue()
        input_values = list(range(110, 150, 10))
        for i in input_values:
            inputs.put(i)
            pool.apply(to_celsius, (inputs, outputs))

        for f in input_values:
            print(outputs.get(block=False))

Это выводит на печать список кортежей своей очереди outputs:

$ python pool_queue_celsius.py
43.333333333333336
48.88888888888889
54.44444444444445
60.0

Наш родительский процесс вначале помещает свои входные значения в очередь inputs. Самый первый исполнитель затем получает из этой очереди некий элемент. Всякий раз, когда некий элемент берётся из этой очереди при помощи .get(), в объекте этой очереди применяется блокирующий семафор:

Рисунок 10-10

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

Рисунок 10-11

Первый исполнитель завершил свои вычисления и помещает полученные результаты в очередь outputs:

Рисунок 10-12

Две очереди применяются для разделения входных и выходных значений. В конечном счёте, обрабатываются все данные на входе и очередь outputs заполнена. Полученные значения затем выводятся на печать своим родительским процессом:

Рисунок 10-13

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

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

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

Конвейеры

Внутри пакета multiprocessing имеется тип Pipe. Установленный конвейер возвращает два соединения, родителя и потомка. Оба способны отправлять и получать данные:

Рисунок 10-14

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

Чтобы приспособить наш последний пример на работу с конвейером, потребуется заменить pool.apply() на pool.apply_async(). Это изменяет наше выполнение следующего процесса в некую операцию без блокирования, cpython-book-samples/33/pool_pipe_celsius.py:

import multiprocessing as mp

def to_celsius(child_pipe: mp.Pipe):
    f = child_pipe.recv()
    # time-consuming task ...
    c = (f - 32) * (5/9)
    child_pipe.send(c)

if __name__ == '__main__':
    mp.set_start_method('spawn')
    pool_manager = mp.Manager()
    with mp.Pool(2) as pool:
        parent_pipe, child_pipe = mp.Pipe()
        results = []
        for input in range(110, 150, 10):
            parent_pipe.send(input)
            results.append(pool.apply_async(to_celsius, args=(child_pipe,)))
            print("Got {0:}".format(parent_pipe.recv()))
        parent_pipe.close()
        child_pipe.close()

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

f = child_pipe.recv()

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

child_pipe.send(c)

Если это произойдёт, данные будут разрушены либо в операции получения, либо в операции отправки:

Рисунок 10-15

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

Требуются две блокировки, одна на стороне получения родительского конвейера и другая на конце отправки дочернего конвейера, cpython-book-samples/33/pool_pipe_locks_celsius.py:

import multiprocessing as mp

def to_celsius(child_pipe: mp.Pipe, child_lock: mp.Lock):
    child_lock.acquire(blocking=False)
    try:
        f = child_pipe.recv()
    finally:
        child_lock.release()
    # time-consuming task ... release lock before processing
    c = (f - 32) * (5/9)
    # reacquire lock when done
    child_lock.acquire(blocking=False)
    try:
        child_pipe.send(c)
    finally:
        child_lock.release()

if __name__ == '__main__':
    mp.set_start_method('spawn')
    pool_manager = mp.Manager()
    with mp.Pool(2) as pool:
        parent_pipe, child_pipe = mp.Pipe()
        child_lock = pool_manager.Lock()
        results = []
        for i in range(110, 150, 10):
            parent_pipe.send(i)
            results.append(pool.apply_async(
                to_celsius, args=(child_pipe, child_lock)))
            print(parent_pipe.recv())
        parent_pipe.close()
        child_pipe.close()

Теперь соответствующий процесс исполнителя будет ожидать получения блокировки перед отправкой данных и снова дожидаться овладения другой блокировкой на отправку данных:

Рисунок 10-16

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

  Разделяемые состояния между процессами

До сих пор вы наблюдали как данные совместно используются процессами потомков и родителей.

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

API разделяемой памяти

API процесса сервера

Пример приложения

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

Поверх сетевой среды некий хост способен контактировать в портах, которые нумеруются с 1 до 65535. Распространённые службы обладают стандартными портами. Например, HTTP применяет порт 80, а HTTPS работает в 443. Сканеры портов TCP это распространённый инструмент тестирования для проверки того, что пакеты способны отправляться в некой сетевой среде.

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

check_port() обнаружит отклик от своего host по заданному port при его возникновении. Если это происходит, тогда check_port() добавит этот номер порта в очередь results.

При выполнении данного сценария check_port() вызывается последовательно для портов с номерами от 80 до 100. После его выполнения извлекается очередь results и полученные результаты выводятся на печать в командной строке. С тем чтобы вы могли сопоставлять отличия, здесь будет в самом конце выводиться на печать время исполнения, cpython-book-samples/33/portscanner.py:

from queue import Queue
import socket
import time
timeout = 1.0

def check_port(host: str, port: int, results: Queue):
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.settimeout(timeout)
    result = sock.connect_ex((host, port))
    if result == 0:
        results.put(port)
    sock.close()

if __name__ == '__main__':
    start = time.time()
    host = "localhost"  # Replace with a host you own
    results = Queue()
    for port in range(80, 100):
        check_port(host, port, results)
    while not results.empty():
        print("Port {0} is open".format(results.get()))
    print("Completed scan in {0} seconds".format(time.time() - start))

Наше выполнение выведет на печать полученные открытые порты и значение времени, которое ушло на это:

$ python portscanner.py
Port 80 is open
Completed scan in 19.623435020446777 seconds

Вы можете перестроить этот пример на использование множества процессов. Переставьте свой интерфейс Queue для multiprocessing.Queue и отсканируйте все порты вместе применяя исполнитель pool, cpython-book-samples/33/portscanner_mp_queue.py:

import multiprocessing as mp
import time
import socket

timeout = 1

def check_port(host: str, port: int, results: mp.Queue):
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.settimeout(timeout)
    result = sock.connect_ex((host, port))
    if result == 0:
        results.put(port)
    sock.close()

if __name__ == '__main__':
    start = time.time()
    processes = []
    scan_range = range(80, 100)
    host = "localhost"  # Replace with a host you own
    mp.set_start_method('spawn')
    pool_manager = mp.Manager()
    with mp.Pool(len(scan_range)) as pool:
        outputs = pool_manager.Queue()
        for port in scan_range:
            processes.append(pool.apply_async(check_port,
                                              (host, port, outputs)))
        for process in processes:
            process.get()
        while not outputs.empty():
            print("Port {0} is open".format(outputs.get()))
        print("Completed scan in {0} seconds".format(time.time() - start))

Как вы могли ожидать, это приложение намного быстрее, потому как оно тестирует все порты параллельно:

$ python portscanner_mp_queue.py
Port 80 is open
Completed scan in 1.556523084640503 seconds

  Аннотация мультипроцессности

Работа со множеством процессов предлагает некий масштабируемый параллельный API для Python. Данные могут совместно применяться процессами, а интенсивно использующие ЦПУ исполнители могут разбиваться на параллельные задачи для получения преимуществ компьютеров со множеством ядер или множеством ЦПУ.

Обработка множеством процессов не подходящее решение когда ваша подлежащая выполнению задача ограничена вводом/ выводом больше чем интенсивным использованием ЦПУ. Например, когда вы расширяете на четыре исполнительных процесса чтения и запись в одни и те же файлы, затем один выполнял бы всю имеющуюся работу, а оставшиеся три ожидали бы высвобождения имеющегося блокирования.

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

В обоих подобных ситуациях вы можете обнаружить более удобными наши следующие подходы.

Многопоточность

CPython предоставляет для создания, порождения и управления потоков из Python API как верхнего уровня, так и нижнего уровня.

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

pthreads: потоки POSIX для Linux и macOS

nt threads: потоки NT для Windows

В нашем разделе Структура процесса вы видели, что процесс обладает следующими функциональными возможностями:

Стеком подпрограмм

Кучей памяти

Доступом к файлам, блокировкам и сокетам своей операционной системы

Самым большим ограничением на масштабирование отдельного процесса является то, что его операционная система будет иметь для такого исполнения единственный программный счётчик.

Чтобы обойти это, современные операционные системы позволяют процессам сигнализировать своей операционной системе ветвить их исполнение по множеству потоков.

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

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

Некий отдельный процесс с тремя потоками будет обладать такой структурой:

Рисунок 10-17

[Совет]
Смотри также

В качестве вводного руководства в API потоков Python обратитесь к книге Real Python Intro to Python Threading.

  GIL

Если вы знакомы с потоками NT или потоками POSIX в C, либо вы пользовались другим языком программирования верхнего уровня, тогда вы могли бы ожидать параллельности от множества потоков.

В CPython имеющиеся потоки основываются на API C, но являются потоками Python. Это означает,что всякий поток требует исполнения байтового кода Python через его цикл вычислений.

Имеющийся цикл вычислений Python не предоставляет безопасность потокам. Существует множество частей состояния интерпретатора, например, сборщик мусора, которые используются совместно и глобально. Чтобы обойти это, разработчики CPython реализовали некую мега- блокировку с названием GIL (global interpreter lock, глобальную блокировку интерпретатора). Прежде чем какой бы то ни было код выполняется в своём цикле вычисления кадра, имеющийся GIL блокируется его потоком. По окончанию выполнения конкретного кода операции, эта GIL высвобождается.

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

Для овладения GIL выполняется вызов к take_gil(). Чтобы освободить её, вызывается drop_gil(). Овладение GIL выполняется внутри самого кадра ядра цикла вычисления, _PyEval_EvalFrameDefault().

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

    if (_Py_atomic_load_relaxed(&ceval->gil_drop_request)) {
        /* Give another thread a chance */
        if (_PyThreadState_Swap(&runtime->gilstate, NULL) != tstate) {
            Py_FatalError("ceval: tstate mix-up");
        }
        drop_gil(ceval, tstate);

        /* Other threads may run now */

        take_gil(ceval, tstate);

        /* Check if we should make a quick exit. */
        exit_thread_if_finalizing(tstate);

        if (_PyThreadState_Swap(&runtime->gilstate, tstate) != NULL) {
            Py_FatalError("ceval: orphan tstate");
        }
    }
...

Несмотря на те ограничения, которые налагает GIL на параллельное исполнение, она превращает многопоточность в Python в очень безопасное и идеальное для одновременного выполнения задач, связанных с вводом/ выводом.

  Относящиеся к делу исходные файлы

Вот те исходные файлы, которые относятся к потокам:

Таблица 10-3. Относящиеся к множеству процессов исходные файлы

Файл
Назначение

Include/pythread.h

API и определение PyThread

Lib/threading.py

API построения потоков и модуль стандартной библиотеки верхнего уровня

Modules/_threadmodule.c

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

Python/thread.c

Расширение C для модуля thread

Python/thread_nt.h

API построения модулей Windows

Python/thread_pthread.h

API построения модулей POSIX

Python/ceval_gil.h

Реализация блокировки GIL

  Старт потоков в Python

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

Вы начнёте с клонирования нашего предыдущего сценария, но с заменой его логики на порождение некого потока для каждого порта с применением threading.Thread(). Это аналогично предыдущему API multiprocessing, где получались вызываемая, target, и кортеж, args.

Запускайте потоки внутри соответствующего цикла, однако не дожидайтесь их завершения. Вместо этого, добавляйте в конец некого списка, threads, соответствующий экземпляр потока:

    for port in range(80, 100):
        t = Thread(target=check_port, args=(host, port, results))
        t.start()
        threads.append(t)

После того как все потоки были созданы, выполнит итеративно проход по списку threads и вызывайте .join() чтобы дожидаться из выполнения:

    for t in threads:
        t.join()

Затем исчерпайте полностью все элементы из своей очереди results и выведите их на печать на экране:

    while not results.empty():
        print("Port {0} is open".format(results.get()))

Вот весь сценарий целиком, cpython-book-samples/33/portscanner_threads.py:

from threading import Thread
from queue import Queue
import socket
import time

timeout = 1.0

def check_port(host: str, port: int, results: Queue):
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.settimeout(timeout)
    result = sock.connect_ex((host, port))
    if result == 0:
        results.put(port)
    sock.close()

def main():
    start = time.time()
    host = "localhost"  # Replace with a host you own
    threads = []
    results = Queue()
    for port in range(80, 100):
        t = Thread(target=check_port, args=(host, port, results))
        t.start()
        threads.append(t)
    for t in threads:
        t.join()
    while not results.empty():
        print("Port {0} is open".format(results.get()))
    print("Completed scan in {0} seconds".format(time.time() - start))

if __name__ == '__main__':
    main()

Когда вы вызовете этот сценарий с построением потоков из своей командной строки, он исполнится быстрее более чем в десять раз чем пример с единственным потоком:

$ python portscanner_threads.py
Port 80 is open
Completed scan in 1.0101029872894287 seconds

Он также исполняется на от 50 до 60 процентов быстрее чем пример со множеством процессов. Помните, что подход со множеством процессов обладает накладными расходами для запуска своих новых процессов. Потоки также обладают накладными расходами, однако они намного меньше.

Вы можете удивиться, Раз GIL означает что лишь одна операция способна выполняться за раз, тогда почему это может быть быстрее?

Вот предложение, которое отнимает 1 - 1000мс:

result = sock.connect_ex((host, port))

В модуле расширения C, Modules/socketmodule.c, эта функция реализует такое соединение, строка 3245 Modules/socketmodule.c:

static int
internal_connect(PySocketSockObject *s, struct sockaddr *addr, int addrlen,
                 int raise)
{
    int res, err, wait_connect;

    Py_BEGIN_ALLOW_THREADS
    res = connect(s-gt;sock_fd, addr, addrlen);
    Py_END_ALLOW_THREADS

Системный вызов connect() окружают макросы Py_BEGIN_ALLOW_THREADS и Py_END_ALLOW_THREADS. Эти макросы определены следующим образом в Include/ceval.h:

#define Py_BEGIN_ALLOW_THREADS { \
                        PyThreadState *_save; \
                        _save = PyEval_SaveThread();
#define Py_BLOCK_THREADS        PyEval_RestoreThread(_save);
#define Py_UNBLOCK_THREADS      _save = PyEval_SaveThread();
#define Py_END_ALLOW_THREADS    PyEval_RestoreThread(_save); \
                 }

Итак, при обращении к Py_BEGIN_ALLOW_THREADS, он вызывает PyEval_SaveThread(). Эта функция изменяет состояние текущего потока на NULL и отбрасывает (drops) GIL, строка 444 Python/ceval.c:

PyThreadState *
PyEval_SaveThread(void)
{
    PyThreadState *tstate = PyThreadState_Swap(NULL);
    if (tstate == NULL)
        Py_FatalError("PyEval_SaveThread: NULL tstate");
    assert(gil_created());
    drop_gil(tstate);
    return tstate;
}

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

После успешного завершения connect() или выхода по таймауту, макро Py_END_ALLOW_THREADS исполняет PyEval_RestoreThread() с изначальным состоянием потока. Значение состояния потока восстанавливается и повторно берётся GIL. Вызов take_gil() это блокирующий вызов, дожидающийся семафора, строка 458 Python/ceval.c:

void
PyEval_RestoreThread(PyThreadState *tstate)
{
    if (tstate == NULL)
        Py_FatalError("PyEval_RestoreThread: NULL tstate");
    assert(gil_created());

    int err = errno;
    take_gil(tstate);
    /* _Py_Finalizing is protected by the GIL */
    if (_Py_IsFinalizing() && !_Py_CURRENTLY_FINALIZING(tstate)) {
        drop_gil(tstate);
        PyThread_exit_thread();
        Py_UNREACHABLE();
    }
    errno = err;

    PyThreadState_Swap(tstate);
}

Это не просто системный вызов, обёрнутый не блокирующей GIL парой Py_BEGIN_ALLOW_THREADS и Py_END_ALLOW_THREADS. Существует более трёх сотен его применений в стандартной библиотеке, включающих:

Выполнение запросв HTTP

Взаимодействие с локальным оборудованием

Шифрование данных

Чтение и запись файлов

  Состояние потока

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

Потоки Python носят название PyThread. Вы кратко рассматривали их в Главе 8, Цикл расчёта.

Потоки Python исполняют код объектов и порождаются своим интерпретатором.

Чтобы напомнить:

CPython обладает единственной средой времени исполнения, которая обладает своим собственным состоянием времени исполнения.

CPython способен обладать одним или множеством интерпертаторов.

Интерпретатор обладает состоянием с названием состояние интерпетатора.

Некий интерпретатор возьмёт объект кода и преобразует его в последовательность объектов кадров.

Некий интерпретатор обладает по крайней мере одним потоком и каждый поток обладает состоянием потока.

Объекты кадров исполняются в стеке, носящем название стека кадров.

CPython ссылается на переменные из стека значений.

Значение состояния интерпретатора содержит связанный список своих потоков.

Среда времени исполнения с единственным потоком, единственным интерпретатором будет обладать следующими состояниями:

Рисунок 10-18

Тип состояния соответствующего потока, PyThreadState, обладает более чем тридцатью свойствами, включая:

Уникальный идентификатор

Связный список с прочими состояниями потоков

Значение состояния породившего его интерпретатора

Значение исполняемого в данный момент кадра

Значение текущей глубины рекурсии

Не обязательные функции отслеживания

Обрабатываемая в данный момент исключительная ситуация

Все обрабатываемые в данный момент асинхронные исключительные ситуации

Стек возбуждённых исключительных ситуаций

Счётчик GIL

Счётчики асинхронного генератора

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

Потоки конкретизируются при помощи своего типа threading.Thread . Это модуль верхнего уровня, который абстрагирует тип PyThread. Экземпляры PyThread управляются модулем расширения C _thread.

Этот модуль _thread обладает точкой входа для запуска нового потока, thread_PyThread_start_new_thread(). start_new_thread() это метод в экземпляре с типом Thread.

Новые потоки конкретизируются в такой последовательности:

Создаётся bootstate, связывается со своей target, причём с аргументами args и kwargs.

Значение bootstate привязывается к состоянию его интерпретатора.

Создаётся новое PyThreadState, которое привязывается к своему текущему интерпретатору.

Включается его GIL, если она ещё не разрешена, при помощи вызова к PyEval_InitThreads().

Полученный новый поток запускается в зависимой от своей операционной системы реализации PyThread_start_new_thread.

Рисунок 10-19

Значение bootstate потока обладает следующими свойствами:

Таблица 10-4. Свойства bootstate потока

Поле
Тип
Назначение

interp

PyInterpreterState*

Ссылка на управляющий этим потоком интерпретатор

func

PyObject * (callable)

Ссылка на свой callable для исполнения в процессе исполнения этого потока

args

PyObject * (tuple)

Аргументы для вызова с ними func

keyw

PyObject * (dict)

Аргументы ключевых слов для вызова с ними func

tstate

PyThreadState *

Состояние потока для этого нового потока

Для bootstate соответствующего потока имеются две реализации PyThread:

Потоки POSIX для Linux и macOS

Потоки NT для Windows

Обе эти реализации создают необходимый поток операционной системы, устанавливают его атрибуты и затем выполняют соответствующий обратный вызов t_bootstrap() изнутри вновь созданного потока.

Эта функция вызывается с единственным аргументом, boot_raw, назначаемом bootstate, построенном в thread_PyThread_start_new_thread().

Эта функция t_bootstrap() выступает интерфейсом между потоком нижнего уровня и средой времени исполнения Python. Такой начальный самораскрутчик проинициализирует свой поток, а затем исполнит вызываемый target при помощи PyObject_Call().

После исполнения свей вызываемой цели соответствующий поток выполняет выход:

Рисунок 10-20

  Потоки POSIX

Потоки POSIX, носящие название pthreads, обладают реализацией в Python/thread_pthread.h. Эта реализация абстрагирует API C <pthread.h> при помощи некоторых дополнительных мер безопасности и оптимизаций.

Потоки способны обладать неким настраиваемым размером стека. Python обладает своей собственной конструкцией кадра стека, который вы изучили в Главе 8, Цикл расчёта. Когда существует некая проблема, вызываемая каким- то рекурсивным циклом, а сам исполняемый кадр достигает значения предела глубины, тогда Python возбуждает RecursionError, которую вы способны обрабатывать при помощи блока try...except в коде Python.

Поскольку pthreads обладают своим собственным размером стека, значение максимальной глубины размеров стека Python и pthread могут вступать в конфликт. Когда значение размера стека потока меньше чем величина максимальной глубины кадра в Python, тогда весь процесс Python целиком может испытать крушение перед возбуждением некой RecursionError.

Во время исполнения значение максимальной глубины в Python можно настраивать при помощи sys.setrecursionlimit(). Во избежание крушений реализация pthread CPython устанавливает величину размера стеа в значение pythread_stacksize состояния своего интерпретатора.

Большинство современных совместимых с POSIX операционных систем поддерживают системное планирование pthreads. Если PTHREAD_SYSTEM_SCHED_SUPPORTED определяется в pyconfig.h, тогда значение pthread устанавливается в PTHREAD_SCOPE_SYSTEM, что означает, что значение приоритета этого потока в планировщике операционной системы принимается на основе сопоставления с прочими потоками в этой системе, а не только с потоками внутри самого процесса Python.

После того как были настроены свойства потоков, при помощи API pthread_create() создаётся необходимый поток. Это запускает функцию самораскрутки изнутри такого нового потока.

Позднее, соответствующий обработчик потока, pthread_t, выполняет выравнивание на unsigned long возвращается чтобы превратиться в надлежащий идентификатор потока.

  Потоки Windows

Потоки Windows реализуются в Python/thread_nt.h, следуя аналогичным, но более простым шаблоном.

Значение размера стека своего нового потока настраивается на значение интерпретатора pythread_stacksize (когда оно установлено). Этот поток затем создаётся при помощи API Windows _beginthreadex() с применением его функции самораскрутки в качестве обратного вызова. Наконец, возвращается значение идентификатора потока.

  Аннотация многопоточности

Это не является исчерпывающим руководством по потокам Pytho. Реализация потока Python обширна и предлагает множество механизмов для совместного использования данных между потоками, блокировок объектов и ресурсов.

Потоки это великолепный, действенный способ улучшения времени выполнения вашего приложения Python когда вы ограничены операциями ввода/ вывода. В этом разделе вы рассмотрели чем является GIL, зачем она существует и какие части общей стандартной библиотеки могут выводиться из- под её ограничений.

Асинхронное программирование

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

Для нашей текущей версии в этой книге, 3.9, считается устаревшим декоратор @coroutine.

Всё ещё доступны следующие системы:

Создание фьючерсов из ключевых слов async

Запуск сопрограмм при помощи ключевых слов yield from

Генераторы

Генераторы Python это возвращающие предложение yield функции и способные постоянно вызываться для выработки последующих значений.

Генераторы зачастую применяются как более действенный в отношении памяти способ зацикливания по значениям в большом блоке данных, таком как некий файл, база данных или при работе с сетевой средой. Объекты генератора возвращаются на месте значения при использовании yield вместо return. Такой объект генератора создаётся из предложения yield и возвращается вызывавшей его стороне.

Эта простая функция генератора даст в результате буквы с a по z, cpython-book-samples/33/letter_generator.py:

def letters():
    i = 97  # Letter 'a' in ASCII
    end = 97 + 26  # Letter 'z' in ASCII
    while i < end:
        yield chr(i)
        i += 1

Если вы вызовете letters(), тогда она не возвратит вам некое значение. Вместо этого она будет возвращать некий объект генератора:

>>> from letter_generator import letters
>>> letters()
<generator object letters at 0x1004d39b0>

В имеющийся синтаксис предложения for встроена возможность перебирать объект генератора пока он не прекратит выдавать значения:

>>> for letter in letters():
...     print(letter)
a
b
c
d
...

Эта реализация использует протокол самого итератора. Объекты, обладающие методом __next__() могут применяться в циклах for и while, либо при помощи встроенного next().

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

Генераторы не исполняются в своём фоновом режиме - они приостанавливаются. Когда вы запрашиваете другое значение, они восстанавливают исполнение. Внутри структуры самого объекта генератора имеется соответствующий объект кадра в том виде, каким он был в самом последнем предложении yield.

  Структура генератора

Объекты генератора создаются шаблонным макро, _PyGenObject_HEAD(prefix).

Этот макро применяется со следующими типами префиксов:

Объекты генератора: PyGenObject(gi_)

Объекты сопрограммы: PyCoroObject(cr_)

Объекты асинхронного генератора: PyAsyncGenObject(ag_)

Позднее в этой главе вы рассмотрите объекты сопрограмм и асинхронного генератора.

Сам тип PyGenObject обладает такими базовыми свойствами:

Таблица 10-5. Базовые свойства PyGenObject

Поле
Тип
Назначение

[x]_code

PyObject * (PyCodeObject*)

Скомпилированная функция, которую выдаёт этот генератор

[x]_exc_state

_PyErr_StackItem

Сведения исключительной ситуации если вызов этого генератора возбуждает исключительную ситуацию

[x]_frame

PyFrameObject*

Текущий объект кадра для данного генератора

[x]_name

PyObject * (str)

Название этого генератора

[x]_qualname

PyObject * (str)

Полностью определённое имя этого генератора

[x]_running

char

Устанавливается в 0 или в 1, когда этот генератор в текущий момент исполняется

[x]_weakreflist

PyObject * (list)

Список слабых ссылок на объекты внутри функции этого генератора

Поверх этих базовых свойств, PyCoroObject обладает таким свойством:

Таблица 10-6. Дополнительное свойство PyCoroObject

Поле
Тип
Назначение

cr_origin

PyObject * (tuple)

Кортеж, содержащий свой исходный кадр и вызывающую сторону

Поверх имеющихся базовых свойств, PyAsyncGenObject обладает такими свойствами:

Таблица 10-7. Дополнительные свойства PyAsyncGenObject

Поле
Тип
Назначение

ag_closed

int

Флаг маркировки того, что этот генератор закрыт

ag_finalizer

PyObject *

Ссылка на финализирующий метод

ag_hooks_inited

int

Флаг маркировки того, что соответствующие специальные точки входа проинициализированы

ag_running_async

int

Флаг маркировки того, что этот генератор исполняется

  Относящиеся к делу исходные файлы

Вот те исходные файлы, которые относятся к генераторам:

Таблица 10-8. Относящиеся к генераторам исходные файлы

Файл
Назначение

Include/genobject.h

API генератора и определение PyGenObject

Objects/genobject.c

Реализация объекта генератора

  Создание генераторов

Когда компилируется функция, содержащая предложение yield, результирующий объект кода обладает неким дополнительным флагом, CO_GENERATOR.

В разделе Построение объектов кадров Главы 8, Цикл расчёта вы изучали как компилированный объект кода преобразуется в объект кадра при его исполнении. В этом процессе имеется специальный вариант для генераторов, сопрограмм и асинхронных генераторов.

_PyEval_EvalCode() проверяет соответствующий объект кода на флаги CO_GENERATOR, CO_COROUTINE и CO_ASYNC_GENERATOR. Когда он обнаруживает один из этих флагов, тогда вместо вычисления этого объекта кода в реальном времени, его функция создаёт некий кадр и возвращает его в объект генератора, сопрограммы или асинхронного генератора применяя, соответственно, PyGen_NewWithQualName(), PyCoro_New() или PyAsyncGen_New():

PyObject *
_PyEval_EvalCode(PyObject *_co, PyObject *globals, PyObject *locals, ...
...
    /* Handle generator/coroutine/asynchronous generator */
    if (co->co_flags & (CO_GENERATOR | CO_COROUTINE | CO_ASYNC_GENERATOR)) {
        PyObject *gen;
        PyObject *coro_wrapper = tstate->coroutine_wrapper;
        int is_coro = co->co_flags & CO_COROUTINE;
        ...
        /* Create a new generator that owns the ready-to-run frame
         * and return that as the value. */
        if (is_coro) {
>>>         gen = PyCoro_New(f, name, qualname);
        } else if (co->co_flags & CO_ASYNC_GENERATOR) {
>>>         gen = PyAsyncGen_New(f, name, qualname);
        } else {
>>>         gen = PyGen_NewWithQualName(f, name, qualname);
        }
        ...
        return gen;
    }
...

Имеющаяся фабрика генератора, PyGen_NewWithQualName(), получает соответствующий кадр и выполняет некие шаги по заполнению полей объекта этого генератора:

Устанавливает свойство gi_code в значение скомпилированного объекта кода

Устанавливает этот генератор как не исполняющийся (gi_running = 0)

Устанавливает списки исключительных ситуаций и слабых ссылок в NULL

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

>>> from letter_generator import letters
>>> gen = letters()
>>> import dis
>>> dis.disco(gen.gi_code)
  2           0 LOAD_CONST               1 (97)
              2 STORE_FAST               0 (i)
...

В Главе 8, Цикл расчёта вы изучили Тип объекта кадра. Объекты кадра содержат локальные и глобальные элементы, самые последние исполненные инструкции и подлежащий выполнению код.

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

  Выполнение генераторов

Всякий раз когда в неком объекте генератора вызывается __next__(), с этим экземпляром генератора вызывается gen_iternext(), который немедленно вызывает gen_send_ex() внутри Objects/genobject.c.

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

gen_send_ex() совместно применяется с генераторами, сопрограммами и асинхронными генераторами и обладает следующими этапами:

Выполняется выборка текущего состояния потока.

Выполняется выборка необходимого объекта кадра из данного объекта генератора.

Если этот генератор выполняется при вызове __next__(), тогда возбуждается ValueError.

Когда этот кадр внутри самого генератора расположен в верху стека:

Если это сопрограмма и эта сопрограмма уже не помечена как закрытая, тогда возбуждается RuntimeError.

Если это некий асинхронный генератор, возбуждается StopAsyncIteration.

Если это стандартный генератор, возбуждается StopIteration.

Когда самая последняя инструкция в этом кадре (f->f_lasti) всё ещё -1 по причине того что она только что была запущена, и если это сопрограмма или асинхронный генератор, тогда любое значение, за исключением None не может быть передано в качестве некого аргумента и возбуждается некая исключительная ситуация.

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

Поле f_back данного кадра это вызывающая сторона, которой возвращаются отправленные значения, поэтому оно устанавливается в значение текущего кадра в этом потоке. Это означает, что такое возвращаемое отправляется вызываемой стороне, не самому создателю этого генератора.

Данный генератор помечается как исполняемый.

Самая последняя исключительная ситуация из имеющихся сведений исключительных ситуаций генератора копируется из самой последней исключительной ситуации состояния его потока.

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

Сам код внутри этого генератора выполняется внутри основного цикла выполнения Python/ceval.c и возвращается полученное значение.

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

Данный генератор помечается как не выполняющийся.

Затем следующие варианты ставят в соответствие возвращаемое значение и все исключительные ситуации перемещаются вызовом в сам генератор. Помните, что генераторы должны возбуждать по своему исчерпанию StopIteration, либо вручную, любо не выдавая некое значение:

Если из этого кадра не возвращается никакое значение, тогда для генераторов возбуждается StopIteration и StopAsyncIteration возбуждается для асинхронных генераторов.

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

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

В конце концов полученный результат возвращается обратно вызывающей стороне __next__().

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

Сопрограммы

Генераторы обладают основным ограничением: они способны выдавать значения только своей непосредственно вызывающей стороне.

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

Например, ваш генератор букв можно повторно собрать в некую функцию утилиты, в которой значением начальной буквы выступает некий аргумент. Применяя yield from вы можете выбирать какой объект генератора возвращать, cpython-book-samples/33/letter_coroutines.py:

def gen_letters(start, x):
    i = start
    end = start + x
    while i < end:
        yield chr(i)
        i += 1

def letters(upper):
    if upper:
        yield from gen_letters(65, 26)  # A--Z
    else:
        yield from gen_letters(97, 26)  # a--z

for letter in letters(False):
    # Lowercase a--z
    print(letter)

for letter in letters(True):
    # Uppercase A--Z
    print(letter)

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

Опираясь на такое поведение генераторов как их способность приостанавливать и возобновлять исполнение, во множестве API Python вновь и вновь появлялось понятие сопрограммы.

Генераторы являются ограниченной формой сопрограммы, потому как вы можете отправлять данные им при помощи метода .send(). Имеется возможность отправки сообщений в двух направлениях между вызывающей стороной и её целью. Сопрограммы также запоминают вызывающую сторону в своём атрибуте cr_origin.

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

Чтобы пометить, что некая функция возвращает какую- то сопрограмму, вы обязаны предварить эту функцию ключевым словом async. Такое ключевое слово async в явном виде указывает на то, что в отличие от генераторов, данная функция возвращает некую сопрограмму вместо какого- то значения.

Для создания сопрограммы, вы определяете некую функцию ключевым словом async def. В данном примере вы добавляете таймер при помощи функции asyncio.sleep() и возвращаете некую строку после пробуждения:

>>> import asyncio
>>> async def sleepy_alarm(time):
...    await asyncio.sleep(time)
...    return "wake up!"
>>> alarm = sleepy_alarm(10)
>>> alarm
<coroutine object sleepy_alarm at 0x1041de340>

Когда вы вызовете эту функцию, она возвратит некий объект сопрограммы.

Существует множество способов исполнения сопрограммы. Самый простой состоит в применении asyncio.run(coro). Исполнив asyncio.run() со своим объектом сопрограммы, далее, после 10 секунд, вы услышите будильник:

>>> asyncio.run(alarm)
'wake up'

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

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

API задачи применяется для планирования и исполнения множества сопрограмм одновременно. Перед планированием задач надлежит запустить некий цикл событий. Основным заданием этого цикла событий является планирование одновременных задач и соединение событий, таких как окончание (completion), отмена (cancellation) и исключительные ситуации (exceptions), с обратными вызовами.

Когда вы вызываете asyncio.run() (из Lib/asyncio/runners.py), эта функция выполняет для вас такие задачи:

Запускает новый цикл событий.

Обёртывает соответствующий объект сопрограммы в некую задачу.

Устанавливает некий обратный вызов по окончанию этой задачи.

Выполняет цикл по всем задачам пока они не завершатся.

Возвращает полученный результат.

  Относящиеся к делу исходные файлы

Вот тот исходный файл, который относится к сопрограммам:

Таблица 10-9. Относящийся к сопрограммам исходный файл

Файл
Назначение

Lib/asyncio

Реализация стандартной библиотеки Python для asyncio

  Циклы событий

Циклы событий выступают тем клеем, который совместно удерживает асинхронный код. Написанные на чистом Python, циклы событий это содержащие задачи объекты.

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

loop = asyncio.new_event_loop()

Внутри некого цикла имеется последовательность задач, представленных типом asyncio.Task. Задачи планируются в неком цикле, а затем, когда этот цикл запущен, он обходит циклом все эти задачи до тех пор, пока они не окончатся.

Вы можете преобразовать отдельный таймер в некий цикл задач, cpython-book-samples/33/sleepy_alarm.py:

import asyncio

async def sleepy_alarm(person, time):
   await asyncio.sleep(time)
   print(f"{person} -- wake up!")

async def wake_up_gang():
    tasks = [
        asyncio.create_task(sleepy_alarm("Bob", 3), name="wake up Bob"),
        asyncio.create_task(sleepy_alarm("Yudi", 4), name="wake up Yudi"),
        asyncio.create_task(sleepy_alarm("Doris", 2), name="wake up Doris"),
        asyncio.create_task(sleepy_alarm("Kim", 5), name="wake up Kim")
    ]
    await asyncio.gather(*tasks)

asyncio.run(wake_up_gang())

Это выдаст на печать следующий вывод:

Doris -- wake up!
Bob -- wake up!
Yudi -- wake up!
Kim -- wake up!

Запущенный цикл событий исполняется по всем своим сопрограммам наблюдая их окончание. Аналогично тому как ключевое слово yield способно возвращать множество значений из одного и того же кадра, ключевое слово await может возвращать множество состояний.

Этот цикл событий будет выполнять имеющиеся объекты сопрограмм sleepy_alarm() снова и снова до тех пор, пока их await asyncio.sleep() не выдаст выполненный результат и не будет способен выполниться print().

Для этой работы вам надлежит воспользоваться asyncio.sleep() вместо блокирующего (и не осведомлённого об асинхронности) time.sleep().

  Пример

Вы можете преобразовать пример сканирования портов со множеством процессов в asyncio следующими шагами:

Заменить check_port() на применение соединения с сокетом из asyncio.open_connection(), что создаст некий фьючерс вместо какого- то немедленного подключения.

Воспользоваться этим фьючерсом подключения к сокету по событию таймера при помощи asyncio.open_connection().

В случае успеха добавить полученный порт в общий список результатов.

Добавить новую функцию, scan(), чтобы создавать сопрограммы check_port() для каждого порта и добавлять их в список tasks.

Слить все полученные tasks в некую новую сопрограмму при помощи asyncio.gather().

Запустить своё сканирование при помощи asyncio.run().

Вот этот код, cpython-book-samples/33/portscanner_async.py:

import time
import asyncio

timeout = 1.0

async def check_port(host: str, port: int, results: list):
    try:
        future = asyncio.open_connection(host=host, port=port)
        r, w = await asyncio.wait_for(future, timeout=timeout)
        results.append(port)
        w.close()
    except OSError:  # pass on port closure
        pass
    except asyncio.TimeoutError:
        pass  # Port is closed, skip and continue

async def scan(start, end, host):
    tasks = []
    results = []
    for port in range(start, end):
        tasks.append(check_port(host, port, results))
    await asyncio.gather(*tasks)
    return results

if __name__ == '__main__':
    start = time.time()
    host = "localhost"  # Pick a host you own
    results = asyncio.run(scan(80, 100, host))
    for result in results:
        print("Port {0} is open".format(result))
    print("Completed scan in {0} seconds".format(time.time() - start))

Данное сканирование оканчивается чуть больше чем за секунду:

$ python portscanner_async.py
Port 80 is open
Completed scan in 1.0058400630950928 seconds

Асинхронные генераторы

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

Когда некая функция объявляется с ключевым словом async и содержит предложение yield, тогда она при вызове преобразуется в объект асинхронного генератора.

Как и генераторы, асинхронные генераторы должны исполнятся чем- то, что понимает протоколы. На месте __next__(), асинхронные генераторы обладают методом __anext__().

Обычный цикл for не понимает асинхронный генератор, поэтому вместо него пользуйтесь предложением async for.

Вы можете переделать check_port() в некий асинхронный генератор, который выдаёт значение следующего открытого порта пока он не столкнётся с самым последним портом или не обнаружит предписанное число открытых портов:

async def check_ports(host: str, start: int, end: int, max=10):
    found = 0
    for port in range(start, end):
        try:
            future = asyncio.open_connection(host=host, port=port)
            r, w = await asyncio.wait_for(future, timeout=timeout)
            yield port
            found += 1
            w.close()
            if found >= max:
                return
        except asyncio.TimeoutError:
            pass # Closed

Для выполнения этого воспользуйтесь предложением async for:

async def scan(start, end, host):
    results = []
    async for port in check_ports(host, start, end, max=1):
        results.append(port)
    return results

Весь пример полностью вы можете найти в cpython-book-samples/33/portscanner_async_generators.py.

Подчинённые интерпретаторы

На данный момент вы рассмотрели:

Параллельное исполнение с применением множества процессов

Одновременное выполнение при помощи потоков и асинхронности

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

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

Четвёртым вариантом является subinterpreters, который обладает меньшими накладными расходами, нежели multiprocessing и делает возможным некий GIL для каждого подчинённого интерпретатора. В конце концов, это глобальная блокировка интерпретатора.

Внутри среды времени исполнения CPython всегда имеется один интерпретатор. Этот интерпретатор удерживает значение состояние интерпретатора, а также внутри интерпретатора у вас есть возможность обладать одним или более потоками Python. Такой интерпретатор выступает надлежащим контейнером для своего цикла вычислений. Он также управляет своей собственной памятью, счётчиком ссылок и сборкой мусора.

CPython обладает API C нижнего уровня для создания интерпретаторов, например, Py_NewInterpreter():

Рисунок 10-21

[Замечание]
Замечание

Модуль всё ещё экспериментальный в 3.9, а потому его API выступает предметом изменений и его реализация всё ещё содержит большое число ошибок.

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

Аналогично работе со множеством процессов, для совместного использования объектов между интерпретаторами вам надлежит упорядочивать их или применять ctypes и пользоваться некой формой IPC (сетевая среда, диск, либо совместная память).

  Относящиеся к делу исходные файлы

Вот те исходные файлы, которые относятся к подчинённым интерпретаторам:

Таблица 10-8. Относящиеся к подчинённым интерпретаторам исходные файлы

Файл
Назначение

Lib/_xxsubinterpreters.c

Реализация C модуля subinterpreters

Python/pylifecycle.c

Реализация C API управления самим интерпретатором

  Пример

В финальном примере приложения, наш код реального подключения должен заключаться в некой строке. В 3.9, подчинённый интерпретатор может исполнять лишь строку кода.

Для запуска каждого подчинённого интерпретатора, запускается некий список потоков с обратным вызовом к функции, run().

Эта функция будет:

Создавать канал взаимодействия

Запускать новый интерпретатор

Отправлять в подчинённый интерпретатор необходимый для исполнения код

Получать данные через установленный канал взаимодействия

В случае успешного подключения к порту, добавлять его в очередь с безопасным потоком, cpython-book-samples/33/portscanner_subinterpreters.py:

import time
import _xxsubinterpreters as subinterpreters
from threading import Thread
import textwrap as tw
from queue import Queue

timeout = 1  # In seconds

def run(host: str, port: int, results: Queue):
    # Create a communication channel
    channel_id = subinterpreters.channel_create()
    interpid = subinterpreters.create()
    subinterpreters.run_string(
        interpid,
        tw.dedent(
    """
    import socket; import _xxsubinterpreters as subinterpreters
    sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM)
    sock.settimeout(timeout)
    result = sock.connect_ex((host, port))
    subinterpreters.channel_send(channel_id, result)
    sock.close()
    """),
        shared=dict(
            channel_id=channel_id,
            host=host,
            port=port,
            timeout=timeout
        ))
    output = subinterpreters.channel_recv(channel_id)
    subinterpreters.channel_release(channel_id)
    if output == 0:
        results.put(port)

if __name__ == '__main__':
    start = time.time()
    host = "127.0.0.1"  # Pick a host you own
    threads = []
    results = Queue()
    for port in range(80, 100):
        t = Thread(target=run, args=(host, port, results))
        t.start()
        threads.append(t)
    for t in threads:
        t.join()
    while not results.empty():
        print("Port {0} is open".format(results.get()))
    print("Completed scan in {0} seconds".format(time.time() - start))

По причине снижения накладных расходов по сравнению с работой со множеством процессов, этот пример должен исполняться на от 30 до 40 процентов быстрее и с меньшими ресурсами памяти:

$ python portscanner_subinterpreters.py
Port 80 is open
Completed scan in 1.3474230766296387 seconds

Выводы

Поздравляем с освоением самой большой главы в этой книге! Вы прошли большой путь. Давайте вспомним некоторые понятия и способы их применения.

Для действительного параллельного выполнения вам необходимо множество ЦПУ и ядер. Вам также требуется применять либо пакет multiprocessing, либо пакет subinterpreters с тем, чтобы интерпретатор Python был способен исполняться параллельно.

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

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

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

Глава 12. Стандартная библиотека

Содержание

Глава 12. Стандартная библиотека

Модули Python

Python и модули C

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

Некоторые из поставляемых совместно с CPython батареек подобны батарейкам АА и полезны практически при любой ситуации. В качестве примеров можно привести модули collections и sys. Но некоторые из них слегка менее известны, превращая их в некое подобие батареек от наручных часов: вы никогда не знаете наперёд когда они вам понадобятся.

Имеются два типа модулей в стандартной библиотеке CPython:

Те, кторые написаны на чистом Python и которые предоставляют утилиты

Те, которые написаны на C с обёрткой из Python

В данной главе мы изучим оба этих типа.

Модули Python

Все те модули, которые написаны на чистом Python, располагаются в каталоге Lib исходного кода. Некоторые более старшие модули обладают подчинёнными модулями в подчинённых папках, такие как модуль email.

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

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

Например, вот как импортировать и применять colorsys:

>>> import colorsys
>>> colorsys
<module 'colorsys' from '/usr/shared/lib/python3.7/colorsys.py'>

>>> colorsys.rgb_to_hls(255,0,0)
(0.0, 127.5, -1.007905138339921)

Вы можете обнаружить исходный код rgb_to_hls() внутри Lib/colorsys.py:

# HLS: Hue, Luminance, Saturation
# H: position in the spectrum
# L: color lightness
# S: color saturation

def rgb_to_hls(r, g, b):
    maxc = max(r, g, b)
    minc = min(r, g, b)
    # XXX Can optimize (maxc+minc) and (maxc-minc)
    l = (minc+maxc)/2.0
    if minc == maxc:
        return 0.0, l, 0.0
    if l <= 0.5:
        s = (maxc-minc) / (maxc+minc)
    else:
        s = (maxc-minc) / (2.0-maxc-minc)
    rc = (maxc-r) / (maxc-minc)
    gc = (maxc-g) / (maxc-minc)
    bc = (maxc-b) / (maxc-minc)
    if r == maxc:
        h = bc-gc
    elif g == maxc:
        h = 2.0+rc-bc
    else:
        h = 4.0+gc-rc
    h = (h/6.0) % 1.0
    return h, l, s

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

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

Python и модули C

Все остающиеся модули написаны на C или на комбинации Pyton и C. Их исходный код расположен в Lib для компонентов Pyton и в Modules для компонентов C. Имеются два исключения:

Модуль sys находится в Python/sysmodule.c

А модуль __builtins__ обнаруживается в Python/bltinmodule.c

Поскольку этот модуль sys настолько специфичен для своего интерпретатора и внутреннего устройства CPython, он обнаруживается в самом каталоге Python. Он также помечается в качестве "подробности реализации" CPythonи не находится в прочих дистрибутивах.

Python будет import * from __builtins__ при конкретизации интерпретатора, поэтому все такие встроенные функции, как print(), chr(), format() и тому подобные обнаруживаются внутри Python/bltinmodule.c.

Встроенная функция print() вероятно была самой первой изученной вами для применения в Python. Итак, что в точности происходит когда вы набираете print("Hello, World")?

Вот разбивка:

Наш компилятор преобразовывает полученный аргумент "Hello, World" из строковой константы в PyUnicodeObject.

builtin_print() исполняется с одним аргументом и NULL kwnames.

Значение переменной file устанавливается в PyId_stdout, системный обработчик stdout.

Все аргументы отправляются в file.

В file отправляется разрыв строки (\n).

Вот как это работает, строка 1828 Python/bltinmodule.c :

static PyObject *
builtin_print(PyObject *self, PyObject *const *args,
    Py_ssize_t nargs, PyObject *kwnames)
{
    ...
    if (file == NULL || file == Py_None) {
        file = _PySys_GetObjectId(&PyId_stdout);
        ...
    }
    ...
    for (i = 0; i < nargs; i++) {
        if (i > 0) {
            if (sep == NULL)
                err = PyFile_WriteString(" ", file);
            else
                err = PyFile_WriteObject(sep, file,
                                         Py_PRINT_RAW);
            if (err)
                return NULL;
        }
        err = PyFile_WriteObject(args[i], file, Py_PRINT_RAW);
        if (err)
            return NULL;
    }

    if (end == NULL)
        err = PyFile_WriteString("\n", file);
    else
        err = PyFile_WriteObject(end, file, Py_PRINT_RAW);
    ...
    Py_RETURN_NONE;
}

Содержимое некоторых модулей написано на C, выставляющем функции операционной системы. Поскольку исходный код CPyton необходимо компилировать в to macOS, Windows, Linux и прочих основанных на *nix операционных системах, существуют некие особые случаи.

Хорошим примером выступает модуль time. Тот способ, коим Windows удерживает и сохраняет время в своей операционной системе фундаментально отличается от Linux и macOS. Это одна из причин, по которой значение точности часов отличается в операционных системах.

В Modules/timemodule.c, функции времени операционной системы для основанных на Unix систем импортируется из <sys/times.h>:

#ifdef HAVE_SYS_TIMES_H
#include <sys/times.h>
#endif
...
#ifdef MS_WINDOWS
#define WIN32_LEAN_AND_MEAN
#include <windows.h>
#include "pythread.h"
#endif /* MS_WINDOWS */
...

Позднее в этом файле, time_process_time_ns(), определяется некая оболочка для _PyTime_GetProcessTimeWithInfo():

static PyObject *
time_process_time_ns(PyObject *self, PyObject *unused)
{
    _PyTime_t t;
    if (_PyTime_GetProcessTimeWithInfo(&t, NULL) < 0) {
        return NULL;
    }
    return _PyTime_AsNanosecondsObject(t);
}

_PyTime_GetProcessTimeWithInfo() реализуется множеством различных способов в имеющемся исходном коде, однако лишь одна определённая часть компилируется в исполняемый файл для этого модуля в зависимости от установленной операционной системы. Системы Windows будут вызывать GetProcessTimes(), а системы Unix будут вызывать clock_gettime().

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

Глава 13. Комплект тестов

Содержание

Глава 13. Комплект тестов

Выполнение комплекта тестов в Windows

Выполнение комплекта тестов в Linux или macOS

Флаги теста

Исполнение особых тестов

Модули тестирования

Утилиты тестирования

Выводы

CPython обладает устойчивым комплектом тестирования, охватывающим собственно ядро интерпретатора, его стандартную библиотеку , имеющиеся инструменты и сам дистрибутив для Windows, Linux и macOS. Он располагается в Lib/test и написан в основном на Python. Весь комплект тестирования является пакетом Python, а потому вы можете исполнить его применяя тот интерпретатор Python, который вы скомпилировали.

Выполнение комплекта тестов в Windows

В Windows воспользуйтесь сценарием rt.bat внутри папки PCBuild. Например, вот как запустить быстрый режим для конфигурации с отладкой в архитектуре x64:

> cd PCbuild
> rt.bat -q -d -x64

== CPython 3.9
== Windows-10-10.0.17134-SP0 little-endian
== cwd: C:\repos\cpython\build\test_python_2784
== CPU count: 2
== encodings: locale=cp1252, FS=utf-8
Run tests sequentially
0:00:00 [  1/420] test_grammar
0:00:00 [  2/420] test_opcodes
0:00:00 [  3/420] test_dict
0:00:00 [  4/420] test_builtin

Для запуска возвратного комплекса тестов для конфигурации выпуска (Release), удалите флаг -d из командной строки.

Выполнение комплекта тестов в Linux или macOS

В Linux и macOS исполните цель test make для компиляции и запустите тесты:

$ make test
== CPython 3.9
== macOS-10.14.3-x86_64-i386-64bit little-endian
== cwd: /Users/anthonyshaw/cpython/build/test_python_23399
== CPU count: 4
== encodings: locale=UTF-8, FS=utf-8
0:00:00 load avg: 2.14 [  1/420] test_opcodes passed
0:00:00 load avg: 2.14 [  2/420] test_grammar passed

В качестве альтернативы воспользуйтесь путём скомпилированного исполняемого файла python или python.exe с пакетом python:

$ ./python -m test
== CPython 3.9
== macOS-10.14.3-x86_64-i386-64bit little-endian
== cwd: /Users/anthonyshaw/cpython/build/test_python_23399
== CPU count: 4
== encodings: locale=UTF-8, FS=utf-8
0:00:00 load avg: 2.14 [  1/420] test_opcodes passed
0:00:00 load avg: 2.14 [  2/420] test_grammar passed
...

Вот дополнительные цели make для тестов:

Таблица 13-1. Цели тестирования для make

Цель
Назначение

test

Запустить базовый набор тестов возврата

testall

Выполнить полный комплект тестов дважды - один раз без файлов .pyc, второй раз с ними

quicktest

Исполнить более быстрое возвратное тестирование, исключая те тесты, которые занимают много времени

testuniversal

Запуск комплекта тестов для osX для обеих архитектур в универсальной сборке

coverage

Скомпилировать и запустить тесты с gcov

coverage-lcov

Создать отчёты HTML областей действия

Флаги теста

Некоторые тесты требуют определённые флаги, либо они будут пропущены. Например, многие из тестов IDLE требуют GUI.

Чтобы просмотреть список комплектов тестов в вашей конфигурации воспользуйтесь флагом --list-tests:

./python -m test --list-tests

test_grammar
test_opcodes
test_dict
test_builtin
test_exceptions
...

Исполнение особых тестов

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

Вот некий пример для Linux и macOS:

$ ./python -m test test_webbrowser

Run tests sequentially
0:00:00 load avg: 2.74 [1/1] test_webbrowser

== Tests result: SUCCESS ==

1 test OK.

Total duration: 117 ms
Tests result: SUCCESS

Вот пример в Windows:

> rt.bat -q -d -x64 test_webbrowser

Вы также можете просмотреть подробный список исполненных тестов совместно с их результатами, воспользовавшись аргументом -v:

$ ./python -m test test_webbrowser -v

== CPython 3.9
== macOS-10.14.3-x86_64-i386-64bit little-endian
== cwd: /Users/anthonyshaw/cpython/build/test_python_24562
== CPU count: 4
== encodings: locale=UTF-8, FS=utf-8
Run tests sequentially
0:00:00 load avg: 2.36 [1/1] test_webbrowser
test_open (test.test_webbrowser.BackgroundBrowserCommandTest) ...ok
test_register (test.test_webbrowser.BrowserRegistrationTest) ...ok
test_register_default (test.test_webbrowser.BrowserRegistrationTest) ...ok
test_register_preferred (test.test_webbrowser.BrowserRegistrationTest) ...ok
test_open (test.test_webbrowser.ChromeCommandTest) ...ok
test_open_new (test.test_webbrowser.ChromeCommandTest) ...ok
...
test_open_with_autoraise_false (test.test_webbrowser.OperaCommandTest) ...ok

----------------------------------------------------------------------

Ran 34 tests in 0.056s

OK (skipped=2)

== Tests result: SUCCESS ==

1 test OK.

Total duration: 134 ms
Tests result: SUCCESS

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

Модули тестирования

Для расширений C или модулей Python вы можете импортировать их и провести тестирование применяя модуль unittest. Тесты собираются по модулям или пакетам.

Например, тип строк Unicode Python обладает тестами в Lib.test.test_unicode.py. Пакет asyncio имеет тесты в Lib/test/test_asyncio.

[Совет]
Смотри также

Если вы новичок в отношении модуля unittest или тестирования в Python, отсылаем вас к книге Real Python “Getting Started With Testing in Python”.

Вот некое извлечение из класса UnicodeTest:

class UnicodeTest(string_tests.CommonTest,
        string_tests.MixinStrUnicodeUserStringTest,
        string_tests.MixinStrUnicodeTest,
        unittest.TestCase):
...
    def test_casefold(self):
        self.assertEqual('hello'.casefold(), 'hello')
        self.assertEqual('hELlo'.casefold(), 'hello')
        self.assertEqual('ß'.casefold(), 'ss')
        self.assertEqual('fi'.casefold(), 'fi')

Вы можете расширить свой оператор почти- равенства, который вы реализовали в более ранних главах для строк Unicode Python, добавив новый метод тестирования внутри имеющегося класса UnicodeTest:

    def test_almost_equals(self):
        self.assertTrue('hello' ~= 'hello')
        self.assertTrue('hELlo' ~= 'hello')
        self.assertFalse('hELlo!' ~= 'hello')

Вы можете исполнить конкретно этот модуль тестирования в Windows:

> rt.bat -q -d -x64 test_unicode

Или же вы можете запустить его в macOS или Linux:

$ ./python -m test test_unicode -v

Утилиты тестирования

Импортировав модуль test.support.script_helper вы можете получить доступ к вспомогательным функциям для тестирования Python времени исполнения:

assert_python_ok(*args, **env_vars) исполняет некий процесс Python с предписанными аргументами и вернуть некий кортеж (код возврата, stdout, stderr).

assert_python_failure(*args, **env_vars) аналогичен assert_python_ok(), но декларирует что он испытал неудачу.

make_script(script_dir, script_basename, source) изготовляет некий сценарий в script_dir с script_basename и source, затем возвращает значение пути этого сценария. Это полезно в сочетании с assert_python_ok() или assert_python_failure().

Если вы желаете создать некий тест, который будет пропускаться когда этот модуль не был собран, тогда вы можете воспользоваться функцией утилиты test.support.import_module(). Это возбудит SkipTest и выдаст сигнал исполнителю теста пропустить данный пакет тестов. Вот некий пример:

import test.support

_multiprocessing = test.support.import_module('_multiprocessing')

# Your tests...

Выводы

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

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

Глава 15. Эталонное тестирование, профилирование и трассировка

Содержание

Глава 15. Эталонное тестирование, профилирование и трассировка

Использование timeit для микро- эталонного тестирования

Пример timeit

Применение Комплекта эталонного тестирования Python для эталонного тестирования времени выполнения

Исполнение эталонного тестирования

Сопоставление эталонного тестирования

Профилирование кода Python при помощи cProfile

Экспорт профилей

Профилирование кода C при помощи DTrace

Относящиеся к делу исходные файлы

Установка DTrace

Компиляция поддержки DTrace

Применение DTrace из Clion

Пример DTrace

Выводы

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

Существуют решения профилирования, которые вы рассмотрите в этой главе:

Применение модуля timeit для проверки проверки предложения Python тысячи раз для исчисления средней скорости исполнения

Выполнения pyperformance, комплекта эталонного тестирования Python для сопоставления множества версий Python

Использование cProfile для анализа времени исполнения кадров

Профилирование исполнения CPython зондированием

Основной выбор решения зависит от типа задачи:

Эталонное тестирование воспроизведёт среднее или медианное значение времени исполнения фрагмента фиксированного кода с тем, чтобы вы могли сопоставлять множество сред исполнения Python.

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

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

Вот суммирование некоторых доступных инструментов:

Таблица 15-1. Инструменты эталонного тестирования и профилирования

Инструмент
Категория
Уровень
Поддерживаемая ОС

timeit

Эталонное тестирование

Python

Все

pyperformance

Эталонное тестирование

Python

Все

cProfile

Профилирование

Python

Все

DTrace

Трассировка/ профилирование

C

Linux, macOS

[Замечание]
Важно

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

Использование timeit для микро- эталонного тестирования

Комплект эталонного тестирования Python является всесторонним тестом среды времени исполнения CPython со множеством итераций. Если вы желаете выполнить быстрое, простое сопоставление определённого фрагмента кода, воспользуйтесь вместо этого модулем timeit.

Чтобы исполнить timeit для короткого сценария, запустите CPython с модулем -m timeit и сценарий в кавычках:

$ ./python -m timeit -c "x=1; x+=1; x**x"
1000000 loops, best of 5: 258 nsec per loop

Для меньшего числа циклов воспользуйтесь флагом -n:

$ ./python -m timeit -n 1000 "x=1; x+=1; x**x"
1000 loops, best of 5: 227 nsec per loop

  Пример timeit

В этой книге вы внесли изменения поддержки оператора почти равенства в свой тип float.

Попробуйте проверить его чтобы увидеть значение текущей производительности сравнения двух плавающих значений:

$ ./python -m timeit -n 1000 "x=1.0001; y=1.0000; x~=y"
1000 loops, best of 5: 177 nsec per loop

Реализация этого сравнения находится в float_richcompare(), внутри Objects/floatobject.c, строка 358:

static PyObject*
float_richcompare(PyObject *v, PyObject *w, int op)
{
    ...
    case Py_AlE: {
            double diff = fabs(i - j);
            double rel_tol = 1e-9;
            double abs_tol = 0.1;
            r = (((diff <= fabs(rel_tol * j)) ||
                     (diff <= fabs(rel_tol * i))) ||
                    (diff <= abs_tol));
            }
            break;
    }

Обратите внимание на то, что значения rel_tol и abs_tol являются константами, но не были помечены таковыми. Изменим это следующим образом:

const double rel_tol = 1e-9;
const double abs_tol = 0.1;

Теперь скомпилируем CPython снова и повторим свой тест:

$ ./python -m timeit -n 1000 "x=1.0001; y=1.0000; x~=y"
1000 loops, best of 5: 172 nsec per loop

Вы можете отметить незначительное (от 1 до 5 процентов) улучшение в производительности. Поэкспериментируйте с различными реализациями сравнения чтобы увидеть сможете ли вы улучшать её дальше.

Применение Комплекта эталонного тестирования Python для эталонного тестирования времени выполнения

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

Этот комплект эталонного тестирования выполняет тестирование чистого Python, поэтому он может применяться для многих сред исполнения, например, PyPy и Jython. Он также совместим с Python 2.7 вплоть до самых последних версий.

Любые фиксации для основной ветви в github.com/python/cpython будут проверены при помощи инструмента эталонного тестирования и полученные результаты будут разгружены в Python Speed Center:

Рисунок 15-1

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

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

(venv) $ pip install pyperformance

Затем вам следует создать некий файл настроек и каталог выходных данных для своего тестируемого профиля. Рекомендуется чтобы вы создавали этот каталог вне вашего рабочего каталога Git. Это также поможет вам проверять большое число версий.

В своём файле настроек, например, ~/benchmarks/benchmark.cfg добавьте такие строки, cpython-book-samples/62/benchmark.cfg:

[config]
# Path to output json files
json_dir = ~/benchmarks/json

# If True, then compile CPython in Debug mode (LTO and PGO disabled),
# run benchmarks with --debug-single-sample, and disable upload.
#
# Use this option to quickly test a configuration.
debug = False

[scm]
# Directory of CPython source code (Git repository)
repo_dir = ~/cpython

# Update the Git repository (git fetch)?
update = False

# Name of the Git remote, used to create revision of
# the Git branch.
git_remote = remotes/origin

[compile]
# Create files in bench_dir:
bench_dir = ~/benchmarks/tmp

# Link-time optimization (LTO)?
lto = True

# Profile-guided optimization (PGO)?
pgo = True

# The space-separated list of libraries that are package only
pkg_only =

# Install Python? If False, then run Python from the build directory
install = True

[run_benchmark]
# Run "sudo python3 -m pyperf system tune" before running benchmarks?
system_tune = True

# --benchmarks option for 'pyperformance run'
benchmarks =

# --affinity option for 'pyperf system tune' and 'pyperformance run'
affinity =

# Upload generated JSON file?
upload = False

# Configuration to upload results to a Codespeed website
[upload]
url =
environment =
executable =
project =

[compile_all]
# List of CPython Git branches
branches = default 3.6 3.5 2.7

# List of revisions to benchmark by compile_all
[compile_all_revisions]
# List of 'sha1=' (default branch: 'master') or 'sha1=branch'
# used by the "pyperformance compile_all" command

  Исполнение эталонного тестирования

После настройки своего файла конфигурации вы можете запустить это эталонное тестирование при помощи такой команды:

$ pyperformance compile -U ~/benchmarks/benchmark.cfg HEAD

Она скомпилирует CPython в предписанном вами каталоге repo_dir и создаст вывод JSON со сведениями эталонного тестирования в том каталоге, который определён в вашем файле настроек.

  Сопоставление эталонного тестирования

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

Прежде всего, установите зависимости:

 $ pip install seaborn pandas pyperformance

Затем создайте сценарий profile.py, cpython-book-samples/62/profile.py:

import argparse
from pathlib import Path
from perf._bench import BenchmarkSuite

import seaborn as sns
import pandas as pd

sns.set(style="whitegrid")

parser = argparse.ArgumentParser()
parser.add_argument("files", metavar="N", type=str, nargs="+",
                    help="files to compare")
args = parser.parse_args()

benchmark_names = []
records = []
first = True
for f in args.files:
    benchmark_suite = BenchmarkSuite.load(f)
    if first:
        # Initialize the dictionary keys to the benchmark names
        benchmark_names = benchmark_suite.get_benchmark_names()
        first = False
    bench_name = Path(benchmark_suite.filename).name
    for name in benchmark_names:
        try:
            benchmark = benchmark_suite.get_benchmark(name)
            if benchmark is not None:
                records.append({
                    "test": name,
                    "runtime": bench_name.replace(".json", ""),
                    "stdev": benchmark.stdev(),
                    "mean": benchmark.mean(),
                    "median": benchmark.median()
                })
        except KeyError:
            # Bonus benchmark! Ignore.
            pass

df = pd.DataFrame(records)

for test in benchmark_names:
    g = sns.factorplot(
        x="runtime",
        y="mean",
        data=df[df["test"] == test],
        palette="YlGnBu_d",
        size=12,
        aspect=1,
        kind="bar")
    g.despine(left=True)
    g.savefig("png/{}-result.png".format(test))

После этого, для создания графика, исполните этот сценарий из своего интерпретатора с созданными вами файлами JSON:

$ python profile.py ~/benchmarks/json/HEAD.json ...

Это воспроизведёт последовательность графиков в соответствующем подкаталоге png/ для каждого из произведённых эталонных тестов.

Профилирование кода Python при помощи cProfile

Стандартная библиотека поставляется с двумя профилировщиками для кода Python:

profile: профилировщик чистого Python

cProfile: более быстрый профилировщик, написанный на C

В большинстве ситуаций лучшим модулем для применения является cProfile.

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

В Главе 10, Параллельность и одновременность вы написали сканер портов н Python. Попробуйте спрофилировать это приложение в cProfile.

Для запуска модуля cProfile, запустите в своей командной строке Python с аргументом -m cProfile. Вторым аргументом является тот сценарий, который надлежит исполнять:

$ python -m cProfile portscanner_threads.py
Port 80 is open
Completed scan in 19.8901150226593 seconds
         6833 function calls (6787 primitive calls) in 19.971 seconds

   Ordered by: standard name

   ncalls  tottime  percall  cumtime  percall filename:lineno(function)
        2    0.000    0.000    0.000    0.000 ...

Получаемый вывод выдаст на печать таблицу со следующими колонками:

Таблица 15-1. Рассматриваемые отладчики

Столбец
Назначение

ncalls

Число вызовов

tottime

Общее время, затраченное в этой фунции (минус подфункции)

percall

Показатель tottime, поделённый на ncalls

cumtime

Общее время, затраченное в этой функции (включая подфункции)

percall

Показатель cumtime, поделённый на примитивы вызовов

filename:lineno(function)

Данные каждой из функций

Вы можете добавить аргумент -s и название колонки по которой производить сортировку получаемого вывода:

$ python -m cProfile -s tottime portscanner_threads.py

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

  Экспорт профилей

Вы можете снова запустить свой модуль cProfile с аргументом -o для определения некого пути вывода файла:

$ python -m cProfile -o out.pstat portscanner_threads.py

Это создаст файл, out.pstat, который вы сможете загрузить для анализа при помощи класса Stats или некого внешнего инструмента.

Визуализация при помощи SnakeViz

SnakeViz это свободно распространяемый пакет Python для визуализации данных профиля внутри некого веб браузера.

Для установки SnakeViz воспользуйтесь pip:

$ python -m pip install snakeviz

Затем в своей командной строке запустите snakeviz с путём для созданного вами файла статистических данных:

$ python -m snakeviz out.pstat

Это откроет ваш браузер и позволит вам изучать и анализировать полученные данные:

Рисунок 15-2

Визуализация при помощи PyCharm

PyCharm обладает встроенным инструментарием для запуска cProfile и визуализации получаемых результатов. Для его исполнения вам требуется иметь настроенной цель Python.

Для запуска этого профилировщика выберите свою запускаемую цель, затем выберите в своём верхнем меню Run/Profle (target). Это запустит на исполнение цель с cProfile и откроет окно визуализации с табличными данными и граф вызовов:

Рисунок 15-3

Профилирование кода C при помощи DTrace

Сам исходный код CPython обладает некоторыми маркерами для инструмента отслеживания DTrace. DTrace исполняет скомпилированный двоичный файл C/C++, а затем отлавливает и обрабатывает события внутри применяемых им зондов (probes).

Чтобы DTrace предоставлял существенные сведения, само компилируемое приложение обязано обладать маркерами, скомпилированными в это приложение. Это события, возбуждаемые на протяжении времени выполнения. Такие маркеры могут подключать произвольные данные в помощь отслеживанию.

Например, функция вычисления кадра в Python/ceval.c содержит вызов dtrace_function_entry():

    if (PyDTrace_FUNCTION_ENTRY_ENABLED())
        dtrace_function_entry(f);

Это возбуждает в DTrace некий маркер с названием function__entry при каждом входе в некую функцию.

CPython обладает встроенными маркерами для:

Исполняемой строки

Входа в функцию и возврата из неё (исполнения кадра)

Запуска и завершения сборки мусора

Запуска и завершения импорта модуля

События особых точек входа аудита из sys.audit()

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

Названия файла

Имени функции

Номера строки

Все статические аргументы маркера определены в официальной документации.

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

  Относящиеся к делу исходные файлы

Вот относящиеся к DTrace исходные файлы:

Таблица 15-2. Относящиеся к DTrace исходные файлы

Файл
Назначение

Include/pydtrace.h

Определение API для маркеров DTrace

Include/pydtrace.d

Метаданные для поставщика Python, используемого DTrace

Include/pydtrace_probes.h

Автоматически вырабатываемые заголовки для обрабатываемых зондов

  Установка DTrace

DTrace поставляется предварительно установленным в macOS и может быть установлен в Linux одним из инструментов, поставляемых пакетом.

Вот команда для основанных на YUM {DNF} систем:

$ yum install systemtap-sdt-devel

А это команда для базирующихся на APT систем:

$ apt-get install systemtap-sdt-dev

  Компиляция поддержки DTrace

Поддержка DTrace может быть скомпилированной в CPython. Вы можете сделать это при помощи соответствующего сценария ./configuration.

Запустите снова ./configure с теми же самыми параметрами, которые вы применяли в Главе 3, Компиляция CPython и добавьте флаг --with-dtrace. По завершению этого исполните make clean && make для повторной сборки своего исполняемого файла.

Убедитесь что ваш инструмент конфигурирования создал соответствующий заголовок зонда:

$ ls Include/pydtrace_probes.h
Include/pydtrace_probes.h

[Совет]
Важно

Более новые версии macOS обладают защитой уровня ядра с названием SIP (System Integrity Protection), которая взаимодействует с DTrace.

Примеры из этой главы пользуются установленными зондами CPython. Если вы желаете включить зонды libc или syscall для получения дополнительных сведений, тогда вам требуется отключать SIP.

  Применение DTrace из Clion

IDE CLion поставляется оснащённым поддержкой DTrace. Для запуска отслеживания пройдите в Run/Attach Profler to Process и выберите запуск процесса Python.

Окно профилирования выдаст вам приглашение для запуска и останова своего сеанса трассировки. По завершению отслеживания он снабдит вас графом кадров, отображающим стеки выполнения и времена вызовов, дерево вызовов и список методов:

Рисунок 15-4

  Пример DTrace

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

Создайте сценарий профиля profile_compare.d. Для снижения уровня шума от запуска самого интерпетатора, этот профилировщик запустится после входа в portscanner_threads.py:main(), cpython-book-samples/62/profile_compare.d:

#pragma D option quiet
self int indent;

python$target:::function-entry
/basename(copyinstr(arg0)) == "portscanner_threads.py"
 && copyinstr(arg1) == "main"/
{
    self->trace = 1;
    self->last = timestamp;
}

python$target:::function-entry
/self->trace/
{
    this->delta = (timestamp - self->last) / 1000;
    printf("%d\t%*s:", this->delta, 15, probename);
    printf("%*s", self->indent, "");
    printf("%s:%s:%d\n", basename(copyinstr(arg0)), copyinstr(arg1), arg2);
    self->indent++;
    self->last = timestamp;
}

python$target:::function-return
/self->trace/
{
    this->delta = (timestamp - self->last) / 1000;
    self->indent--;
    printf("%d\t%*s:", this->delta, 15, probename);
    printf("%*s", self->indent, "");
    printf("%s:%s:%d\n", basename(copyinstr(arg0)), copyinstr(arg1), arg2);
    self->last = timestamp;
}

python$target:::function-return
/basename(copyinstr(arg0)) == "portscanner_threads.py"
 && copyinstr(arg1) == "main"/
{
    self->trace = 0;
}

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

Вам следует выполнять его с аргументом сценария -s profile_compare и аргументом команды -c './python portscanner_threads.py':

$ sudo dtrace -s profile_compare.d -c './python portscanner_threads.py'
0    function-entry:portscanner_threads.py:main:16
28   function-entry: queue.py:__init__:33
18   function-entry:  queue.py:_init:205
29  function-return:  queue.py:_init:206
46   function-entry:  threading.py:__init__:223
33  function-return:  threading.py:__init__:245
27   function-entry:  threading.py:__init__:223
26  function-return:  threading.py:__init__:245
26   function-entry:  threading.py:__init__:223
25  function-return:  threading.py:__init__:245

[Совет]
Важно

Более ранние версии DTrace могут не обладать параметром -c. В таком случае вам придётся запускать DTrace и саму команду Python в отдельных оболочках.

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

Выводы

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

Глава 16. Последующие шаги

Содержание

Глава 16. Последующие шаги

Написание расширений C для CPython

Улучшение ваших приложений Python

Вклад в проект CPython

Сортировка проблем

Возбуждение пула запроса на исправление проблемы

Прочий вклад

Продолжайте обучаться

Еженедельные советы для разработчиков Real Python

Библиотека видеокурсов Python

В этой главе вы рассмотрите три возможных варианта применения сведений из этой книги:

Написание модулей расширения C или C++

Импорт ваших приложений Python

Вклад в сам проект CPython

Первейшим практическим применением этих знаний выступает написание модулей расширения на C или C++.

Написание расширений C для CPython

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

Если вы желаете писать модуль расширения C, тогда имеется ряд существенных элементов рассмотренных в этой книге знаний, которыми вы можете пользоваться для справки:

Как настраивать компилятор C и компилировать модули C из Главы 3, Компиляция CPython

Как настраивать среду разработки для C из Главы 2, Настройка вашей среды разработки

Как увеличивать и уменьшать на единицу ссылки на сгенерированные объекты из раздела Подсчёт ссылок в Главе 9, Управление памятью

Чем является PyObject* и как с ним взаимодействовать из раздела Объекты и типы переменных объектов в главе Главе 11, Объекты и типы

Что такое слоты типов и как получать доступ к API типа Python из C из раздела Слоты типов в главе Главе 11, Объекты и типы

Как добавлять точки прерывания в исходные файлы C для вашего модуля расширения и отлаживать его из Главы 14, Отладка

[Совет]
Смотри также

Если вы не писали ранее модулей расширений C, обратитесь к книге Real Python Building a C Extension Module. Это руководство содержит конкретный пример построения, компиляции и проверки некого модуля расширения.

Улучшение ваших приложений Python

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

Применение технологий распараллеливания и одновременности для снижения времени выполнения ваших приложений в Главе 10, Параллельность и одновременность

Индивидуальная настройка алгоритма сборки мусора для лучшей обработки памяти в вашем приложении посредством сборки в конце задачи из раздела Сборка мусора в Главе 9, Управление памятью

Применение отладчиков для отладки расширений C и просеивания проблем из из Главы 14, Отладка

Применение профилировщиков для профилирования времени выполнения вашего кода из раздела Профилирование кода Python при помощи cProfile в Главе 15, Эталонное тестирование, профилирование и трассировка

Анализ выполнения кадра на предмет инспекции и отладки сложных проблем из раздела Отслеживание выполнения кадра Главе 8, Цикл расчёта

Вклад в проект CPython

За двенадцать месяцев CPython получил двенадцать новых второстепенных выпусков, сотни изменений и сообщений об ошибках, а также тысячи фиксаций в своём исходном коде.

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

Сообщество CPython жаждет большего числа соучастников. Однако прежде чем представлять некое изменение, улучшение или исправление для CPython, вам требуется узнать с чего начинать. Вот несколько идей:

Просеивание проблем, возбуждаемое разработчиками в bugs.python.org

Исправление небольших, хорошо описанных проблем

Давайте изучим каждую из них чуть подробнее.

  Сортировка проблем

Все отчёты об ошибках и запросы на изменение вначале подставляются в bugs.python.org, также именуемый как BPO. Этот вебсайт отслеживает ошибки для проекта CPhyton. Если вы желаете вставить запрос пула в GitHub, тогда вам вначале нужен номер BPO, который является номером проблемы, созданном BPO (bugs.python.org).

Чтобы начать, зарегистрируйтесь в качестве пользователя пройдя в User/Register в меню слева.

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

Вместо этого, после регистрации, проследуйте в Your Queries/Edit в меню слева. Эта страница снабдит вас списком запросов на индексацию ошибок, которые вы можете помечать в закладки:

Рисунок 16-1

Измените нужное значение на leave in чтобы включить эти запросы в меню Your Queries.

Easy Documentation Issues: Ещё не осуществлённые улучшения в документации

Easy Tasks: Задачи, которые были выявлены подходящими для начинающих

Recently Created: Недавно созданные проблемы

Reports Without Replies: Отчёты об ошибках, которые никогда не получали отклика

Unread: Сообщения об ошибках, которые никогда не читались

50 Latest Issues: Пятьдесят самых последних обновлений проблем

В этих представлениях вы можете следовать руководству Triaging an Issue для самых последних процессов комментирования проблем.

  Возбуждение пула запроса на исправление проблемы

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

Убедитесь что вы обладаете номером BPO.

Создайте ветку в вашей развилке CPython. Обратитесь к Главе 1, Начинаем работать с исходным кодом CPython чтобы ознакомиться с этапами выгрузки исходного кода.

Создайте некий тест для воспроизведения вашей проблемы. Ознакомьтесь с разделом Модули тестирования из Главы 13, Комплект тестов.

Внесите свои изменения, следуя PEP 7 и PEP 8.

Выполните комплект возвратного тестирования чтобы убедиться что все тесты прошли. Такой комплект возвратного тестирования автоматически запустится в GitHub когда вы вставите свой пул запроса, но лучше вначале выполнить проверку локально. Относительно этапов обратитесь к Главе 13, Комплект тестов.

Зафиксируйте свои изменения и доставьте их в GitHub.

Пройдите в github.com/python/cpython и создайте запрос пула для своей ветки.

После подстановки своего запроса, он будет отсеян одной из команд сортировщиков и назначен какому- то разработчику ядра или их команде для обзора.

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

[Совет]
Важно

Важно исправлять только по одной проблеме в пуле запроса. Если вы увидели некую отдельную, несвязанную проблему в каком- то коде при написании своего исправления, сделайте заметку и подставьте её как второй пул запроса.

Хорошее объяснение проблемы, её решение и соответствующее исправление имеют большое значение чтобы помочь вам быстро объединять изменения.

  Прочий вклад

Помимо собственно исправления ошибок, есть ещё несколько различных типов улучшений, которыми вы можете выполнять свой вклад в проект CPython:

Многие функции и модули стандартной библиотеки не имеют тестов своего элемента. Вы можете написать некий тест и представить его в общий проект.

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

Продолжайте обучаться

Часть того, что делает Python столь замечательным, это его сообщество. Знаете кого- нибудь, изучающего Python? Помогите им! Единственный способ осознать, что вы освоили некое понятие, это пояснить его кому- то ещё.

Продолжайте посещать нас в Интернете и продлите своё путешествие по вебсайту realpython.com и следите за нашей учётной записью в Твиттер @realpython.

  Еженедельные советы для разработчиков Real Python

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

Рассылаемые нами новостные бюллетени это не просто перечень популярных статей. Вместо этого мы стремимся делиться хотя бы одной оригинальной мыслью в неделю в виде формата (краткого) эссе.

Если вы желаете узнать о чём идёт речь, перейдите на realpython.com/newsletter и введите свой адрес электронной почты в регистрационную форму. Ждём встречи с вами!

  Библиотека видеокурсов Python

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

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

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

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

Отслеживайте свой прогресс обучения: Помечайте уроки как выполненные или находящиеся в освоении и изучайте их на своём собственном месте. Помечайте закладками заинтересовавшие вас занятия и просматривайте их позднее повторно для повышения эффективности их сохранения в памяти.

Сертификаты об окончании: Для каждого освоенного вами курса вы получаете сертификат о прохождении (и его можно распечатать), который в частном порядке размещается на вебсайте Real Python. Вставляйте свои сертификаты в ваш портфолио, резюме LinkedIn и прочие вебсайты, чтобы показать всему миру что вы преданный приверженец Python.

Регулярно обновляйте знания: Постоянно обновляйте свои навыки и держите руку на пульсе технологии. Мы постоянно выпускаем руководства исключительно для своих участников и обновляем свой контент на постоянной основе.

Знакомьтесь с тем, что доступно на realpython.com/courses