- •Об авторе
- •О группе редакторов
- •Предисловие
- •Введение
- •Как использовать эту книгу
- •Загрузка исходного кода CPython
- •Что в исходном коде?
- •Настройка среды разработки
- •IDE или редактор?
- •Настройка Visual Studio
- •Настройка Visual Studio Code
- •Настройка Vim
- •Выводы
- •Компиляция CPython
- •Компиляция CPython на macOS
- •Компиляция CPython на Linux
- •Установка специализированной версии
- •Знакомство с Make
- •Make-цели CPython
- •Компиляция CPython на Windows
- •Профильная оптимизация
- •Выводы
- •Грамматика и язык Python
- •Спецификация языка Python
- •Генератор парсеров
- •Повторное генерирование грамматики
- •Выводы
- •Конфигурация и ввод
- •Конфигурация состояния
- •Структура данных конфигурации среды выполнения
- •Конфигурация сборки
- •Сборка модуля из входных данных
- •Выводы
- •Генерирование конкретного синтаксического дерева
- •Парсер/токенизатор CPython
- •Абстрактные синтаксические деревья
- •Важные термины
- •Пример: добавление оператора «почти равно»
- •Выводы
- •Компилятор
- •Исходные файлы
- •Важные термины
- •Создание экземпляра компилятора
- •Флаги будущей функциональности и флаги компилятора
- •Таблицы символических имен
- •Основная компиляция
- •Ассемблер
- •Создание объекта кода
- •Использование Instaviz для вывода объекта кода
- •Пример: реализация оператора «почти равно»
- •Выводы
- •Цикл вычисления
- •Исходные файлы
- •Важные термины
- •Построение состояния потока
- •Построение объектов кадров
- •Выполнение кадра
- •Стек значений
- •Пример: добавление элемента в список
- •Выводы
- •Управление памятью
- •Выделение памяти в C
- •Проектирование системы управления памятью Python
- •Аллокаторы памяти CPython
- •Область выделения объектной памяти и PyMem
- •Область выделения сырой памяти
- •Нестандартные области выделения памяти
- •Санитайзеры выделенной памяти
- •Арена памяти PyArena
- •Подсчет ссылок
- •Сборка мусора
- •Выводы
- •Параллелизм и конкурентность
- •Модели параллелизма и конкурентности
- •Структура процесса
- •Многопроцессорный параллелизм
- •Многопоточность
- •Асинхронное программирование
- •Генераторы
- •Сопрограммы
- •Асинхронные генераторы
- •Субинтерпретаторы
- •Выводы
- •Объекты и типы
- •Примеры этой главы
- •Встроенные типы
- •Типы объектов
- •Тип type
- •Типы bool и long
- •Тип строки Юникода
- •Словари
- •Выводы
- •Стандартная библиотека
- •Модули Python
- •Модули Python и C
- •Набор тестов
- •Запуск набора тестов в Windows
- •Запуск набора тестов в Linux или macOS
- •Флаги тестирования
- •Запуск конкретных тестов
- •Модули тестирования
- •Вспомогательные средства тестирования
- •Выводы
- •Отладка
- •Обработчик сбоев
- •Компиляция поддержки отладки
- •LLDB для macOS
- •Отладчик Visual Studio
- •Отладчик CLion
- •Выводы
- •Бенчмаркинг, профилирование и трассировка
- •Использование timeit для микробенчмарка
- •Использование набора тестов производительности Python
- •Профилирование кода Python с использованием cProfile
- •Выводы
- •Что дальше?
- •Создание расширений C для CPython
- •Улучшение приложений Python
- •Участие в проекте CPython
- •Дальнейшее обучение
- •Препроцессор C
- •Базовый синтаксис C
- •Выводы
- •Благодарности
Сопрограммы 249
Если StopAsyncIteration было выдано явно, и это асинхронный генератор, то выдается RuntimeError, так как такая ситуация недопустима.
15. Наконец, результат возвращается вызывающей стороне __next__().
Объединяя все сказанное, можно заключить, что генераторы — мощная синтаксическая конструкция, в которой одно ключевое слово yield запускает цепочку действий для создания уникального объекта, копирования объекта скомпилированного кода в качестве свойства, назначения кадра и сохранения списка переменных в локальной области видимости.
СОПРОГРАММЫ
У сопрограмм есть одно серьезное ограничение: они могут возвращать значения вызовом 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):
# Нижний регистр a--z print(letter)
Книги для программистов: https://t.me/booksforits
250 Параллелизм и конкурентность
for letter in letters(True):
# Верхний регистр A--Z print(letter)
Генераторы также отлично подходят для ленивых последовательностей (lazy sequences1), в которых они могут вызываться многократно.
Концепция сопрограммы (coroutine), развивающая поведение генераторов (например, возможность приостановки и возобновления выполнения), была применена в Python во многих API.
Генераторы представляют собой ограниченную форму сопрограммы, потому что генераторам можно отправлять данные методом .send(). Возможна двусторонняя отправка сообщений между стороной вызова и целью. Сопрограммы также хранят сторону вызова в атрибуте cr_origin.
Изначально сопрограммы включались декоратором, но этот вариант считается устаревшим и был заменен «полноценными» сопрограммами с использованием ключевых слов async и await.
Чтобы пометить, что функция возвращает сопрограмму, необходимо поставить перед ней ключевое слово 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 секунд будет выдан сигнал:
1Вычисление элемента последовательности откладывается, пока не потребуется результат. — Примеч. ред.
Книги для программистов: https://t.me/booksforits
Сопрограммы 251
>>> asyncio.run(alarm) 'wake up'
Среди преимуществ сопрограмм можно выделить возможность их параллельного выполнения. Так как объект сопрограммы представляет собой переменную, которую можно передать функции, эти объекты можно объединить в цепочку или создать из них последовательность.
Например, если вы хотите установить десять сигналов с разными интервалами и таймеры должны запускаться одновременно, эти объекты сопрограмм можно преобразовать в задачи.
API задач используется для планирования и выполнения нескольких сопрограмм одновременно. Перед планированием задач должен быть запущен цикл событий. Цель цикла событий — планирование одновременно выполняемых задач и связывание событий (завершение, отмена, исключения и т. д.) с обратными вызовами.
Когда мы вызывали asyncio.run() (из Lib asyncio runners.py), функция выполняла за нас следующие задачи:
1.Запуск нового цикла событий.
2.Упаковка объекта сопрограммы в задачу.
3.Назначение обратного вызова для завершения задачи.
4.Цикл по задаче до ее завершения.
5.Возвращение результата.
Исходные файлы
Ниже указан исходный файл, относящийся к сопрограммам.
ФАЙЛ НАЗНАЧЕНИЕ
Lib asyncio |
Реализация asyncio из стандартной библиотеки Python |
Циклы событий
Циклы событий связывают асинхронный код воедино. Они пишутся на чистом Python и представляют собой объекты, содержащие задачи.
Книги для программистов: https://t.me/booksforits
252 Параллелизм и конкурентность
Все задачи в цикле могут иметь обратные вызовы. Цикл выполняет обратные вызовы при завершении задачи или сбое:
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() вместо блокирующего (и не async-совместимого) вызова time.sleep().
Книги для программистов: https://t.me/booksforits
Сопрограммы 253
Пример
Многопоточный сканер портов можно преобразовать для asyncio по следующей схеме:
zz Измените check_port() для подключения через сокет, а не через функцию asyncio.open_connection(), создающую future-объект вместо немедленного подключения.
zz Используйте future-подключение через сокет в событии таймера с asyncio.wait_for().
zz Добавьте порт в список результатов (results), если проверка была успешной.
zz Добавьте новую функцию scan() для создания сопрограмм check_port() для каждого порта. Добавьте их в список задач tasks.
zz Объедините все задачи в новую сопрограмму вызовом asyncio.gather(). zz Проведите сканирование вызовом 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
except asyncio.TimeoutError:
pass # Порт закрыт, пропустить и продолжить
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__':
Книги для программистов: https://t.me/booksforits