列表理解循环排序取决于嵌套[关闭]

Posted

技术标签:

【中文标题】列表理解循环排序取决于嵌套[关闭]【英文标题】:List comprehension loop ordering depends on nesting [closed] 【发布时间】:2021-02-20 22:32:08 【问题描述】:

如果可能,我正在寻找对嵌套数据结构使用推导式的替代方法,或者寻找适应嵌套列表推导式的方法。

如果没有理解,使用嵌套循环生成项目列表的工作方式如下:

combos = []
for a in iterable:
    for b in valid_posibilities(a):
        combos.append((a,b))

把它变成一个理解保留了循环的顺序,这使得使用多行很好:

combos = [
    (a,b)
    for a in iterable
        for b in valid_posibilities(a)
    ]

但是,这会创建一个列表。如果我想要一些代码来生成嵌套数据结构,那么我会使用这样的东西:

# same as above but instead of list of (a,b) tuples,
# I want a dictionary of a:[b] structure
combos_map = 
for a in iterable:
    options = []
    for b in valid_posibilities(a):
        options.append(b)
    combos_map[a] = options

(以下 sn-p 具有使用普通列表的等效代码,对于那些以前没有看过字典理解的人来说,第一次看到它以一种奇怪的方式嵌套很难理解)

# for people unfamilar with dictionary comprehension
# this is the equivelent nesting structure
combos = []
for a in iterable:
    options = []
    for b in valid_posibilities(a):
        options.append(b)
    combos.append(options)

######## or equivelently
combos = [
      [b
        for b in valid_posibilities(a)
      ]
    for a in iterable
    ]

现在将其转换为推导式,我们得到:

combos_map = 
    a:[b
        for b in valid_posibilities(a)
      ]
    for a in iterable
    

什么鬼?循环的顺序切换了!这是因为必须将内部循环放在内部列表中。如果当你想要一个嵌套数据结构时它总是被颠倒我会很好,但条件或非嵌套循环会使它变得更糟:

# for a list of files produce a mapping of filename:(set of all words)
# only in text files.
file_to_words_map = 
for filename in list_of_files:
    if filename.endswith(".txt"):
        word_set = set()
        for line in open(filename):
            for word in line.split():
                word_set.add(word)
        file_to_words_map[filename] = word_set
        

### or using comprehension we get this lovely mess:

file_to_words_map = 
    filename:  word
            for line in open(filename)
               for word in line.split()
        
    for filename in list_of_files
        if filename.endswith(".txt")
    

我向初学者教授 Python,当有人想要生成带有理解的嵌套数据结构时,我告诉他们“这不值得”,我希望能够将它们发送到这里作为更好的解释为什么。

因此,对于我将发送到这里的人,我正在寻找的是以下之一:

    是否有另一种方法可以重构这些循环,使代码更容易理解,而不是直接将它们粘贴在推导式中?

    有没有办法以直观的方式解释和构造这些嵌套循环? 有时,不熟悉 python 理解的人会偶然发现一些像这里显示的代码,希望会最后在这里寻找一些见解。

【问题讨论】:

让 Python 奇怪的是,当多个 for 子句出现在一个理解的同一级别时,外部循环首先出现。这与通常将它们分组的方式相反,第一个循环是内部分组的一部分。 Fortran 有隐含的 DO 循环,它们使用了更合乎逻辑的顺序。为方便起见,Python 将其颠倒过来,但它使其不太一致且更加混乱。 @MichaelButscher 这通常出现在有人使用列表推导但希望使其生成嵌套数据结构时,我不可避免地告诉他们只使用显式循环,他们问为什么。总是很难平衡挥手或过多的细节来给出一个令人满意的答案,说明为什么它不值得。我真正的目标是“查找这个答案”作为解释:) @TadhgMcDonald-Jensen - 问题以“我正在寻找一种有助于记住和轻松解析列表理解的直觉”开头。问题以放弃最初的问题结束,而是说“所以我真正的问题是:有没有另一种方法来重构这些循环,使代码更容易理解,而不是直接将它们粘贴在理解中?”。这不是大转弯,大矛盾吗?未来的读者会对这里的真正问题感到困惑。它还会引起关于已发布答案的相关性的可避免的辩论。 @fountainhead 谢谢并已修复。我开始尝试模仿 this question in intent,但在收到 cmets 关于“不要进行嵌套推导,因为它们令人困惑”之后意识到那是错误的方向,所以我改写了它,错过了开头的句子 @TadhgMcDonald-Jensen - 就我个人而言,现在已删除的原始问题是更有趣的问题(实际上已经开始着手回答它了!)。我真的不相信嵌套循环有一些需要重构的固有不可读性。 AFAIK,人们使用推导式最流行的动机是潜在的简洁性,而不是因为嵌套循环有一些不可读性 【参考方案1】:

也许问题在于您过度使用列表理解。我也喜欢它,但是,当代码变得比循环更复杂时,它的目的是什么?

