Pull to refresh

Comments 19

Спасибо за статью, про функциональные возможности питона часто незаслуженно забывают.

Однако на мой взгляд многое из описанного во второй части статьи, про пакет functional и библиотеку returns, на практике довольно бесполезно.
Код, который мимикрирует под функциональный, вызывая рантайм-исключения при нарушении каких-то условий, только мешает — на его месте мог бы быть более привычной питонячий код, который можно проверить линитерами и mypy, и выявить похожие ошибки «статически», без запуска кода.

Примерно тоже самое применимо и к returns. В частности хаскель, из которого похоже Maybe и притащили, встречая Maybe в коде заставляет тебя проверять везде что именно вернулось — Just T или Nothing, что и даёт серьезные гарантии на уровне типов. В returns не вникал, но судя по примеру из статьи, этот инструмент не даёт никаких «статических» проверок, и вообще будто ломает тайп чекер (mypy), подменяя возвращаемые значения на собственные типы — а если и не ломает, то однозначно усложняет чтение кода.

Я сам функциональную парадигму тоже люблю и уважаю, но объектно-ориентированный, динамически типизированный, интепретируемый Python фундаментально очень от нее далек, несмотря на то что имеет и функции как объекты первого класса, и замыкания, и много чего ещё.
Поэтому на мой взгляд выгоднее внедрять мощь функциональщины в свои Python-программы порционально, там где она решает конкретные проблемы, не выходя далеко за рамки «идиоматического» питона)
Уточню, что я говорю исключительно про «фреймворки» для функциональной разработки в питоне — с основными выводами статьи я совершенно согласен и большинством из описанного сам активно пользуюсь
# Normal statement-based flow control
if <cond1>: 
    func1() 
elif <cond2>: 
    func2() 
else: 
    func3() 

    # Equivalent "short circuit" expression
(<cond1> and func1()) or (<cond2> and func2()) or (func3()) 

"Equivalent "short circuit" expression" НЕ является эквивалентом "Normal statement-based flow control". Пример:


>>> func1 = lambda: print('func1')
>>> func2 = lambda: print('func2')
>>> func3 = lambda: print('func3')
>>> x = 1
>>> (x == 1 and func1()) or (x == 2 and func2()) or (func3())
func1
func3
>>> if x == 1: func1()
... elif x == 2: func2()
... else: func3()
...
func1

Разница в том, что с if'ом func3 не вызывается.

Собственно, следующий пример имеет аналогичную проблему:


# statement-based while loop
while <cond>: 
    <pre-suite> 
    if <break_condition>: 
        break
    else: 
        <suite> 

# FP-style recursive while loop
def while_block(): 
    <pre-suite> 
    if <break_condition>: 
        return 1 
    else: 
        <suite> 
        return 0 

while_FP = lambda: (<cond> and while_block()) or while_FP() 
while_FP()

Если запустить, то получим следующий вывод:


IMP -- quit
FP -- quit
quit

FP версия делает 1 лишнее действие (по сравнению с IMP).

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

Вопрос касательно кода из примера "Маршрутизация данных на генераторах (мультиплексирование, броадкастинг)":


  1. будет ли работать этот код с разными очередями?
    Я так понимаю что нет.
  2. для чего нужен gen_cat(consumers) в multiplex()?
    Я так понимаю что не нужен, нас интересует только genfrom_queue(in_q).
    Т.к. (при текущей реализации) gen_cat() никогда не дойдет до 2+ элемента sources,
    т.к. sendto_queue() никогда не дойдет до StopIteration,
    т.к. в follow() бесконечный цикл.
  1. В данном конкретном примере — только одна очередь и с несколькими работать не будет, верно. Ничего не мешает пример развить и работать с несколькими очередями, но это уже вне данной статьи.
  2. gen_cat(consumers) в примере входит внутрь каждого контекста genfrom_queue и забирает оттуда данные, распаковывая в единый список (похожее поведение с itertools.chain). gen_cat действительно работает в бесконечном цикле, однако мы предусмотрительно заставляем follow генераторы работать внутри треда, поэтому во время time.sleep будут отрабатывать (или также ожидать) другие треды.
    Так что никаких подводных камней тут нет. Треды решают второй вопрос, поставленный вами, т.к. используется одна очередь для всех тредов (всех follow файлов)

Условно говоря, с разных тредов всё валится в одну кучу (очередь) в фоновом режиме и gen_cat всё это добро разгребает в единый результирующий список

