在Python中如何链式使用map和filter?
我现在正在学习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 个回答
你可以通过在生成器表达式中使用海象运算符来实现这个功能。
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)
自己定义一个函数组合的元函数其实很简单。一旦你有了这个,连接多个函数在一起也会变得很简单。
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/ 就不错)。
使用迭代器(在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
如果你不介意使用一个软件包,这里有另一种方法可以做到这一点,使用的链接是 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 还有一个让人烦恼的地方就是需要使用反斜杠。话虽如此,我觉得上面的内容最接近你最初发布的内容。
免责声明:我是上面提到的软件包的作者
一种很好的做法是把多个过滤器和映射结合成一个生成器表达式。如果不能这样做,就定义一个中间变量来处理你需要的中间映射或过滤,而不是强行把所有映射放在一起。例如:
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_stock
和as_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)
分开这个映射会更清晰。)