if-else与“或”操作进行无检查

时间:2018-08-11 19:20:05

标签: python

比方说,我们有一个字典,它将始终具有first_name和last_name键,但它们可能等于None。

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

如果要传入名字,我们想保存它;如果没有传入名字,我们要保存为一个空字符串。

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

vs

first_name = account['first_name'] or ""

这两项工作在幕后有什么区别?一个比另一个更有效吗?

6 个答案:

答案 0 :(得分:6)

由于其更大的灵活性,因此在第一个版本中有许多幕后工作。毕竟,a if b else c是具有3个可能不同的输入变量/表达式的表达式,而a or b是二进制。您可以disassemble表达式以更好地理解:

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

答案 1 :(得分:6)

以下两个表达式有什么区别?

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

vs

first_name = account['first_name'] or ""

主要区别在于,在Python中,第一个是conditional expression

  

表达式x if C else y首先计算条件,C而是   比x。如果C为真,则评估x并返回其值;   否则,将评估y并返回其值。

第二个使用boolean operation

  

表达式x or y首先计算x;如果x为true,则其值为   回到;否则,将评估y并得出结果值为   返回。

请注意,第一个可能需要两次键查找,而第二个可能只需要一个键查找。

此查找称为subscript notation

name[subscript_argument]

下标符号执行__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的身份,然后在这种情况下将其重新分配给空字符串。

答案 2 :(得分:3)

TLDR:没关系。如果您关心正确性,则应该将其与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的评估最多为{em>两次。相比之下,a or b仅对a进行一次评估{em> 。

在反汇编中,您可以看到LOAD_NAMELOAD_CONSTBINARY_SUBSCR在第一种情况下发生两次-但仅当值是true-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[]""

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测试比较身份-这是内置的指针比较。特别是对于自定义类型,这比查找和评估自定义__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支票没有重大损失。

无论哪种方式,如果您关心性能-请勿针对CPython进行优化!如果您需要速度,那么使用JIT /静态编译器可以显着提高收益。但是,它们的优化使指令计数成为性能指标的误导。

对于纯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 :(得分:1)

条件运算符

result = value if value else ""

这是三元组conditional operator,基本上等同于以下if语句:

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

它非常明确,可以让您准确描述所需的条件。在这种情况下,它仅查看value的真实值,但是您可以轻松扩展此值以对None进行严格测试,例如:

result = value if value is not None else ""

例如,这将保留伪造的值,例如False0

或运算符

value or ""

这使用了boolean or operator

  

表达式x or y首先计算x;如果x为true,则返回其值;否则,将评估y并返回结果值。

因此,这基本上是获取第一个真实值(默认为正确的操作数)的方法。因此,这与value if value else ""相同。除非有条件运算符,否则它不支持其他检查,因此您只能在此处检查真实性。

比较

在您的情况下,您只想检查None并退回到一个空字符串,就没有任何区别。只需选择最可理解的内容即可。从“ pythonic”的角度来看,人们可能更喜欢or运算符,因为它也短一些。

从性能的角度来看,在这种情况下,条件运算符会稍微昂贵一些,因为这需要两次评估字典访问权限。实际上,这并不是很明显,尤其是对于词典访问而言。

如果您确实认为这可能对您的应用程序性能产生影响,那么您不应该相信从单个语句的隔离基准中获得的数字;相反,您应该对应用程序进行概要分析,然后尝试确定可以改进的瓶颈。我向您保证,第二次访问字典对您有任何帮助。

是的,您可以完全忽略性能参数。只需选择您喜欢的任何东西,什么对您来说最有意义。还考虑是否只需要进行真实性检查,或者对None进行严格检查是否会更好。

答案 4 :(得分:1)

我知道这不能回答您有关效率或幕后差异的问题,但我想指出,我认为以下代码更可取:

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

这样,您不必两次访问account['first_name']

此解决方案的另一个副作用(显然取决于您是否要执行此操作),即使KeyError不在first_name中,您也永远不会得到account字典显然,如果您也希望看到KeyError也很好。

dict的{​​{1}}的文档在这里:https://docs.python.org/3/library/stdtypes.html#dict.get

答案 5 :(得分:0)

对于您的特定情况,布尔or运算符看起来更具有Python风格,并且非常简单的基准测试表明它的效率更高:

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个评估),实际上,效率根本没有显着差异。