LINUX.ORG.RU

Чем плохо много return?

 ,


0

3

В линтерах во многих языках программирования есть такая проверка: https://docs.astral.sh/ruff/rules/too-many-return-statements/

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

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

def f(x):
  if cond1(x):
    return res1
  y = process1(x);
  if cond2(y):
    return res2
  if cond3(y):
    return res3
  z = process2(x, y)
  if cond4(x,y,z):
    return res4
  if cond5(x,y,z):
    return res5
  ...  

после убирания return получается что-то вроде

def f(x):
  done = False 
  if cond1(x):
    res = res1
    done = True
  if not done:
    y = process1(x);
  if not done and cond2(y):
    res = res2
    done = True
  if not done and cond3(y):
    res = res3
    done = True
  if not done:
    z = process2(x, y)
  if not done and cond4(x,y,z):
    res = res4
    done = True
  if not done and cond5(x,y,z):
    res = res5
    done = True
  ...
  return res  

Второй вариант действительно легче читается или я неправильно преобразовал первый во второй?

UPD: С учётом наличия elif

def f(x):
  done = False 
  if cond1(x):
    res = res1
    done = True
  if not done:
    y = process1(x);
    if cond2(y):
      res = res2
      done = True
    elif cond3(y):
      res = res3
      done = True
  if not done:
    z = process2(x, y)
    if cond4(x,y,z):
      res = res4
      done = True
    elif cond5(x,y,z):
      res = res5
      done = True
  ...
  return res  

Вот это читается легче, чем первый вариант?

★★★★★

Последнее исправление: monk (всего исправлений: 1)
Ответ на: комментарий от Kogrom

А предложенная альтернатива нарушает сразу 2 принципа - KISS и YAGNI.

самые вредные принципы )

KISS: Используется для отказа от необходимых абстракций («и так сойдет»).

YAGNI: Блокирует проектирование, приводит к архитектурному долгу, когда добавление функциональности требует переписывания.

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

oxo
()
Последнее исправление: oxo (всего исправлений: 2)
Ответ на: комментарий от oxo

Вот это ты серьёзно?

В смысле, если в чужой функции обнаружишь

def f(x):
    handler1 = HandlerCond1()
    handler2 = HandlerProcess1()
    handler3 = HandlerCond2()
    handler4 = HandlerCond3()
    handler5 = HandlerProcess2()
    handler6 = HandlerCond4()
    handler7 = HandlerCond5()

    handler1.set_next(handler2).set_next(handler3).set_next(handler4).set_next(handler5).set_next(handler6).set_next(handler7)
    return handler1.handle(Request(x))

то тебе будет гораздо понятнее её алгоритм, чем если наткнёшься на текст функции из темы?

monk ★★★★★
() автор топика
Ответ на: комментарий от monk

Разумеется (с учётом что будет нормальное именование), а не как в примере.

И я получаю бонусом огромное количество ништяков:

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

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

oxo
()
Последнее исправление: oxo (всего исправлений: 1)
Ответ на: комментарий от oxo

За сколько времени поймёшь, что делает функция f?

from abc import ABC, abstractmethod
from typing import Any, Optional

def f(x):
  handler1 = HandlerLess0()
  handler2 = HandlerMore0Cont()
  handler3 = HandlerEq0()
  handler1.set_next(handler2).set_next(handler3)
  return handler1.handle(Request(x))

class Request:
    """Объект, содержащий данные для обработки и промежуточные результаты."""
    def __init__(self, x):
        self.x = x - 2
        self.y = 1
        self.z = 1

class Handler:
    def __init__(self, next_handler: Optional['Handler'] = None):
        self._next_handler = next_handler

    """Абстрактный класс Обработчика."""
    def set_next(self, handler: 'Handler') -> 'Handler':
        """Устанавливает следующий обработчик в цепочке."""
        self._next_handler = handler
        return handler

    @abstractmethod
    def handle(self, request: Request) -> Any:
        """Обрабатывает запрос или передает его дальше."""
        if self._next_handler:
            return self._next_handler.handle(request)
        return None # Если достигнут конец цепочки без результата

# --- Обработчик 1: условие меньше 0 ---
class HandlerLess0(Handler):
    def handle(self, request: Request) -> Any:
        if request.x < 0:
            return 1
        return super().handle(request)

# --- Обработчик 2: условие больше 0 ---
class HandlerMore0Cont(Handler):
    def handle(self, request: Request) -> Any:
        if request.x > 0:
            request.x = request.x - 1
            z = request.z
            request.z = request.y + request.z
            request.y = z
            self.handle(request)
        return super().handle(request)