gen_cat(consumers) в примере входит внутрь каждого контекста genfrom_queue

Как он может зайти внутрь каждого контекста, если genfrom_queue() никогда не выйдет из вечного цикла, т.к. StopIteration никогда не появится в очереди (см. блок "т.к." из моего предыдущего сообщения)?


P.S. Попробую с другой стороны:
Если приглядеться, то можно увидеть, что (в данном конкретном примере) consumers содержит результаты 3х вызовов genfrom_queue(in_q).
Чем эти 3 вызова genfrom_queue(in_q) отличаются друг от друга?

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


import queue
import threading
import time
import os

Попробую поочерёдно разобрать, как это работает, в порядке, в котором оно запускается.


  1. follow. Идём в конец файла, пытаемся считать новую строку, если она есть. В противном случае спим
  2. multiplex. Создаём очередь, считываем в треде с помощью функции sendto_queue источник (функция follow) и в конце добавляем StopIteration, если мы завершили итерирование (в данном конкретном случае этого не произойдёт, т.к. вечный цикл)
  3. С помощью genfrom_queue считываем с очереди всё, что туда пришло. При StopIteration выходим из цикла
  4. С помощью gen_cat входим в контекст считывания очереди по мере её наполнения
  5. С помощью функции broadcast передаём один и тот же результат в разные экземпляры Consumer

Итак, где же тут магия, которая позволяет считывание из каждого из источников follow конкурентно? Всё дело в sleep у follow, что позволяет работать конкуррентно бесконечному циклу в треде и genfrom_queue, где у нас происходит из очереди, а не непосредственно из функций follow.


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


Чтобы подтвердить теорию, добавим src как один из аргументов в genfrom_queue


def genfrom_queue(thequeue, src):
    print(src.__name__)
    while True:
        item = thequeue.get()
        if item is StopIteration:
            break
        yield item
...
def multiplex(sources):
   ...
   consumers.append(genfrom_queue(in_q, src))
...
log1.__name__ = 'log1'
log2.__name__ = 'log2'
log3.__name__ = 'log3'

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


def multiplex(sources):
    in_q = queue.Queue()
    for src in sources:
        thr = threading.Thread(target=sendto_queue, args=(src, in_q))
        thr.start()
    yield from genfrom_queue(in_q)

Работает аналогично. В источнике Бизли, видимо, по какой то причине включили эти ненужные шаги

  1. Я не ставил под сомнение работоспособность кода. Под сомнением находится только объяснение что делает этот код.
  2. Хорошо что у меня получилось обратить внимание на непонятный (в данном примере) consumers
  3. Ваши ответы порождают только больше вопросов:
    1. Всё дело в sleep у follow, что позволяет работать конкуррентно бесконечному циклу в треде
      т.е. без sleep() ничего работать не будет?
    2. Почему вы в своей версии multiplex() сделали именно так:
      yield from genfrom_queue(in_q)
      а не
      return genfrom_queue(in_q)

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

Чтобы это работало корректно, необходимо, чтобы short circuit возвращал не Falsy значения


>>> def return_non_falsy_values(value):
...        print(value)
...        return True
...
>>> func1 = lambda: return_non_falsy_values('func1')
>>> func2 = lambda: return_non_falsy_values('func2')
>>> func3 = lambda: return_non_falsy_values('func3')
>>> x = 1
>>> (x == 1 and func1()) or (x == 2 and func2()) or (func3())
func1
True

Именно так.
Я считаю что это ограничение должно быть явно указано в статье.

Спасибо за уточнения, добавил описание ограничений под примерами в примечаниях

Можно еще прикольнее, чтобы быть на стиле
return_non_falsy_values = lambda x: print(x) or True
Огромный процент программных ошибок и главная проблема, требующая применения отладчиков, случается из-за того, что переменные получают неверные значения в процессе выполнения программы.


Есть какие-то исследования, которые это подтверждают?

На самом деле нет, однако на этот счёт есть занимательная ссылка, в которой раскрываются ошибки достаточно косвенно связанные, но всё таки связанные с тем, что переменные могут быть мутабельны (перезаписываться в ту же переменную). И, как можно увидеть, много проблем может быть косвенно связано именно с мутабельностью переменных, а это достаточно широкий пласт ошибок.
https://www.quora.com/Why-is-immutability-important-in-functional-programming


Большего, к сожалению, дать не смогу

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

Sign up to leave a comment.

Articles