Как стать автором
Обновить

Комментарии 28

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

Это не просто "специфическое применение", это их причина появления в языке.


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

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


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

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


Обращает на себя внимание выбор моментов переключения сопрограмм. Их расстановка следует простому правилу — ставим оператор yield перед блокирующими функциями.

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


Но… вернемся к сопрограммам. Оказывается, что ныне они проходят под именем корутин. Что же нового произошло со сменой названия?

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


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

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

Не скажите. По факту это так (см. код). Как «превратить», если код Вам не доступен? Что делают в примере. Приостанавливают перед возможной блокировкой и уходят в анализ. Возвращаются обратно, когда поняли, что блокирующая функция не будет тратить время на ожидание. Роль здесь jield — возможность «сходить в гости», а не ждать, когда кто-то и когда-то разрешит что-то делать дальше.
Не скажите. По факту это так (см. код).

По факту это не так (см. код)


Как «превратить», если код Вам не доступен?

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


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

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

Как «превратить», если код Вам не доступен?

Да достаточно просто, на самом деле. Если речь про Python, то блокирующие IO-bound вызовы легко и непринужденно превращаются в неблокирующие асинхронные путем запуска их в отдельных потоках. Если вызов не IO-bound, а CPU-bound, то из-за GIL запускать желательно в отдельном процессе. Тут уже могут быть некоторые проблемы, т.к. не все и не всегда легко переживет сериализацию и перенос в другой процесс. Но в большинстве случаев проблема тоже вполне решаемая.


В asyncio это все есть из коробки: run_in_executor, ThreadPoolExecutor, ProcessPoolExecutor и вот это вот все.

О варианте переноса блокирующей функции в другой поток написано и в статье, а предыдущей рассмотрен и пример реализации этой процедуры. Правда, на С++ и в Qt.

Два момента я нашёл особенно показательными.


Здесь приведены два варианта чтения символов с клавиатуры. Первый вариант — блокирующий. Он заблокирует вычисления и не запустит оператор вывод символа, до тех пор, пока функция getch() не получит его от клавиатуры. Во втором варианте эта же функция будет запущена только в нужный момент, когда парная ей функция kbhit() подтвердит, что символ находится в буфере ввода. Тем самым блокировки вычислений не будет.

Фокус в том, что блокировка таки будет! Только это будет не системная блокировка, а активное ожидание в цикле while (C != 'e'), что ещё хуже, ведь процессорное время не достаётся другим программам.


Настоящее отсутствие блокировки — это когда чтение с клавиатуры не мешает выполнению a.exec().




  1. Классическая автоматная реализация — 110.66 сек
  2. Автоматная реализация без автоматных методов — 73.38 сек
  3. Без автомата-секундомера — 35.14
  4. Счетчик в форме while с выходом на каждой итерации — 30.53
  5. Счетчик с блокирующим циклом — 18.27
  6. Исходный счетчик с декоратором — 6.96

Отличные цифры, которые говорят сами за себя! Варианты 1 и 6 отличаются почти в 16 раз, как вообще после таких результатов можно задумываться о написании "автоматной" реализации?

Настоящее отсутствие блокировки — это когда чтение с клавиатуры не мешает выполнению a.exec().
Настоящее — это когда не мешает работа с дискетой :-D
Правильное замечание… Или когда мышку прибивают гвоздями к экрану монитора :)
Просто, извините, вспомнился анекдот времён винды 3.1:
— Папа, а покажи, что такое многозадачность?
— Пожалуйста, только подожди, когда дискета доформатируется.
win 3.1 вспоминается. За всеми этими попытками сформировать новые термины — кооперативная многозадачность.

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

вообще-то это означает уменьшение возможных ништяков.

Прежде всего это уменьшение возможных проблем.

