Питоновский способ избежать "если x: вернуть x"

У меня есть метод, который вызывает по 4 других метода для проверки определенных условий и немедленно возвращает (не проверяя следующие), когда кто-то возвращает что-то Truthy.

def check_all_conditions():
    x = check_size()
    if x:
        return x

    x = check_color()
    if x:
        return x

    x = check_tone()
    if x:
        return x

    x = check_flavor()
    if x:
        return x
    return None

Это похоже на много кода багажа. Вместо каждого двухстрочного оператора if я бы предпочел сделать что-то вроде:

x and return x

Но это недопустимый Python. Я пропустил простое, элегантное решение здесь? Кстати, в этой ситуации эти четыре метода проверки могут быть дорогими, поэтому я не хочу называть их несколько раз.

Ответ 1

Вы можете использовать цикл:

conditions = (check_size, check_color, check_tone, check_flavor)
for condition in conditions:
    result = condition()
    if result:
        return result

Это дает дополнительное преимущество в том, что теперь вы можете изменить количество условий.

Вы можете использовать map() + filter() (версии Python 3, используйте future_builtins версии в Python 2), чтобы получить первое такое подходящее значение:

try:
    # Python 2
    from future_builtins import map, filter
except ImportError:
    # Python 3
    pass

conditions = (check_size, check_color, check_tone, check_flavor)
return next(filter(None, map(lambda f: f(), conditions)), None)

но если это более удобочитаемо, то можно обсуждать.

Другой вариант - использовать выражение генератора:

conditions = (check_size, check_color, check_tone, check_flavor)
checks = (condition() for condition in conditions)
return next((check for check in checks if check), None)

Ответ 2

В качестве альтернативного ответа Мартинн вы можете связать or. Это вернет первое значение правды или None, если нет правдивого значения:

def check_all_conditions():
    return check_size() or check_color() or check_tone() or check_flavor() or None

Демо:

>>> x = [] or 0 or {} or -1 or None
>>> x
-1
>>> x = [] or 0 or {} or '' or None
>>> x is None
True

Ответ 3

Не меняйте его

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

Ответ 4

Эффективно тот же ответ, что и timgeb, но вы можете использовать скобки для более удобного форматирования:

def check_all_the_things():
    return (
        one()
        or two()
        or five()
        or three()
        or None
    )

Ответ 5

Согласно Curly law, вы можете сделать этот код более читаемым, разделив две проблемы:

  • Что я могу проверить?
  • Верна ли одна вещь?

на две функции:

def all_conditions():
    yield check_size()
    yield check_color()
    yield check_tone()
    yield check_flavor()

def check_all_conditions():
    for condition in all_conditions():
        if condition:
            return condition
    return None

Это позволяет избежать:

  • сложные логические структуры
  • действительно длинные строки
  • повторение

... при сохранении линейного, легко читаемого потока.

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

Ответ 6

Это вариант первого примера Martijns. Он также использует "коллекцию вызывающих" стилей, чтобы обеспечить короткое замыкание.

Вместо цикла вы можете использовать встроенный any.

conditions = (check_size, check_color, check_tone, check_flavor)
return any(condition() for condition in conditions) 

Обратите внимание, что any возвращает логическое значение, поэтому, если вам нужно точное возвращаемое значение проверки, это решение не будет работать. any не будет различать 14, 'red', 'sharp', 'spicy' как возвращаемые значения, все они будут возвращены как True.

Ответ 7

Вы считали, что просто пишете if x: return x все в одной строке?

def check_all_conditions():
    x = check_size()
    if x: return x

    x = check_color()
    if x: return x

    x = check_tone()
    if x: return x

    x = check_flavor()
    if x: return x

    return None

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

Ответ 8

Я очень удивлен, что никто не упомянул встроенный any, который сделан для этой цели:

def check_all_conditions():
    return any([
        check_size(),
        check_color(),
        check_tone(),
        check_flavor()
    ])

Обратите внимание, что хотя эта реализация, вероятно, самая ясная, она оценивает все проверки, даже если первая имеет значение True.


Если вам действительно нужно остановиться при первой неудачной проверке, рассмотрите возможность использования reduce, которая предназначена для преобразования списка в простое значение:

def check_all_conditions():
    checks = [check_size, check_color, check_tone, check_flavor]
    return reduce(lambda a, f: a or f(), checks, False)

reduce(function, iterable[, initializer]): применить функцию двух аргументы кумулятивно к пунктам iterable, слева направо, чтобы уменьшить итерабельность до одного значения. Левый аргумент x, - это накопленное значение, а правый аргумент y - это обновление значение из итерируемого. Если имеется необязательный инициализатор, помещенный перед элементами итерации в расчете