# --- Обработчик 3: условие равно 0 ---
class HandlerEq0(Handler):
    def handle(self, request: Request) -> Any:
        return request.z - 1
monk ★★★★★
() автор топика
Ответ на: комментарий от monk

Кажется у нас тут два вопроса:

  1. Из топика «В общем случае» – я это понял что например в «process» у нас не x = x - 1, а например IO операция и что мы в целом находимся в контексте большого приложения. В этом случае я прав, а ты нет
  2. Если мы говорим про то как закодить алгоритм и нам process это реально x = x -1 – ты прав, а я нет
oxo
()
Последнее исправление: oxo (всего исправлений: 1)
Ответ на: комментарий от oxo
  1. process сложный, но вызывается одной строкой. Его действие уже вынесено в отдельную функцию и у этой функции есть входящие параметры и результат, видимые в коде, в отличие от handleProcess.

  2. Цепочка мне напоминает предварительный результат компиляции Бейсика.

Там после синтаксического разбора такое и было

line10.set_next(line20).set_next(line30)
line30.set_nextif(line20, line40)
line40.set_next(line50)
line50.set_nextif(line20, line60)

Называется, если нет goto, но очень хочется, то можно такое использовать.

monk ★★★★★
() автор топика
Ответ на: комментарий от oxo

KISS: Используется для отказа от необходимых абстракций («и так сойдет»).

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

YAGNI: Блокирует проектирование, приводит к архитектурному долгу, когда добавление функциональности требует переписывания.

Проектирование нужно, когда есть сложные данные. Если у нас набор действий, то всё проектирование - разделение кода на функции. Тут даже самое простейшее ООП только усложнит код. А в предложенной альтернативе не просто иерархия, там мини-фреймворк для написания внутреннего DSL.

Кроме того, что код без всякой нужды громоздкий, он ещё и по быстродействию хуже. Та же цепочка handler1.set_next(handler2).set_next(handler3).set_next(handler4)… не может прерваться, её нужно обходить всю, если не использовать хаков типа прерываний. В исходном коде и в коде с однозвенным циклом выйти из алгоритма можно в любом месте, если встретим нужное условие.

Kogrom
()
Последнее исправление: Kogrom (всего исправлений: 1)
Ответ на: комментарий от monk

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


def stepper(result, steps):
    if not steps or result["condition"] is None:
        return result
    else:
        result = steps[0](result)
        return stepper(result, steps[1:])

def w(env):
    x, y, z = env['x'], env['y'], env['z']
    while x > 0:
      x = x - 1
      y, z = z, y + z
    return z - 1

def f2(x):
    return stepper({'x': x, 'condition': True},
                   [lambda env: env.update({'x': env['x'] - 2}) or env,
                    lambda env: {'x': 1, 'condition': None} if x < 2 else env,
                    lambda env: env.update({'y': 1, 'z': 1}) or env,
                    w])

ugoday ★★★★★
()
Ответ на: комментарий от ugoday

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

Если уж делать из питона хаскелл, то как-то так:

def f2(x):
    return stepper({'x': x, 'condition': False},
                   [lambda env: env.update({'x': env['x'] - 2}) or env,
                    lambda env: env.update({'result': 1, 'condition': env['x'] < 0}) or env,
                    lambda env: env.update({'y': 1, 'z': 1}) or env,
                    w(env) or env])

def w(env):
  ...
  env.update({'result': z-1})
monk ★★★★★
() автор топика
Последнее исправление: monk (всего исправлений: 3)
Ответ на: комментарий от monk

причём не относительно предыдущего шага, а относительно параметров

Признаться, мне не кажется это важным в данной ситуации. Можно для явного выделения параметра останова вместо словаря использовать кортеж (error, env). Те же яйца вид в профиль.

Насчёт,

В списке функций как действия, так и условия

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

P.S. Всё равно тут главная проблема — отсутствие нормальных лямбд. А она не решается, разве python4 мастерить начнут.

ugoday ★★★★★
()
Последнее исправление: ugoday (всего исправлений: 1)

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

Во втором продолжит проверять условия после первого же done = true, не делая ничего полезного. Во-первых, гонять пустые такты - дурной тон. Во-вторых, Питон медленный и переменные в нем громоздкие. Имеем антипаттерн, который попьет крови на большой нагрузке или в цикле со множеством итераций. В-третьих, чем больше переменных, детерминирующих поведение, тем труднее читать код.

