if-else vs "или" операция для проверки None

Скажем, у нас есть dict, который всегда будет иметь ключи first_name и last_name, но они могут быть равны None.

{
    'first_name': None,
    'last_name': 'Bloggs'
}

Мы хотим сохранить первое имя, если оно передано или сохранить его как пустую строку, если None передан.

first_name = account['first_name'] if account['first_name'] else ""

против

first_name = account['first_name'] or ""

Однако, обе эти работы, какова разница за кулисами? Является ли более эффективным, чем другой?

Ответ 1

Из-за большей гибкости в первой версии больше всего происходит за кулисами. В конце концов, a if b else c - выражение с 3, возможно, различными входными переменными/выражениями, а a or b - двоичными. Вы можете разобрать выражения, чтобы получить лучшую идею:

def a(x):
    return x if x else ''

def b(x):
    return x or ''

>>> import dis
>>> dis.dis(a)
  2           0 LOAD_FAST                0 (x)
              2 POP_JUMP_IF_FALSE        8
              4 LOAD_FAST                0 (x)
              6 RETURN_VALUE
        >>    8 LOAD_CONST               1 ('')
             10 RETURN_VALUE
>>> dis.dis(b)
  2           0 LOAD_FAST                0 (x)
              2 JUMP_IF_TRUE_OR_POP      6
              4 LOAD_CONST               1 ('')
        >>    6 RETURN_VALUE

Ответ 2

TL;DR: Это не имеет значения. Если вы заботитесь о правильности, вы должны сравнить с None.

account['first_name'] if account['first_name'] is not None else "" 

Существует заметное влияние на то, что account['first_name'] в основном None или фактическая величина - однако это в наносекундном масштабе. Это пренебрежимо мало, если не работать в очень плотной петле.

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

Посмотрите, что происходит

Python дает много гарантий, что то, что вы пишете, - это то, что выполняется. Это означает, что a if a else b, a a if a else b случай оценивает не более двух раз. В отличие от этого, a or b оценивает ровно один раз. a

При их разборке вы можете видеть, что LOAD_NAME, LOAD_CONST и BINARY_SUBSCR происходят дважды для первого случая - но только если значение is-ish. Если он неверен, количество запросов одинаково!

dis.dis('''account['first_name'] if account['first_name'] else ""''')
  1           0 LOAD_NAME                0 (account)
              2 LOAD_CONST               0 ('first_name')
              4 BINARY_SUBSCR
              6 POP_JUMP_IF_FALSE       16
              8 LOAD_NAME                0 (account)
             10 LOAD_CONST               0 ('first_name')
             12 BINARY_SUBSCR
             14 RETURN_VALUE
        >>   16 LOAD_CONST               1 ('')
             18 RETURN_VALUE

dis.dis('''account['first_name'] or ""''')
  1           0 LOAD_NAME                0 (account)
              2 LOAD_CONST               0 ('first_name')
              4 BINARY_SUBSCR
              6 JUMP_IF_TRUE_OR_POP     10
              8 LOAD_CONST               1 ('')
        >>   10 RETURN_VALUE

Технически, операторы также выполняют другую проверку: логическую ложность (POP_JUMP_IF_FALSE) по сравнению с логической правдой (JUMP_IF_TRUE_OR_POP). Поскольку это единственная операция, она оптимизирована внутри интерпретатора, и разница незначительна.

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


Хотя в вашем случае это не делает заметной разницы, обычно лучше явно проверить, что is not None. Это позволяет различать None и другие значения false-ish, такие как False, [] или "", которые могут быть действительными.

account['first_name'] if account['first_name'] is not None else ""

Строго говоря, это наименее эффективно. В дополнение к добавленному поиску, есть дополнительный поиск для None а сравнение is not.

dis.dis('''account['first_name'] if account['first_name'] is not None else ""''')
  1           0 LOAD_NAME                0 (account)
              2 LOAD_CONST               0 ('first_name')
              4 BINARY_SUBSCR
              6 LOAD_CONST               1 (None)
              8 COMPARE_OP               9 (is not)
             10 POP_JUMP_IF_FALSE       20
             12 LOAD_NAME                0 (account)
             14 LOAD_CONST               0 ('first_name')
             16 BINARY_SUBSCR
             18 RETURN_VALUE
        >>   20 LOAD_CONST               2 ('')
             22 RETURN_VALUE

Обратите внимание, что этот тест может быть быстрее. is not None сравнивается is not None тест для идентичности - это сравнение встроенных указателей. Специально для пользовательских типов это быстрее, чем поиск и оценка пользовательского __bool__ или даже __len__.


На практике добавленный поиск не будет иметь заметной разницы в производительности. Это зависит от вас, предпочитаете ли вы более короткие a or b или более надежные a if a is not None else b. Использование a if a else b дает вам ни кратковременности, ни правильности, поэтому его следует избегать.