В вашем случае:

  • lambda a, f: a or f() - это функция, которая проверяет, что либо аккумулятор a, либо текущая проверка f() равна True. Обратите внимание, что если a True, f() не будет оцениваться.
  • checks содержит функции проверки (элемент f из лямбда)
  • False - это начальное значение, иначе проверка не произойдет, и результат всегда будет True

any и reduce являются базовыми инструментами для функционального программирования. Я настоятельно рекомендую вам обучить их, а также map, что тоже удивительно!

Ответ 9

Если вам нужна такая же структура кода, вы можете использовать трехмерные выражения!

def check_all_conditions():
    x = check_size()
    x = x if x else check_color()
    x = x if x else check_tone()
    x = x if x else check_flavor()

    return x if x else None

Я думаю, что это выглядит красиво и понятно, если вы посмотрите на него.

Демо:

Screenshot of it running

Ответ 10

Небольшая вариация первого примера Martijns выше, что позволяет избежать if внутри цикла:

Status = None
for c in [check_size, check_color, check_tone, check_flavor]:
  Status = Status or c();
return Status

Ответ 11

Для меня лучшим ответом является то, что из @phil-frost, а затем @wayne-werner's.

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

Итак, я бы смешал ответ @PhilFrost с идеей сохранения одного типа:

def all_conditions(x):
    yield check_size(x)
    yield check_color(x)
    yield check_tone(x)
    yield check_flavor(x)

def assessed_x(x,func=all_conditions):
    for condition in func(x):
        if condition:
            return x
    return None

Обратите внимание, что в качестве аргумента передается x, но также all_conditions используется как переданный генератор контрольных функций, где все они получают проверку x и возвращают True или False, Используя func с all_conditions как значение по умолчанию, вы можете использовать assessed_x(x), или вы можете передать дополнительный персонализированный генератор через func.

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

Ответ 12

В идеале я бы перезаписал функции check_, чтобы вернуть True или False, а не значение. Затем ваши чеки становятся

if check_size(x):
    return x
#etc

Предполагая, что ваш x не является неизменным, ваша функция все равно может его изменить (хотя они не могут переназначить его), но функция, называемая check, в действительности не должна ее изменять.

Ответ 13

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

Основная идея заключается в raise исключении, когда одна из функций оценивается как правдивая и возвращает результат. Вот как это выглядит:

def check_conditions():
    try:
        assertFalsey(
            check_size,
            check_color,
            check_tone,
            check_flavor)
    except TruthyException as e:
        return e.trigger
    else:
        return None

Вам понадобится функция assertFalsey, которая вызывает исключение, если один из аргументов вызываемой функции оценивается как правдивый:

def assertFalsey(*funcs):
    for f in funcs:
        o = f()
        if o:
            raise TruthyException(o)

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

И, конечно, вам понадобится TruthyException. Это исключение предоставляет object, вызвавшее исключение:

class TruthyException(Exception):
    def __init__(self, obj, *args):
        super().__init__(*args)
        self.trigger = obj

Вы можете превратить исходную функцию в нечто более общее, конечно:

def get_truthy_condition(*conditions):
    try:
        assertFalsey(*conditions)
    except TruthyException as e:
        return e.trigger
    else:
        return None

result = get_truthy_condition(check_size, check_color, check_tone, check_flavor)

Это может быть немного медленнее, потому что вы используете инструкцию if и обрабатываете исключение. Тем не менее, исключение обрабатывается не более одного раза, поэтому удар по производительности должен быть незначительным, если вы не ожидаете запустить проверку и получить значение True много тысяч раз.

Ответ 14

Пути pythonic либо используют сокращение (как уже упоминалось), либо itertools (как показано ниже), но мне кажется, что простое использование короткого замыкания оператора or дает более четкий код

from itertools import imap, dropwhile

def check_all_conditions():
    conditions = (check_size,\
        check_color,\
        check_tone,\
        check_flavor)
    results_gen = dropwhile(lambda x:not x, imap(lambda check:check(), conditions))
    try:
        return results_gen.next()
    except StopIteration:
        return None

Ответ 15

Мне нравится @timgeb's. Тем временем я хотел бы добавить, что выражение None в операторе return не требуется, так как вычисляется коллекция or разделенных операторов, и возвращается первый none-zero, none-empty, none-None и если нет, то возвращается None, есть ли None или нет!

Итак, моя функция check_all_conditions() выглядит так:

def check_all_conditions():
    return check_size() or check_color() or check_tone() or check_flavor()

Используя timeit с number=10**7, я просмотрел время выполнения ряда предложений. Для сравнения я использовал функцию random.random() для возврата строки или None на основе случайных чисел. Вот весь код:

import random
import timeit

def check_size():
    if random.random() < 0.25: return "BIG"

def check_color():
    if random.random() < 0.25: return "RED"