С elif - еще более трудночитаемая городуха.

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

Vidrele ★★★★★
()
Ответ на: комментарий от qulinxao3

Как выяснилось, я весьма качественно забыл питон. Разбираться как там красиво со словарями внутри лямбд работать мне лень, поэтому вот ответ на более человечном языке:

(defn continue "Продолжаем вычисление, на этом шаге нет :exit-condition."
  [_] false)
(defn break "Прерываем вычисление, на этом шаге есть только :exit-condition."
  [_] true)
(defn pass "Ничего не делаем, оставляем окружение без изменений."
  [env] env)

(defn stepper
  "Обрабатываем окружение шаг за шагом пока не выполнится :exit-condition или закончатся шаги."
  [env [{:keys [exit-condition exit-step action-step] :as step} & steps]]
  (cond
    (empty? step) env
    (exit-condition env) (exit-step env)
    :else (stepper (action-step env) steps)))


(defn fib [x]
  (stepper {:x x}
           [{:exit-condition continue
             :action-step (fn [env]
                            (assoc env :x (- (:x env) 2)))}
            {:exit-condition (fn [env]
                               (< (:x env) 2))
             :exit-step (fn [_] 1)
             :action-step pass}
            {:exit-condition continue
             :action-step (fn [env]
                            (loop [x (:x env)
                                   y 1
                                   z 1]
                              (if (<= x 0) (- z 1)
                                  (recur (dec x) z (+ y z)))))}]))

Пояснение: stepper принимает два аргумента: словарь env и список шагов для исполнения, каждый из которых является словарём с ключами :exit-condition, :exit-step и :action-step.

stepper работает пока не закончатся шаги или выполнится условие выхода, в этом случае исполняется :exit-step, в котором можно привести данные к нужному виду, например вытащить из словаря env значение, которое мы считаем результатом работы.

ugoday ★★★★★
()
Ответ на: комментарий от monk

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

Можно, конечно, добавить обработку специальных случаев, для сокращения писанины, но: а) на общую идею, которую я хотел выразить, это не влияет и б) относительно этого варианта и так довольно кратко вышло.

P.S. А впрочем, чего б нет, добавив для шагов значения по умолчанию, получаем:

(defn stepper
  "Обрабатываем окружение шаг за шагом пока не выполнится :exit-condition или закончатся шаги."
  [env [{:keys [exit-condition exit-step exit-value action-step]
         :or {exit-condition continue
              exit-step pass
              action-step pass}
         :as step} & steps]]
  (cond
    (empty? step) env
    (exit-condition env) (or exit-value (exit-step env))
    :else (stepper (action-step env) steps)))

(defn fib [x]
  (stepper {:x x}
           [{:action-step (fn [env]
                            (assoc env :x (- (:x env) 2)))}
            {:exit-condition (fn [env]
                               (< (:x env) 2))
             :exit-value 1}
            {:action-step (fn [env]
                            (loop [x (:x env)
                                   y 1
                                   z 1]
                              (if (<= x 0) (- z 1)
                                  (recur (dec x) z (+ y z)))))}]))
ugoday ★★★★★
()
Ответ на: комментарий от ugoday

Уже не жуть.

Но смысл всё равно не понятен. Фактически, получили хитровывернутый block. Который делает ровно то, что делал бы block, но выполняется, через интерпретатор (stepper).

monk ★★★★★
() автор топика
Ответ на: комментарий от ugoday

Когда такое городят на Си++ ещё можно понять. Хотя после нескольких уточнений системы правил хочется написать что-то вроде

int f(int x)
{
  return run_script("(if (< x 2) 1
    (do ((y 1 z) (z 1 (+ y z)) (x (- x 2) (- x 1))) 
        ((= x 0) (- z 1))))", 
    script_args("x", x));
}

А вот когда в таком стиле пишут на JS (в котором есть eval) и даже на clojure (в которой есть даже макросы), совершенно не понимаю.

monk ★★★★★
() автор топика
Последнее исправление: monk (всего исправлений: 1)
Ответ на: комментарий от ugoday

Покажите, что ли, как.

def stepper(steps, init = {}):
    for s in steps:
        exec(s, {}, init)
        if 'result' in init:
            break
    return init['result']

def f(x):
    return stepper(['x = x - 2', 
                    'if x < 2:\n  result=1', 
                    'y=1\nz=1', 
                    'result=w(x,y,z)'], 
                  {'x': x, 'w': w})