Вот цифры из Python 3.6.4, perf timeit:

# a is None
a or b                       | 41.4 ns +- 2.1 ns
a if a else b                | 41.4 ns +- 2.4 ns
a if a is not None else b    | 50.5 ns +- 4.4 ns
# a is not None
a or b                       | 41.0 ns +- 2.1 ns
a if a else b                | 69.9 ns +- 5.0 ns
a if a is not None else b    | 70.2 ns +- 5.4 ns

Как вы можете видеть, есть влияние стоимости a - если вам небезразличны десятки наносекунд. Операция terser с меньшим количеством основных инструкций выполняется быстрее и, что важнее, стабильна. Нет никакого существенного штрафа за добавленную is not None проверку is not None.

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

Для кода pure-Python, как и в вашем случае, интерпретатор PyPy является очевидным выбором. Кроме того быстрее в целом, кажется, оптимизировать is not None тест. Вот цифры от PyPy 5.8.0-beta0, perf timeit:

# a is None
a or b                       | 10.5 ns +- 0.7 ns
a if a else b                | 10.7 ns +- 0.8 ns
a if a is not None else b    | 10.1 ns +- 0.8 ns
# a is not None
a or b                       | 11.2 ns +- 1.0 ns
a if a else b                | 11.3 ns +- 1.0 ns
a if a is not None else b    | 10.2 ns +- 0.6 ns

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

Ответ 3

В чем разница между двумя следующими выражениями?

first_name = account['first_name'] if account['first_name'] else ""

против

first_name = account['first_name'] or ""

Основное отличие состоит в том, что первое, в Python, является условным выражением,

Выражение x if C else y сначала оценивает условие C а не x. Если C истинно, x оценивается и возвращается его значение; в противном случае y вычисляется и возвращается его значение.

а вторая использует логическую операцию:

Выражение x or y сначала оценивает x; если x истинно, возвращается его значение; в противном случае y вычисляется и возвращается возвращаемое значение.

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

Этот поиск называется индексной нотацией:

name[subscript_argument]

Обозначение __getitem__ метод __getitem__ объекта, на который ссылается name.

Для этого требуется как имя, так и аргумент индекса.

Теперь, в контексте вопроса, если он проверяет как True в булевом контексте (который выполняет непустая строка, а None - нет), для этого потребуется вторая (избыточная) загрузка как словаря, так и ключа для условное выражение, просто возвращая первый поиск для логического or операции.

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

Абстрактное синтаксическое дерево (AST)

Другие сравнили байт-код, сгенерированный обоими выражениями.

Однако AST представляет собой первое разбиение языка, которое анализируется интерпретатором.

Следующий AST демонстрирует, что второй поиск, вероятно, требует больше работы (обратите внимание, что я отформатировал вывод для более легкого анализа):

>>> print(ast.dump(ast.parse("account['first_name'] if account['first_name'] else ''").body[0]))
Expr(
    value=IfExp(
        test=Subscript(value=Name(id='account', ctx=Load()),
                       slice=Index(value=Str(s='first_name')), ctx=Load()),
        body=Subscript(value=Name(id='account', ctx=Load()),
                       slice=Index(value=Str(s='first_name')), ctx=Load()),
        orelse=Str(s='')
))

против

>>> print(ast.dump(ast.parse("account['first_name'] or ''").body[0]))
Expr(
    value=BoolOp(
        op=Or(),
        values=[
            Subscript(value=Name(id='account', ctx=Load()),
                      slice=Index(value=Str(s='first_name')), ctx=Load()),
            Str(s='')]
    )
)

Анализ байт-кода

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

>>> import dis   
>>> dis.dis("d['name'] if d['name'] else ''")
  1           0 LOAD_NAME                0 (d)
              2 LOAD_CONST               0 ('name')
              4 BINARY_SUBSCR
              6 POP_JUMP_IF_FALSE       16
              8 LOAD_NAME                0 (d)
             10 LOAD_CONST               0 ('name')
             12 BINARY_SUBSCR
             14 RETURN_VALUE
        >>   16 LOAD_CONST               1 ('')
             18 RETURN_VALUE

Для булевой операции это почти вдвое меньше:

>>> dis.dis("d['name'] or ''")
  1           0 LOAD_NAME                0 (d)
              2 LOAD_CONST               0 ('name')
              4 BINARY_SUBSCR
              6 JUMP_IF_TRUE_OR_POP     10
              8 LOAD_CONST               1 ('')
        >>   10 RETURN_VALUE

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

Поэтому давайте посмотрим, есть ли там большая разница в производительности.

Спектакль

Производительность здесь не очень важна, но иногда я должен сам убедиться:

def cond(name=False):
    d = {'name': 'thename' if name else None}
    return lambda: d['name'] if d['name'] else ''