не без того :-)
Хотя, с точки зрения приложения — если накосячено внутри, то всё равно будет больно, тут уже неважно, на каком уровне диспетчер распределяет.
А, если выполнение заблокировано снаружи — какая разница, по какой причине.
То есть для разработки особой разницы нет. Всё равно самописные диспетчеры/планировщики быстро вымрут, а прилинкован чужой к приложению или расположен в ядре ОС — не особо что изменит в отладке.
Кооперативная многозадачность была и остается и в рамках приложения. Вы ее или сами создаете (например, по типу моего кода) и/или пользуетесь встроенной — событиями, сигналами и т.п. Например, для реализации кооперативности в ВКП(а) в Windows я использую цикл Idle, а в Linux из-за его отсутствия — события от таймера.

Это в win 3.1-то? Ну-ну...

Лучше бы вы примеры в статье делали на C++, в котором, с ваших слов, вы разбираетесь лучше. Ваш код на Python откровенно плох.
"Однострочники", использование имён вида x1, y1, y2 и др. совершенно сбивает с толку и делает код ваших примеров непонятным.
И что это за использование внутри PTimer глобально созданного, мутабельного экземпляра PCount? У нас в команде за подобные вещи отрезают палец на ноге (шутка).
И что такое 4 в выражении self.nState = 4? Это какая-то очень известная константа про которую можно ничего не объяснять?

Код, упоминаемого Вами, счетчика есть и на С++, но только в предыдущей статье. Так что можно сравнить и разобраться где же хуже ;) Ну а то, что код на Python плох так это еще и во многом, думаю, уже дело его восприятия. Главное, — работает ;)
Когда Вы поймете принцип построения «автоматного кода», то это уже не будет столь «отталкивать». Автоматные методы достаточно коротки и нет особого смысла их растягивать. По мне лучше, когда код компактнее. Так легче его охватить одним взглядом.
Так что использование «типовых имен» типа 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.
Как может испортить то, о чем не ведаешь. Если я до этого использовал язык, в котором «все шаблоны» и это мне ну ни как не мешало, то о таких шаблонах можно только мечтать. Проблемы в поездке начинаются, когда возникает необходимость лезть под капот.

Кстати, об утках… Когда-то, собирая грибы, я под кустом наткнулся на утку, которую, видимо, недавно подстрели, но не смогли найти. Брать я ее не стал, но решил взять крыло целиком, чтобы показать детям, какая она (утка) красивая. Крыло сунул в пакет и пошел дальше. Но когда я через какое-то время заглянул в пакет, то ужаснулся множеству мелких насекомых, которые из него полезли. Пришлось выбросить, кроме нескольких красивых перышек. Теперь, когда я вижу красивых плавающих и прихорашивающихся уточек, я всегда вспоминаю о том, что у них там может быть под перьями…

Надеюсь, что я тоже не испортил Ваше представление о «милых уточках». Но как-то так — вспомнилось :)

Какое отношение крыло подстреленной утки имеет к утиной типизации?


Я поражаюсь вашему (не)умению опознавать в тексте термины и искать их определения...

Спасибо за комплимент ;) Может я выразился слишком «образно», но речь просто о том, что почти всегда есть некий «скрытый смысл», подкапотный код, «букашки-насекомые» и т.п. Многие, да и я все чаще, хочу понимать так, «как вижу»: утка — так утка, селезнь — так селезень, шаблон — так шаблон. «Вообще все» Питона — это больше о том, что у него «внутре». А там, как известно по Стругацким — «лпч» ;) Эту «лпч» и эти шаблоны, о которых Вы пишете, я не вижу и до определенной поры мне и не хочется их видеть. Мне просто хочется пользоваться и воспринимать все «как есть». Ведь под уткой, которая может и плавать, крякать и даже летать, может скрываться очень крутая игрушка. А мне нравится то, что я вижу, и я уже не ребенок, чтобы ее ковырять и интересоваться, что там у нее внутри. Этот этап я уже давно прошел и чем-то удивить меня сложно ;) Пока я вижу, что это «живая утка», которой я и любуюсь. Потом, может быть, я сам догадаюсь или мне скажут, что это «игрушка». А пока меня все, или почти все, устраивает. Как в том же Питоне ;)
Зарегистрируйтесь на Хабре , чтобы оставить комментарий

Публикации

Истории