是否有更好的方法(而不是变量)来查找循环块是否被执行(甚至一次)?

时间:2017-05-23 11:15:29

标签: python for-loop

我知道python有else循环功能:

for item in items:
    # loop block
else:
    # will execute if there is no exception or break in loop block

由于这个功能,我想知道在python中是否还有其他关于循环的聪明之处。现在我想找到一个更好的方法(而不是变量)来查找循环块是否被执行(甚至一次):

items = []
for item in items:
    # loop block

if #loop block was executed:
    print("Big brother is watching you!")

4 个答案:

答案 0 :(得分:1)

如果item未在其他任何地方定义,您只需检查是否已分配item

items = []
for item in items:
    pass
try:
    del item
except NameError:
    print("loop wasn't executed")
else:
    print("loop was executed")

因此,如果items为空,则不执行循环,因此item未定义,您将获得异常。

del item调用可确保在您第二次执行此代码时item不存在。

(确定没有使用其他变量,但它仍然过于复杂:))

答案 1 :(得分:1)

items = []
for item in items:
    # loop block

if items:
    print("Big brother is watching you!")

答案 2 :(得分:1)

一些神奇的方法:

emptysentinel = item = object()
for item in items:
    # loop block

if item is not emptysentinel:
    print("Big brother is watching you!")

此方法提供了两个好处:

  1. 避免在每个循环开销中添加任何(与Jean-François Fabre's approach相同)
  2. 最后检查是否有任何循环发生的成本是该语言中第二便宜的条件检查(唯一便宜的是对值TrueFalse的变量进行直接真实性测试,或None;这是一个字节代码,执行直接C指针比较并存储TrueFalse,然后进行相同的直接真实性测试)(类似于Jean-François Fabre's approach的成本items为非空值,而便宜为空时更便宜)

缺点是:

  1. 需要创建一个新对象来用作哨兵(不算什么;普通object的开销非常低,仅使用16个字节的内存,而CPU时间却可以忽略不计)
  2. 有点神奇(这种使用哨兵不是“明显的方式”)

我个人会坚持标记的循环:

empty = True
for item in items:
    empty = False
    # loop block

if not empty:
    print("Big brother is watching you!")

是的,它必须一遍又一遍地存储False,但是(至少在CPython 3上,只要您在函数作用域内),这只是几个非常便宜的字节码:

  1. LOAD_CONST(C级数组查找操作,用于从函数常量中拉出False
  2. STORE_FAST(C级数组存储操作,用于将刚加载的值推入分配给empty变量的帧的数组插槽中)

只要您在循环中进行任何实际工作,额外的LOAD_CONST / STORE_FAST的花费就毫无意义;在我的机器上,每个循环的成本增加了12-13纳秒。首先创建一个emptysentinel对象这样简单的事情的开销大约是90 ns。在简单的微基准测试中,基于emptysentinel的方法不会比基于empty标志的方法领先,直到items包含至少25个元素(您可以提前八个元素,如果您将哨兵创建方式更改为emptysentinel = item = [],则可以避免加载全局变量并通过常规函数调用机制进行构造,但是对哨兵使用空的list会使意图变得不那么明显。)

如果常见的情况是items是非空的,则Jean-François Fabre's approach的稍微修改(对于样式/最低定位的try块)版本是最快的:

for item in items:
    pass

try:
    del item
except NameError:
    pass
else:
    # Everything but the del should go here, not the try block, so you don't
    # accidentally catch NameErrors from other sources
    print("Big brother is watching you!")

不需要初始化标记或标志,并且不会引发异常的try块非常便宜(当items是一个tuple时)元素,它比标志方法要快一点,并且随着items的增大而变得更快;当items为非空时,它也比哨兵方法更快,但这是固定开销的问题;每增加一件商品的成本显然是相同的。

问题是,当items为空时,它要昂贵得多。每种方法的微基准(将print# loop block替换为pass)的空items的成本为:

  1. 基于标记:64.8 ns
  2. 基于前哨:159 ns(如果使用emptysentinel而非[]创建object(),则为87.6)
  3. 基于
  4. try / except / else:661 ns

因此,我为您提供使用哪种方法的最终规则是:

  1. 使用基于标志的方法
  2. 认真地,使用基于标志的方法;如果items在很短的时间内为空,则最快,而在不为空时,则不要太长,更重要的是,它显而易见
  3. 真的吗?好的,然后:如果您的非空输入比较大并且确实需要速度,但是空输入仍然是半常见的,请使用基于哨兵的方法;它可以像try / except一样缩放,并且可以处理空输入,而不会降低病理学速度
  4. 如果您的输入几乎总是非空的,则可以使用try / except / else方法,这对于非空items始终是最快的,每当items为空时,都要付出75个长循环的开销。

旁注:另一种相对明显的方法(相当于对我而言明显的标志)是使用enumerate;如果您想知道许多物品的数量,这不仅很有用,而且不仅是/不是“是否有物品?”,但它并不可怕:

numitems = 0
for numitems, item in enumerate(items, 1):
    # loop block

if numitems:
    print("Big brother is watching you!")

缺点是,在除一种情况以外的所有情况下,它都比所有其他方法慢。当try为空时,它比except / else / items快。与基于标志的方法相比,它的每个循环开销更高,并且与所有其他选项相比,其固定开销更高。显而易见,基于标志的方法也是如此,并且标志速度更快,因此只需使用它们即可。

答案 3 :(得分:0)

另一种解决方案是检查locals()中的循环变量:

for item in items:
    # loop block

# loop block was executed
if 'item' in locals():
    print("Big brother is watching you!")

尽管有一些限制:

  1. 如果在循环之前定义了变量,则将满足条件。
  2. 如果您要对该变量进行全局重命名,则变量名的字符串表示将不满足搜索条件。