Python 列推导总是良好的编程习惯吗?

2 投票
3 回答
1348 浏览
提问于 2025-04-17 06:32

为了让问题更清楚,我用一个具体的例子来说明。

我有一份大学课程的列表,每门课程都有几个字段(这些字段都是字符串)。用户给我一串搜索词,我就返回一份符合所有搜索词的课程列表。这可以通过一个简单的列表推导式或者几个嵌套的循环来实现。

下面是实现的代码。首先是 Course 类:

class Course:
    def __init__(self, date, title, instructor, ID, description, instructorDescription, *args):
        self.date = date
        self.title = title
        self.instructor = instructor
        self.ID = ID
        self.description = description
        self.instructorDescription = instructorDescription
        self.misc = args

每个字段都是字符串,除了 misc,它是一个字符串列表。

接下来是用单个列表推导式进行搜索的代码。courses 是课程的列表,query 是搜索词的字符串,比如 "历史 项目"。

def searchCourses(courses, query):
    terms = query.lower().strip().split()
    return tuple(course for course in courses if all(
            term in course.date.lower() or
            term in course.title.lower() or
            term in course.instructor.lower() or
            term in course.ID.lower() or
            term in course.description.lower() or
            term in course.instructorDescription.lower() or
            any(term in item.lower() for item in course.misc)
        for term in terms))

你会发现,复杂的列表推导式读起来比较费劲。

我用嵌套的循环实现了相同的逻辑,得到了这个替代方案:

def searchCourses2(courses, query):
    terms = query.lower().strip().split()
    results = []
    for course in courses:
        for term in terms:
            if (term in course.date.lower() or
                term in course.title.lower() or
                term in course.instructor.lower() or
                term in course.ID.lower() or
                term in course.description.lower() or
                term in course.instructorDescription.lower()):
                break
            for item in course.misc:
                if term in item.lower():
                    break
            else:
                continue
            break
        else:
            continue
        results.append(course)
    return tuple(results)

不过,这种逻辑也不容易理解。我已经验证了这两种方法都能返回正确的结果。

这两种方法在速度上几乎是一样的,除了某些情况下。我用 timeit 进行了测试,发现当用户搜索多个不常见的词时,前者快三倍,而当用户搜索多个常见的词时,后者快三倍。不过,这个差距并不大到让我担心。

所以我的问题是:哪种方法更好?列表推导式总是更好吗,还是复杂的语句应该用嵌套循环来处理?或者有没有更好的解决方案?

3 个回答

0

我觉得列表推导式并不总是最好的选择。有时候,传统的 for 循环效果更好(我发现这在处理语法时尤其明显)。

还有第三种方法,就是使用内置的 filter 函数或者 itertools.ifilter 迭代器。用法如下:

result = ifilter(test1f, ifilter(test2f, ifilter(test3f, someiterator)))
try:
  result.next()
  return True
except StopIteration:
  return False

在这个特定的情况下,你需要的测试函数数量取决于提交的不同搜索词的数量,所以我不建议在这里使用 ifilter,除非你想实现一种将多个测试合并成一个函数的方法(我自己做过,但你现在的做法已经足够了,所以没必要麻烦)。

在其他更简单的情况下,filter 的表现非常不错,而且它的效率很高,因为它是用 C 语言实现的。通过基于迭代器的方法,你通常可以通过一两种方式修改数据流来解决看似复杂的问题(使用过滤函数或其他方法),然后将结果流传递给一个常用的库函数。

例如,这里有一个素数筛选器,它大大减少了你识别素数时需要进行的因式分解工作量(第一次浪费的努力出现在 49 这个数字上,之前它已经连续输出了 12 个实际的素数):

sieve = itertools.ifilter(lambda x: x % 5 != 0,
  itertools.ifilter(lambda x: x % 3 != 0,
  itertools.ifilter(lambda x: x % 2 != 0, itertools.count(7))))
1

列表推导式(或者叫生成器表达式)更贴近你想做的事情,也就是从其他值的集合中组合生成一组新的值。

嵌套的for循环可以让你组合执行一些操作。你当然可以用这些操作来建立一个集合,和列表推导式做同样的事情(就像你已经做的那样),但是你需要写很多额外的代码来创建这个列表、往里面添加元素和返回它。多层循环中复杂的中断和继续结构让我觉得比起相应的列表推导式,直观理解起来要难得多。我到现在还搞不清楚它是怎么工作的(不过我怀疑在StackOverflow上显示的时候可能有缩进错误)。根据我的经验,带有中断和继续的嵌套循环也是难以发现bug的一个重要来源。

不过在我看来,这两种写法中真正让人难以理解的地方是判断一个课程是否符合查询条件的逻辑。这个操作应该提取成一个在Course类中的方法(就像Blair的回答中展示的那样),或者如果你不能或不想修改这个类,你也可以定义一个单独的函数来检查课程是否符合查询条件。这样一来,无论是for循环还是列表推导式都变得几乎很简单。

4

在我看来,当某种编程方式比其他方式更清晰(或者至少不比其他方式差)并且更简洁时,这就是好的编程习惯。在这个例子中,两种选择都不是特别清楚。就我个人而言,我会把搜索逻辑放在Course类里面。这样对我来说更有意义,因为这个逻辑和这个类是相关的。

class Course:
    def __init__(self, date, title, instructor, ID, description, instructorDescription, *args):
        self.date = date
        self.title = title
        self.instructor = instructor
        self.ID = ID
        self.description = description
        self.instructorDescription = instructorDescription
        self.misc = args

    def matches_term(self, term):
        if term in self.date.lower():
            return True
        # etc
        return False

然后你可以用更简单的生成器(或者列表)表达式来进行搜索:

def searchCourses(courses, query):
    terms = query.lower().strip().split()
    return tuple(course for course in courses
                 if all(course.matches_term(term)
                        for term in terms)
                )

一个简单的测试:

courses = (
    Course("today", "", "", "", "", ""),
    Course("wednesday", "", "", "", "", ""),
    Course("today", "", "", "", "", ""),
    Course("sunday", "", "", "", "", ""),
)

results = searchCourses(courses, "on today or wednesday")
for course in results:
    print course.date

输出结果是:

today
wednesday
today

撰写回答