如果您想继续使用列表理解优先于一切的方法,您可以将内部循环分解为辅助函数。这样更容易消化:

def collect_words(file):
    ...

file_to_words_map = 
    filename: collect_words(open(filename))
    for filename in list_of_files if filename.endswith(".txt")

顺便说一句,我不认为在多行中打破这样的陈述一定会使它们更清晰(相反,你这样做的冲动很能说明问题)。在上面的示例中,我有意重新加入了 forif 部分。

【讨论】:

缩进更多是为了让问题的读者能够跟踪语句的顺序以及它们在重写为理解时的去向。你认为它说明了什么,它足够复杂,应该重构? +1000 列表推导旨在成为创建列表的简洁方式。不要成为另一种循环。 @TadhgMcDonald-Jensen 是的,或者换句话说,您使用的工具旨在使某些陈述更加简单明了,而使用此工具不会变得更加简单明了工具。有时候,枯燥、老套的写法依然是最优雅的写法。我了解您的意图是使用这些示例来教授概念,但我不确定您可以通过一个声明传达多少信息,即即使对于经验丰富的开发人员来说,阅读本身也很容易混淆。 顺便说一句。我确实相信这是一个有趣的练习,并且通过您的file_to_words_map 代码找到了一些乐趣。而不是试图简化它,我可能会将它用于“这段代码的输出是什么?”,甚至是“如何以更易读的方式写下来?(提示:使用循环)”家庭作业。绝对是讨论代码风格的良好开端。 this is how this typically comes up for me idk 如果其中任何一个都可以工作,作为“输出是什么?”问题我觉得人们要么得到正确,要么对变量初始化的顺序感到困惑(因为定义变量的地方已经与理解混淆了)。我同意这里的源泉,我不认为嵌套的理解太难以理解你只需要期待它。【参考方案2】:

一种方法是使用生成器!因为您最终以“语句”而不是“表达式”为基础编写代码,所以它最终变得更具可扩展性。

def words_of_file(filename):
    """opens the file specified and generates all words present."""
    with open(filename) as file:
        for line in file:
            for word in line.split():
                yield word
                
def get_words_in_files(list_of_files):
    """generates tuples of form (filename, set of words) for all text files in the given list"""
    for filename in list_of_files:
        # skip non text files
        if not filename.endswith(".txt"):
            continue # can use continue to skip instead of nesting everything
        
        words_of_file = set(words_of_file(filename))
        # dict constructor takes (key,value) tuples.
        yield (filename, words_of_file)

file_to_words_map = dict(get_words_of_files(["a.txt", "b.txt", "image.png"]))

使用生成器有很多好处:

我们可以使用像withcontinue 这样的语句以及变量赋值和调试print 语句。这一切都是因为我们处于块作用域而不是表达式作用域。 words_of_file 只是生成单词,并不规定必须将它们放入set。其他一些代码可能会选择直接迭代单词或将其传递给list 构造函数。也许collections.Counter 也会有用!关键是生成器让调用者决定如何使用序列。

这也不会阻止您使用推导式或其他快捷方式,如果您想生成迭代器的所有元素,您可以 yield from 该迭代器,这样您最终可能会得到如下代码:

def words_of_file(filename):
    """opens the file specified and generates all words present."""
    with open(filename) as file:
        for line in file:
            # produces all words of the line
            yield from line.split()
            
file_to_words_map = filename:set(words_of_file(filename))
                      for filename in list_of_files
                         if filename.endswith(".txt")
                     

不同的人有不同的看法,我知道我最喜欢的是生成器唯一选项,因为我是生成器的忠实粉丝。我敢肯定,有些人喜欢嵌套推导的单线解决方案,但使用简单推导和辅助函数的最后一个版本可能是大多数人最喜欢的。

【讨论】:

一个非常好的角度。虽然,虽然 words_of_file 看起来不错,但它没有直接存储到集合中的效率,当输入中有很多重复时,这可能非常重要。 相比之下,collections.Counter(words_of_file(name)) 使用 yield from 版本比 collections.Counter(word for line in file for word in line.split()) 稍快,所以除非你总是使用内置的 setlistdict理解,使用生成器函数而不是生成器表达式不会导致任何明显的性能损失。 是的,您使用生成器来避免不必要的中间容器。谢谢你的好建议!

以上是关于列表理解循环排序取决于嵌套[关闭]的主要内容,如果未能解决你的问题,请参考以下文章

如何理解和完成下面提供的嵌套循环挑战[关闭]

具有自定义数据结构的 SwiftUI 列表,每个循环都嵌套 [关闭]

根据另一个列表中定义的元素位置对嵌套列表进行排序[关闭]

C ++ ifstream嵌套循环[关闭]

python生成器比嵌套for循环快吗? [关闭]

无法让 ForEach 循环在 SwiftUI 中使用嵌套的 JSON 数据运行 [关闭]