Комментарии 28
Но, как оказывается, генераторы нашли достаточно специфическое применение для создания списков данных.
Это не просто "специфическое применение", это их причина появления в языке.
Например, как его использовать повторно, если корутины по определению работают только «в одну сторону»? Как вычислить произвольный его элемент, если индексирование процесса невозможно? Поиск в интернете убеждает, что аналогичные вопросы возникли не только у меня, но решения, которые при этом предлагаются, не так уж тривиальны и очевидны.
Решение тут тривиально — материализовать список функцией list. Генераторы созданы для удобного построения однопроходных алгоритмов. Не знаю, как вам, но мне вот очевидно, что многопроходный алгоритм невозможно выполнить в один проход.
Может вызывать сомнение и надежность подобного синтаксиса определения списков, т.к. достаточно легко по ошибке вместо круглых скобок применить квадратные и наоборот.
Забавно видеть это как критику генераторов в сравнении с конечными автоматами, ведь конечные автоматы ещё больше подвержены опечаткам.
Обращает на себя внимание выбор моментов переключения сопрограмм. Их расстановка следует простому правилу — ставим оператор yield перед блокирующими функциями.
Нет, это ошибочное понимание. Оператор yield перед блокирующей функцией ничего не даст. Чтобы избежать блокировки, нужно сначала превратить функцию в неблокирующую-асинхронную.
Но… вернемся к сопрограммам. Оказывается, что ныне они проходят под именем корутин. Что же нового произошло со сменой названия?
А что в принципе может произойти со сменой названия? Вроде бы всем видно, что ничего, почему для вас это стало открытием, которому посвящено несколько абзацев?
Для справки: "корутина" — это калька с английского "coroutine", что на русский язык всегда переводилось и до сих пор переводится как "сопрограмма".
Нет, это ошибочное понимание. Оператор yield перед блокирующей функцией ничего не даст. Чтобы избежать блокировки, нужно сначала превратить функцию в неблокирующую-асинхронную.
Не скажите. По факту это так (см. код). Как «превратить», если код Вам не доступен? Что делают в примере. Приостанавливают перед возможной блокировкой и уходят в анализ. Возвращаются обратно, когда поняли, что блокирующая функция не будет тратить время на ожидание. Роль здесь jield — возможность «сходить в гости», а не ждать, когда кто-то и когда-то разрешит что-то делать дальше.
Не скажите. По факту это так (см. код).
По факту это не так (см. код)
Как «превратить», если код Вам не доступен?
Никак, для асинхронной программы нужны асинхронные библиотеки. В крайнем случае можно переписать функцию самостоятельно.
Что делают в примере. Приостанавливают перед возможной блокировкой и уходят в анализ. Возвращаются обратно, когда поняли, что блокирующая функция не будет тратить время на ожидание.
Это возможно только для примитивных операций ввода-вывода, и то не для всех. Но нормальные программисты так не пишут; эти два шага должны быть объединены в один.
Как «превратить», если код Вам не доступен?
Да достаточно просто, на самом деле. Если речь про Python, то блокирующие IO-bound вызовы легко и непринужденно превращаются в неблокирующие асинхронные путем запуска их в отдельных потоках. Если вызов не IO-bound, а CPU-bound, то из-за GIL запускать желательно в отдельном процессе. Тут уже могут быть некоторые проблемы, т.к. не все и не всегда легко переживет сериализацию и перенос в другой процесс. Но в большинстве случаев проблема тоже вполне решаемая.
В asyncio
это все есть из коробки: run_in_executor
, ThreadPoolExecutor
, ProcessPoolExecutor
и вот это вот все.
Два момента я нашёл особенно показательными.
Здесь приведены два варианта чтения символов с клавиатуры. Первый вариант — блокирующий. Он заблокирует вычисления и не запустит оператор вывод символа, до тех пор, пока функция getch() не получит его от клавиатуры. Во втором варианте эта же функция будет запущена только в нужный момент, когда парная ей функция kbhit() подтвердит, что символ находится в буфере ввода. Тем самым блокировки вычислений не будет.
Фокус в том, что блокировка таки будет! Только это будет не системная блокировка, а активное ожидание в цикле while (C != 'e')
, что ещё хуже, ведь процессорное время не достаётся другим программам.
Настоящее отсутствие блокировки — это когда чтение с клавиатуры не мешает выполнению a.exec()
.
- Классическая автоматная реализация — 110.66 сек
- Автоматная реализация без автоматных методов — 73.38 сек
- Без автомата-секундомера — 35.14
- Счетчик в форме while с выходом на каждой итерации — 30.53
- Счетчик с блокирующим циклом — 18.27
- Исходный счетчик с декоратором — 6.96
Отличные цифры, которые говорят сами за себя! Варианты 1 и 6 отличаются почти в 16 раз, как вообще после таких результатов можно задумываться о написании "автоматной" реализации?
Ключевое отличие от win 3.1 — в том, что там кооперативная многозадачность была в рамках всей системы, а тут — лишь в рамках одного приложения, и то не целиком.
Прежде всего это уменьшение возможных проблем.
Хотя, с точки зрения приложения — если накосячено внутри, то всё равно будет больно, тут уже неважно, на каком уровне диспетчер распределяет.
А, если выполнение заблокировано снаружи — какая разница, по какой причине.
То есть для разработки особой разницы нет. Всё равно самописные диспетчеры/планировщики быстро вымрут, а прилинкован чужой к приложению или расположен в ядре ОС — не особо что изменит в отладке.
Лучше бы вы примеры в статье делали на C++, в котором, с ваших слов, вы разбираетесь лучше. Ваш код на Python откровенно плох.
"Однострочники", использование имён вида x1, y1, y2 и др. совершенно сбивает с толку и делает код ваших примеров непонятным.
И что это за использование внутри PTimer глобально созданного, мутабельного экземпляра PCount? У нас в команде за подобные вещи отрезают палец на ноге (шутка).
И что такое 4 в выражении self.nState = 4
? Это какая-то очень известная константа про которую можно ничего не объяснять?
Когда Вы поймете принцип построения «автоматного кода», то это уже не будет столь «отталкивать». Автоматные методы достаточно коротки и нет особого смысла их растягивать. По мне лучше, когда код компактнее. Так легче его охватить одним взглядом.
Так что использование «типовых имен» типа x1, y1 ..., наоборот, определяет эти методы, как методы автоматного класса. «Иксы» — предикаты, «игрики» — действия.
По поводу PCount хочется, чтобы Вы (или Ваша команда) мне бы помогли. В С++ мне понятно, а здесь я не знаю, как передать ссылку на класс в параметрах конструктора класса. Поэтому сделал так, как получилось. А задумка такая: классу PTimer передавать ссылки на класс, который он должен контролировать. А здесь, чтобы заработало хоть как-то, пришлось использовать глобальное объявления. Вы заметили и это прекрасно. Теперь — помогайте. Пальцы жалко, однако… :)
По умолчанию nState — переменная состояния любого автоматного класса, принимающая целые значения. Это, безусловно, опять к слову понимания технологии формирования кода. Любая технология тем и хороша, что может уменьшить «словословие».
Зачем передавать ссылку на класс через аргументы? И как такое вообще можно сделать в C++, где класс не является объектом как в питоне?
Передавать надо экземпляр класса и сохранять его в "конструкторе" в поле экземпляра класса. Вот так:
class PTime:
def __init__(self, p_count):
self.p_count = p_count
...
p_count = PCount(1000)
p_time = PTime(p_count)
По поводу наименования методов и переменных я категорически с вами не согласен. Мало того, что автоматный код (судя по вашим примерам) размазывает логику какого-то действия по разным методам, и собрать всё вместе не так просто. Так ещё эти x1, y1 точно не помогают понять, что именно делают эти методы, какая у них задача.
И хочется перефразировать известную фразу: "Вы пишете на питоне, но делаете это без уважения". Было бы полезно почитать рекомендации по оформлению кода и неймингу в языке программирования, прежде чем публично показывать ваш код на нём.
Вот подправленный код:
import time
# 1) 110.66 sec
class PCount:
def __init__(self, cnt ): self.n = cnt; self.nState = 0
def x1(self): return self.n > 0
def y1(self): self.n -=1
def loop(self):
if (self.nState == 0 and self.x1()):
self.y1();
elif (self.nState == 0 and not self.x1()): self.nState = 4;
class PTimer:
def __init__(self, p_count):
self.st_time = time.time(); self.nState = 0; self.p_count = p_count
# def x1(self): return self.p_count.nStat == 4 or self.p_count.nState == 4
def x1(self): return self.p_count.nState == 4
def y1(self):
t = time.time() - self.st_time
print ("speed CPU------%s---" % t)
def loop(self):
if (self.nState == 0 and self.x1()): self.y1(); self.nState = 1
elif (self.nState == 1): pass
cnt1 = PCount(1000000)
cnt2 = PCount(10000)
tmr1 = PTimer(cnt1)
tmr2 = PTimer(cnt2)
# event loop
while True:
cnt1.loop(); tmr1.loop()
cnt2.loop(); tmr2.loop()
Думаю, теперь понятно зачем так нужно. В принципе даже понятно как работает (это «утка» :)): интерпретатор все выясняет «по ходу». Вариант предиката x1 (см. коммент в коде) в этом убеждает.
Питон я, конечно, уважаю. Рекомендации, если они разумные, — тоже и даже иногда соблюдаю. Но в этом случае язык это разрешает и я пишу, как мне удобно и как диктует используемая мною технология проектирования. А в ней, как ни странно, но и «логика» и «наименование» помогают понять. Об этом (технологии и ее особенностях) многое сказано и объяснено в моих предыдущих статьях.
Но, еще раз, большущий респект за подсказку. В статье, теперь можно признаться, я сознательно «накосячил» на эту тему. Вы заметили и помогли. А это о многом говорит ;) Еще раз спасибо.
Все перечисленные вами ошибки — языконезависимые. Он и на С++ так же пишет.
я, скажем так, критически настроен к шаблонам. Ну, очень уж не глянулся мне когда-то их своеобразный «птичий язык», который, как показалось, сильно затрудняет восприятие кода и понимание алгоритма. [...]
Иное дело Python. Шаблонов в нем пока не заметил и это успокаивает.
Надеюсь, не испорчу вам удовольствие от Python, но там как бы вообще всё — «шаблоны», ибо duck typing.
Кстати, об утках… Когда-то, собирая грибы, я под кустом наткнулся на утку, которую, видимо, недавно подстрели, но не смогли найти. Брать я ее не стал, но решил взять крыло целиком, чтобы показать детям, какая она (утка) красивая. Крыло сунул в пакет и пошел дальше. Но когда я через какое-то время заглянул в пакет, то ужаснулся множеству мелких насекомых, которые из него полезли. Пришлось выбросить, кроме нескольких красивых перышек. Теперь, когда я вижу красивых плавающих и прихорашивающихся уточек, я всегда вспоминаю о том, что у них там может быть под перьями…
Надеюсь, что я тоже не испортил Ваше представление о «милых уточках». Но как-то так — вспомнилось :)
Какое отношение крыло подстреленной утки имеет к утиной типизации?
Я поражаюсь вашему (не)умению опознавать в тексте термины и искать их определения...
Мир без корутин. Итераторы-генераторы