def bool_op(name=False):
    d = {'name': 'thename' if name else None}
    return lambda: d['name'] or ''

Мы видим, что когда имя находится в словаре, логическая операция примерно на 10% быстрее, чем условная.

>>> min(timeit.repeat(cond(name=True), repeat=10))
0.11814919696189463
>>> min(timeit.repeat(bool_op(name=True), repeat=10))
0.10678509017452598

Однако, когда имя не находится в словаре, мы видим, что почти нет разницы:

>>> min(timeit.repeat(cond(name=False), repeat=10))
0.10031125508248806
>>> min(timeit.repeat(bool_op(name=False), repeat=10))
0.10030031995847821

Заметка о правильности

В общем, я бы предпочел, or булеву операцию в условное выражение - со следующими оговорками:

  • Словарь гарантированно имеет только непустые строки или None.
  • Производительность здесь имеет решающее значение.

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

first_name = account['first_name']
if first_name is None:
    first_name = ''

Повышениями являются то, что

  • поиск выполняется один раз,
  • проверка на " is None довольно быстро,
  • код явно ясен, и
  • код легко обслуживается любым программистом Python.

Это также не должно быть менее результативным:

def correct(name=False):
    d = {'name': 'thename' if name else None}
    def _correct():
        first_name = d['name']
        if first_name is None:
            first_name = ''
    return _correct

Мы видим, что мы получаем довольно конкурентоспособную производительность, когда ключ есть:

>>> min(timeit.repeat(correct(name=True), repeat=10))
0.10948465298861265
>>> min(timeit.repeat(cond(name=True), repeat=10))
0.11814919696189463
>>> min(timeit.repeat(bool_op(name=True), repeat=10))
0.10678509017452598

когда ключ не находится в словаре, это не совсем хорошо:

>>> min(timeit.repeat(correct(name=False), repeat=10))
0.11776355793699622
>>> min(timeit.repeat(cond(name=False), repeat=10))
0.10031125508248806
>>> min(timeit.repeat(bool_op(name=False), repeat=10))
0.10030031995847821

Заключение

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

Для правильности, однако, сделайте поиск один раз, проверьте идентичность None с is None, а затем переназначьте пустую строку в этом случае.

Ответ 4

Условный оператор

result = value if value else ""

Это тернарный условный оператор и в основном эквивалентен следующему утверждению if:

if value:
    result = value
else:
    result = ""

Он очень ясен и позволяет вам точно описать, какое условие вы требуете. В этом случае он просто смотрит на правдивое значение value, но вы можете легко расширить его, чтобы провести строгий тест против None например:

result = value if value is not None else ""

Это, например, сохранит значения фальшивки, такие как False или 0.

или оператора

value or ""

Это использует логическое or операторное:

Выражение x or y сначала оценивает x; если x истинно, возвращается его значение; в противном случае y вычисляется и возвращается возвращаемое значение.

Таким образом, это в основном способ получить первое правное значение (по умолчанию - правый операнд). Так что это делается так же, как value if value else "". Если это не условный оператор, он не поддерживает другие проверки, поэтому вы можете только проверить правдивость здесь.

сравнение

В вашем случае, где вы хотите просто проверить против None и вернуться к пустой строке, нет никакой разницы. Просто выберите то, что наиболее понятно для вас. С точки зрения "питонов", вероятно, предпочтительнее оператор or, так как это также немного короче.

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

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

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

Ответ 5

Я знаю, что это не отвечает на ваш вопрос об эффективности или различии за кулисами, но я хотел бы указать, что я думаю, что следующий код предпочтительнее:

first_name = account.get('first_name') or ''

Таким образом, вам не нужно дважды обращаться к account['first_name'].

Другой побочный эффект этого решения (очевидно, это зависит от того, хотите ли вы этого поведения или нет), вы никогда не получите KeyError, даже если first_name не находится в account dict. Очевидно, если вы предпочитаете видеть KeyError, это тоже хорошо.

Документация для get dict находится здесь: https://docs.python.org/3/library/stdtypes.html#dict.get

Ответ 6

Для вашего конкретного случая логический or оператор выглядит более pythonic, а также очень простой тест показывает, что он немного эффективнее:

import timeit
setup = "account = {'first_name': None, 'last_name': 'Bloggs'}"
statements = {
    'ternary conditional operator': "first_name = account['first_name'] if account['first_name'] else ''",
    'boolean or operator': "first_name = account['first_name'] or ''",
}
for label, statement in statements.items():
    elapsed_best = min(timeit.repeat(statement, setup, number=1000000, repeat=10))
    print('{}: {:.3} s'.format(label, elapsed_best))

Выход:

ternary conditional operator: 0.0303 s
boolean or operator: 0.0275 s

Принимая во внимание, что приведенные выше числа - это общее время выполнения в секундах (1000000 оценок на каждое выражение), на практике нет существенной разницы в эффективности вообще.