关于列表:Python:通过对象在某些地方和最后都执行代码来迭代

Python: Iterate through object executing code both at certain places and also at end

下面是一些要解释的示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
outputText=""
counter=0
for obj in specialObjects:
    if (obj.id < 400) or (obj.name.startswith("he")) or (obj.deliberateBreak==True):
        print"The object %s is causing a section break."%obj.details
        outputText = outputText.rjust(80)
        open("file%d.txt"%counter,"w").write(outputText)
        outputText=""
    outputText+=obj.shortValue()
# THIS CODE IS DUPLICATED
outputText = outputText.rjust(80)
open("file%d.txt"%counter,"w").write(outputText)

我需要做的是迭代这些特殊对象的列表,每次检查几个不同的条件。如果满足任何条件(如图所示),那么我需要获取当前输出缓冲区,将其写入一个文件,然后启动一个新的输出缓冲区并继续处理。

这里的问题是代码重复。注意这两行(outputText=和open)是如何复制的。如果我没有输入第二组行,最后一组对象将被处理,但它们的输出将永远不会被写入。

我可以想到两种可能的解决方案来防止代码重复。它们看起来都有点不雅,所以我想知道是否还有更好的方法。

1)包装将在一个函数中重复的代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
outputText=""
counter=0
for obj in specialObjects:
    if (obj.id < 400) or (obj.name.startswith("he")) or (obj.deliberateBreak==True):
        print"The object %s is causing a section break."%obj.details
        counter = writeData(outputText)
        outputText=""
    outputText+=obj.shortValue()
writeData(outputText,counter)

def writeData(outputText,counter):
    outputText = outputText.rjust(80)
    open("file%d.txt"%counter,"w").write(outputText)
    return counter+1

2)使用数字for循环,计数比对象列表的长度高一个;使用该值作为标志表示"写入,但现在退出":

1
2
3
4
5
6
7
8
9
10
11
outputText=""
counter=0
for obj in range(len(specialObjects))+1:
    if (obj = len(specialObjects)) or (specialObjects[obj].id < 400) or (specialObjects[obj].name.startswith("he")) or (specialOejcts[obj].deliberateBreak==True):
        print"The object %s is causing a section break."%specialObjects[obj].details
        outputText = outputText.rjust(80)
        open("file%d.txt"%counter,"w").write(outputText)
        outputText=""
        if (obj==len(specialObjects)):
            break
    outputText+=specialObjects[obj].shortValue()

如果我必须选择一个,我可能会选择2,但如果需要使用更复杂的布尔逻辑,这可能最终会用"if"语句创建一些奇怪的边缘情况。

有没有一种更干净或更"Python式"的方法可以在不重复代码的情况下完成这项工作?

谢谢!


这里有一种处理哨兵物体的方法。这和你的第二个选择很相似,但我觉得更干净。

1
2
3
4
5
6
7
8
for obj in itertools.chain(specialObjects, [None]):
    if (obj is None) or (obj.id < 400) or (obj.name.startswith("he")) or (obj.deliberateBreak==True):
        outputText = outputText.rjust(80)
        open("file%d.txt"%counter,"w").write(outputText)
        if obj is None: break
        print"The object %s is causing a section break."%obj.details
        outputText=""
    outputText+=obj.shortValue()

当我发现自己在编写这样的代码时,我在一个集合上进行迭代,并在循环结束后重复代码,我通常把它当作我没有在正确的事情上进行迭代的标志。

在本例中,您将遍历对象列表。但我认为,您真正想要迭代的是一组对象的列表。这就是itertools.groupby的用途。

您的代码有很多情况,所以我将使用一个简化的示例来说明如何消除重复的代码。例如,我有一个这样的列表:

1
2
3
things = ["apples","oranges","pears", None,
         "potatoes","tomatoes", None,
         "oatmeal","eggs"]

这是对象列表。仔细看,有几个由None分隔的对象组(注意,您通常将things表示为嵌套列表,但为了示例的目的,让我们忽略这一点)。我的目标是在单独的行上打印出每个组:

1
2
3
apples, oranges, pears
potatoes, tomatoes
oatmeal, eggs

这样做的"丑陋"方式是:

1
2
3
4
5
6
7
8
9
current_things = []
for thing in things:
    if thing is None:
        print",".join(current_things)
        current_things = []
    else:
        current_things.append(thing)

print",".join(current_things)

如你所见,我们在循环之后有一个复制的print。讨厌!

下面是使用groupby的解决方案:

1
2
3
4
5
from itertools import groupby

for key, group in groupby(things, key=lambda x: x is not None):
    if key:
        print",".join(group)

groupby具有一个不可重复(things和一个关键功能。它查看iterable的每个元素并应用key函数。当键更改值时,将形成一个新组。结果是一个迭代器,它返回(key, group)对。

在这种情况下,我们将使用None的检查作为我们的关键功能。这就是为什么我们需要if key:,因为我们的列表中会有对应于None元素的大小为1的组。我们就跳过这些。

如您所见,groupby允许我们迭代我们真正想要迭代的东西:对象组。这对于我们的问题来说更自然,因此代码简化了。看起来您的代码与上面的示例非常相似,只是您的键函数将检查对象的各种属性(obj.id < 400 ...)。我将把实现细节留给您……


如果使用迭代器,有一个解决方案,next可以在末尾给出一个特殊值。因此,您可以使用sentinel来检查当前对象是否为真对象,或者您是否完成了迭代。

尝试如下操作:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
outputText=""
counter=0
ending = object()
it = iter(specialObjects)
while True:
    obj = next(it, ending)
    if obj is ending or obj.id < 400 or obj.name.startswith("he") or obj.deliberateBreak:
        outputText = outputText.rjust(80)
        open("file%d.txt"%counter,"w").write(outputText)
        counter += 1
        outputText=""
    if obj is ending:
        break
    outputText+=obj.shortValue()

您可以将分解对象的代码分离到生成器中,这样以后的处理步骤就不需要重复了。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
def yield_sections(specialObjects):
    outputText = ''
    for obj in specialObjects:
        if (obj.id < 400) or (obj.name.startswith("he")) or (obj.deliberateBreak==True):
            yield outputText
            outputText = ''
        outputText += obj.shortValue()
    if outputText:
        yield outputText


for counter, outputText in enumerate(yield_sections(specialObjects)):
    outputText = outputText.rjust(80)
    open("file%d.txt"%counter,"w").write(outputText)