monk ★★★★★
() автор топика
Последнее исправление: monk (всего исправлений: 1)
Ответ на: комментарий от ugoday

Что-то много ритён консидеред хармфул нас как-то не туда завело. Вообще не туда.

Ага. Вспомнился Столяров с его «программа должна или компилироваться и выполнять конкретный алгоритм или быть скриптом и выполнять произвольный предоставленный пользователем алгоритм». Глядя на эти недоскрипты через ООП начинаешь его понимать.

monk ★★★★★
() автор топика
Ответ на: комментарий от qulinxao3

Ну да. Считаем, что переданный список обязательно на каком-то шаге установит result.

Твой вариант красивее.

monk ★★★★★
() автор топика
Последнее исправление: monk (всего исправлений: 1)
Ответ на: комментарий от monk

#импорт забыл удалить ибо по началу было из

угорая по коде-голфингу в очередной раз обнаружил что

all/any очень хороши как lambda содержимое на месте примитивной_рекурсии/итерации али цикла while :

from itertools import count

class Solution:
    def processQueries(_, c,e,q):
        w,s,i={0},defaultdict(lambda:[-1]),(a:=[*range(c+1)]).__setitem__
        f=lambda u:any(a[u]==u or[i(u,a[t:=a[u]]),u:=t]*0 for _ in count()) and u  
        g=lambda z:all(z[-1] in w and z.pop()for _ in count())or z[-1]
        #g(z.pop()and z)if z[-1]in w else z[-1]
        set(i(f(x),f(v)     )for x,v in e)
        set(s[f(x)].append(x)for x in range(c,0,-1))
        return [x in w and g(s[f(x)])or x for y,x in q if y==1 or w.add(x)]

#https://leetcode.com/problems/power-grid-maintenance от 6 ноября 25го да

CodeGolphing (интересно есть ли ArchitecticGolphing) очевидно не для непосредственного промышленного кода а как гаммы у исполнителей для овладения инструментом

qulinxao3 ★☆
()
Ответ на: комментарий от ugoday

Откровение не столько в лиспе - сколько в универсальности plex :)

т.е когда есть RAM на которой можно творить произвольную БНЮ графа графов то от кода достаточно «универсального исполнителя» по этому «метаграфу»

крч Дейкстра Goto не про готу вообщем

как и Бахман(овская) статья Программист навигатор тож

очевидно что неограниченное письмо_чтение памяти хаотично без иных инвариантов на автор(ов) кода :)

поэтому то и ЧистаКод Мартина(и прочих банд) не худший выход из длящегося с 1968 года кризиса в инжинегрии софта

qulinxao3 ★☆
()
Ответ на: комментарий от anonymous

Можно для каждого Res-ы в массиве место отвести и указать индекс элемента содержащего результат.

Пошучу

Маленькая добавочка к предложенному - «И лучше выдумать не мог».

anonymous
()
Ответ на: комментарий от anonymous

Можно для каждого Res-ы в массиве место отвести и указать индекс элемента содержащего результат.

Профит в том, что лапши if будет много меньше, да и код компактней.

anonymous
()
Ответ на: комментарий от ugoday

У нас всё не со зла. Как начинаешь докапываться, так получаешь в ответ смесь из «хотели как лучше» и «тут так принято».

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

Если бы.

Вот «этот код нарушает Принцип Открытости/Закрытости (OCP), поскольку добавление новой логики (нового условия/процесса) требует изменения существующей функции f. OCP гласит: программные сущности (классы, модули, функции и т. д.) должны быть открыты для расширения, но закрыты для модификации.» в пределе приводит к тому, что тело функции f должно быть неизменным для любой комбинации условий и процессов. То есть одна функция должна реализовывать любой возможный алгоритм. Это про «хотели как лучше».

По логике f должна просто читать скрипт с алгоритмом из параметра и это желание будет исполнено.

А дальше «тут так принято»: любую задачу реализуем через паттерны ООП. Поэтому вместо скрипта у нас будет Chain of responsibility, которым можно эмулировать машину состояний. Тут так принято.

monk ★★★★★
() автор топика
Ответ на: комментарий от ugoday

Хз ремисия Али рецидив , а после выкуривания что первая eniac такова ибо ей предшествовали сварм считалок женского рода и из этого в знаменитом Неймана( и остальных не прославленных этим) репорте чуть ли не нейро нечто а по факту память( склад по бебиджу) вырубило массу паралельности

qulinxao3 ★☆
()