def check_tone():
    if random.random() < 0.25: return "SOFT"

def check_flavor():
    if random.random() < 0.25: return "SWEET"

def check_all_conditions_Bernard():
    x = check_size()
    if x:
        return x

    x = check_color()
    if x:
        return x

    x = check_tone()
    if x:
        return x

    x = check_flavor()
    if x:
        return x
    return None

def check_all_Martijn_Pieters():
    conditions = (check_size, check_color, check_tone, check_flavor)
    for condition in conditions:
        result = condition()
        if result:
            return result

def check_all_conditions_timgeb():
    return check_size() or check_color() or check_tone() or check_flavor() or None

def check_all_conditions_Reza():
    return check_size() or check_color() or check_tone() or check_flavor()

def check_all_conditions_Phinet():
    x = check_size()
    x = x if x else check_color()
    x = x if x else check_tone()
    x = x if x else check_flavor()

    return x if x else None

def all_conditions():
    yield check_size()
    yield check_color()
    yield check_tone()
    yield check_flavor()

def check_all_conditions_Phil_Frost():
    for condition in all_conditions():
        if condition:
            return condition

def main():
    num = 10000000
    random.seed(20)
    print("Bernard:", timeit.timeit('check_all_conditions_Bernard()', 'from __main__ import check_all_conditions_Bernard', number=num))
    random.seed(20)
    print("Martijn Pieters:", timeit.timeit('check_all_Martijn_Pieters()', 'from __main__ import check_all_Martijn_Pieters', number=num))
    random.seed(20)
    print("timgeb:", timeit.timeit('check_all_conditions_timgeb()', 'from __main__ import check_all_conditions_timgeb', number=num))
    random.seed(20)
    print("Reza:", timeit.timeit('check_all_conditions_Reza()', 'from __main__ import check_all_conditions_Reza', number=num))
    random.seed(20)
    print("Phinet:", timeit.timeit('check_all_conditions_Phinet()', 'from __main__ import check_all_conditions_Phinet', number=num))
    random.seed(20)
    print("Phil Frost:", timeit.timeit('check_all_conditions_Phil_Frost()', 'from __main__ import check_all_conditions_Phil_Frost', number=num))

if __name__ == '__main__':
    main()

И вот результаты:

Bernard: 7.398444877040768
Martijn Pieters: 8.506569201346597
timgeb: 7.244275416364456
Reza: 6.982133448743038
Phinet: 7.925932800076634
Phil Frost: 11.924794811353031

Ответ 16

Я собираюсь прыгнуть сюда и никогда не писал ни одной строки Python, но я полагаю, что if x = check_something(): return x действительно?

если так:

def check_all_conditions():

    if x = check_size(): return x

    if x = check_color(): return x

    if x = check_tone(): return x

    if x = check_flavor(): return x

    return None

Ответ 17

Я видел некоторые интересные реализации операторов switch/case с dicts в прошлом, которые привели меня к этому ответу. Используя предоставленный вами пример, вы получите следующее. (Это безумие using_complete_sentences_for_function_names, поэтому check_all_conditions переименовано в status. См. (1))

def status(k = 'a', s = {'a':'b','b':'c','c':'d','d':None}) :
  select = lambda next, test : test if test else next
  d = {'a': lambda : select(s['a'], check_size()  ),
       'b': lambda : select(s['b'], check_color() ),
       'c': lambda : select(s['c'], check_tone()  ),
       'd': lambda : select(s['d'], check_flavor())}
  while k in d : k = d[k]()
  return k

Функция выбора исключает необходимость вызова двух check_FUNCTION дважды, т.е. вы избегаете check_FUNCTION() if check_FUNCTION() else next путем добавления другого функционального уровня. Это полезно для длительных функций. Lambdas в задержке dict заставляет его значения до цикла while.

В качестве бонуса вы можете изменить порядок выполнения и даже пропустить некоторые из тестов, изменив k и s, например. k='c',s={'c':'b','b':None} уменьшает количество тестов и отменяет исходный порядок обработки.

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

В качестве альтернативы более простая реализация может быть следующей:

def status(k=check_size) :
  select = lambda next, test : test if test else next
  d = {check_size  : lambda : select(check_color,  check_size()  ),
       check_color : lambda : select(check_tone,   check_color() ),
       check_tone  : lambda : select(check_flavor, check_tone()  ),
       check_flavor: lambda : select(None,         check_flavor())}
  while k in d : k = d[k]()
  return k
  • Я имею в виду это не с точки зрения pep8, а с точки зрения использования одного краткого описательного слова вместо предложения. Предоставленный OP может следовать за некоторым соглашением о кодировании, работая с какой-либо существующей базой кода или не заботясь о красных терминах в своей кодовой базе.