在Python中如何链式使用map和filter?

60 投票
10 回答
35015 浏览
提问于 2025-04-18 13:53

我现在正在学习Python,之前学过JavaScript和Ruby。对于我来说,习惯了把很多转换和过滤操作连在一起,但我觉得在Python里这样做可能不太对:filter函数需要在可遍历对象之前放一个lambda表达式,所以写一个长的或者多行的函数看起来很奇怪,而把它们连在一起又得反着写,这样就不太好读了。

那么,在Python中,如何写出这个JavaScript函数里的映射和过滤呢?

let is_in_stock = function() /* ... */
let as_item = function() /* ... */

let low_weight_items = shop.inventory
    .map(as_item)
    .filter(is_in_stock)
    .filter(item => item.weight < 1000)
    .map(item => {
        if (item.type == "cake") {
            let catalog_item = retrieve_catalog_item(item.id);

            return {
                id: item.id,
                weight: item.weight,
                barcode: catalog_item.barcode
            };
        } else {
            return default_transformer(item);
        }
    });

我知道我可以用列表推导式来处理第一个映射和接下来的两个过滤,但我不太确定最后一个映射该怎么做,以及如何把所有的部分组合在一起。

谢谢!

10 个回答

2

你可以通过在生成器表达式中使用海象运算符来实现这个功能。

low_weight_items = (
    z
    for x in [
        Item(1, 100, "cake"),
        Item(2, 1000, "cake"),
        Item(3, 900, "cake"),
        Item(4, 10000, "cake"),
        Item(5, 100, "bread"),
    ]
    if (y := as_item(x))
    if is_in_stock(y)
    if y.weight < 1000
    if (z := transform(y))
)

不过,你需要给不同的变量(在这个例子中是x/y/z)赋值,因为海象运算符不能给已经存在的变量赋值。


完整示例

def as_item(x):
    return x

def is_in_stock(x):
    return True

class Item:
    def __init__(self, id, weight, type):
        self.id = id
        self.weight = weight
        self.type = type

class CatalogItem:
    def __init__(self, id, barcode):
        self.id = id
        self.barcode = barcode

def retrieve_catalog_item(id):
    return CatalogItem(id, "123456789")

def default_transformer(item):
    return item

def transform(item):
    if item.type == "cake":
        catalog_item = retrieve_catalog_item(item.id)
        return {
            'id': item.id,
            'weight': item.weight,
            'barcode': catalog_item.barcode,
        }
    else:
        return default_transformer(item)

low_weight_items = (
    z
    for x in [
        Item(1, 100, "cake"),
        Item(2, 1000, "cake"),
        Item(3, 900, "cake"),
        Item(4, 10000, "cake"),
        Item(5, 100, "bread"),
    ]
    if (y := as_item(x))
    if is_in_stock(y)
    if y.weight < 1000
    if (z := transform(y))
)

for item in low_weight_items:
    print(item)
7

自己定义一个函数组合的元函数其实很简单。一旦你有了这个,连接多个函数在一起也会变得很简单。

import functools
def compose(*functions):
    return functools.reduce(lambda f, g: lambda x: f(g(x)), functions)
def make_filter(filter_fn):
    return lambda iterable: (it for it in iterable if filter_fn(it))

pipeline = compose(as_item, make_filter(is_in_stock),
                   make_filter(lambda item: item.weight < 1000),
                   lambda item: ({'id': item.id,
                                 'weight': item.weight,
                                 'barcode': item.barcode} if item.type == "cake"
                                 else default_transformer(item)))
pipeline(shop.inventory)

如果你还不太了解迭代器,我建议你先学习一下(像这个链接 http://excess.org/article/2013/02/itergen1/ 就不错)。

10

使用迭代器(在Python 3中,所有这些函数都是迭代器;而在Python 2中,你需要使用itertools.imap和itertools.ifilter)。

m = itertools.imap
f = itertools.ifilter
def final_map_fn(item):
   if (item.type == "cake"):
        catalog_item = retrieve_catalog_item(item.id);
        return {
            "id": item.id,
            "weight": item.weight,
            "barcode": catalog_item.barcode}
    else:
        return default_transformer(item)

items = m(as_item,shop.inventory) #note it does not loop it yet
instockitems = f(is_in_stock,items) #still hasnt actually looped anything
weighteditems = (item for item instockitems if item.weight < 100) #still no loop (this is a generator)
final_items = m(final_map_fn,weighteditems) #still has not looped over a single item in the list
results = list(final_items) #evaluated now with a single loop
55

如果你不介意使用一个软件包,这里有另一种方法可以做到这一点,使用的链接是 https://github.com/EntilZha/PyFunctional

from functional import seq

def as_item(x):
    # Implementation here
    return x

def is_in_stock(x):
    # Implementation
    return True

def transform(item):
    if item.type == "cake":
        catalog_item = retrieve_catalog_item(item.id);
        return {
            'id': item.id,
            'weight': item.weight,
            'barcode': catalog_item.barcode
        }
    else:
        return default_transformer(item)

low_weight_items = seq(inventory)\
    .map(as_item)\
    .filter(is_in_stock)\
    .filter(lambda item: item.weight < 1000)\
    .map(transform)

之前提到过,Python 允许你使用 lambda 表达式,但它们没有 JavaScript 中的闭包那么灵活,因为它们不能包含多个语句。另外,Python 还有一个让人烦恼的地方就是需要使用反斜杠。话虽如此,我觉得上面的内容最接近你最初发布的内容。

免责声明:我是上面提到的软件包的作者

18

一种很好的做法是把多个过滤器和映射结合成一个生成器表达式。如果不能这样做,就定义一个中间变量来处理你需要的中间映射或过滤,而不是强行把所有映射放在一起。例如:

def is_in_stock(x):
   # ...
def as_item(x):
   # ...
def transform(item):
    if item.type == "cake":
        catalog_item = retrieve_catalog_item(item.id)
        return {
            "id": item.id,
            "weight": item.weight,
            "barcode": catalog_item.barcode
        }
    else:
        return default_transformer(item)

items = (as_item(item) for item in shop.inventory)
low_weight_items = (transform(item) for item in items if is_in_stock(item) and item.weight < 1000)

注意,实际应用这些映射和过滤的操作都是在最后两行完成的。前面的部分只是定义了实现映射和过滤的函数。

第二个生成器表达式把最后两个过滤和映射都放在了一起。使用生成器表达式的好处是,inventory中的每个原始项目都会被懒惰地映射和过滤。也就是说,它不会一次性处理整个列表,所以如果列表很大,性能可能会更好。

需要注意的是,Python没有像你在JavaScript示例中那样可以直接定义长函数的方式。你不能在代码中直接写复杂的过滤条件(比如item.type == "cake")。相反,正如我在示例中所展示的,你必须把它定义为一个单独的函数,就像你之前定义的is_in_stockas_item一样。

(第一个映射被拆分的原因是,后面的过滤在映射数据之前无法作用于这些数据。虽然可以把它们合并成一个,但那样就需要在表达式内部手动重新做as_item的映射:

low_weight_items = (transform(as_item(item)) for item in items if is_in_stock(as_item(item)) and as_item(item).weight < 1000)

分开这个映射会更清晰。)

撰写回答