比方说,我们有一个字典,它将始终具有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 ""
这两项工作在幕后有什么区别?一个比另一个更有效吗?
答案 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演示了第二次查找可能涉及更多的工作(请注意,我已经格式化了输出以便于分析):
>>> 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
的检查非常快,这也不应该表现得更好:
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_NAME
,LOAD_CONST
和BINARY_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 ""
例如,这将保留伪造的值,例如False
或0
。
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个评估),实际上,效率根本没有显着差异。