Python中如何链式调用map和filter函数?

56

我目前正在学习 Python(之前使用过像 JavaScript 和 Ruby 这样的其他语言)。我非常习惯链式调用一些转换/过滤操作,但我相信在 Python 中这不是正确的方式:filter 在可枚举对象之前需要一个 lambda 函数,因此编写长/多行函数看起来非常奇怪,并且将它们链接在一起意味着将它们以相反的顺序放置,这不易于阅读。

在 Python 中,如何编写此 JavaScript 函数中的映射和过滤的“Python方式”?

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);
        }
    });

我理解可以在第一个map和接下来的两个filter中使用列表推导式,但是不确定如何完成最后一个map以及如何将所有内容组合在一起。

谢谢!


1
你可以为最后一个 map 编写一个命名函数,并对其他内容使用列表推导式。虽然我个人更喜欢 map/filter/reduce,但大多数 Python 程序员似乎更喜欢列表推导式。你应该尝试使用列表推导式逐步完成一两个部分,以便熟悉它们的用法。 - BlackVegetable
9个回答

51

如果您不介意使用软件包,这是使用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 的另一个令人烦恼的问题是需要使用反斜杠。话虽如此,我认为上述内容最接近您最初发布的内容。

免责声明:我是上述软件包的作者。


10
注意:如果您将 seq 对象及其调用链包含在括号中,则可以删除反斜杠。有关示例,请参阅 GitHub 自述文件。---(我知道您编写了这个软件包,但我是为了让其他人看到而写的) - byxor
6
终于有些有用且易读的东西了!这就是该做事情的方式。 - MEMark

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)

最好将那张地图单独分开来,这样更加清晰明了。


2
+1 for style,这听起来很易读且符合Python的风格。虽然其他解决方案似乎也在运行,但你的代码在Python中看起来自然而易读。 - Jan Vlcinsky
1
我也认为这是漂亮的代码... (+1) - Joran Beasley
1
非常感谢!我一直在想如何定义那个复杂的过滤器。现在非常清楚了! - Edward
@BrenBarn Python中的mapgenerator效率相同吗?计时结果为:map(lambda x*x, range(100)) < (x*x for x in range(100)) - John Strood

10

使用迭代器(在python 3中,所有这些函数都是迭代器,在python2中,您需要使用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

1
+1 非常好地解释了迭代器的美妙之处(以及所有相关的类型)。是的 - Python 可以像管道一样,能够对长迭代对象进行复杂转换。 - Jan Vlcinsky
2
这个代码可以运行,但是我认为列表/生成器推导式通常比map/filter应用更易读,特别是当map-filter需要将表达式lambda化时,而这些表达式在推导式中可以直接编写(例如你的weightitems过滤器)。 - BrenBarn
@BrenBarn修改使用生成器作为lambda,因为您是正确的,这看起来更好(特别是对于那个)。 - Joran Beasley
考虑到函数式编程优先考虑消除赋值,我必须说,这个解决方案在任何接近函数规范的意义上都完全缺乏优雅。我不认为这与FP的精神有任何关系。这让我真的怀疑Python是否真的适合FP。 - PlexQ

6
定义自己的函数组合元函数非常容易。一旦你拥有了它,将函数链接在一起也非常容易。
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/)。

2
这是一个解决方案,但我绝对不会称其为Pythonic解决方案。 - BrenBarn
3
Python是一种函数式语言——因为所有其他解决方案都是过程式而不是函数式的,所以在我看来,这是页面上最符合Python风格的解决方案。其他解决方案都没有充分利用Python进行元编程的能力。 - metaperture
3
不是使用Python的所有特性就意味着它最符合Python风格。如果想称某个东西为Pythonic,请阅读Python之禅($ python -c "import this")。 - Jan Vlcinsky
3
Python具有函数式编程能力,但为了使用它们而费尽心思并不符合Pythonic的风格。一些任务在Python中使用函数式技巧可以优雅地解决;而其他任务则不然。这并不是函数式与其他范例的问题;对于这个任务来说,这种方法比基于迭代器的解决方案要不可读得多。 - BrenBarn
1
make_filter函数可以使用functools.partial简化,具体做法如下: def filterBy(filter_fn): return functools.partial(filter, filter_fn)同样地,我们也可以对内置函数map进行partialize操作,从而创建一个mapBy函数。 - Gibezynu Nu
显示剩余3条评论

1

你可以在生成器推导式中使用海象运算符来实现这一点。

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)

1
我们可以使用 Pyterator: https://github.com/remykarem/pyterator(免责声明:我是作者)。它与 @EntilZha 的库非常相似。
pip install git+https://github.com/remykarem/pyterator#egg=pyterator

定义函数

def is_in_stock(x):
    pass

def as_item(x):
    pass

def transform_cake_noncake(item):
    pass

那么

from pyterator import iterate

low_weight_items = (
    iterate(shop.inventory)
    .map(as_item)
    .filter(is_in_stock)
    .filter(lambda item: item.weight < 1000)
    .map(transform_cake_noncake)
    .to_list()
)

请注意,所有类似于mapfilter的操作都是惰性求值的。因此,您需要调用.to_list()来评估管道。否则,它将保持为一个Iterator(您稍后可以在for循环等中使用)。
另外,与Fluentpy(https://github.com/dwt/fluent)类似。

0
def is_in_stock():
    ...

def as_item():
    ...

def get_low_weight_items(items):
    for item in items:
        item = as_item(item)
        if not is_in_stock(item):
            continue
        if item.weight < 1000:
            if item.type == "cake":
                catalog_item = retrieve_catalog_item(item.id)
                yield {
                    "id": item.id,
                    "weight": item.weight,
                    "barcode": catalog_item.barcode,
                }
            else:
                yield default_transformer(item)


low_weight_items = list(get_low_weight_items(items))

0

from functools import reduce

class my_list(list):
    def filter(self, func):
        return my_list(filter(func, self))
    def map(self, func):
        return my_list(map(func, self))
    def reduce(self, func):
        return reduce(func, self)

temp = my_list([1,2,3,4,5,6,7])
print(temp.filter(lambda n: n%2==0).map(lambda n: n*2).reduce(lambda a,b: a+b))

如果你想使用内置的filter、map和reduce方法,你可以在Python中使用继承来实现同样的功能。

这里我创建了一个名为my_list的类,它继承了list类。我将用my_list包装我的列表,然后通过传递函数作为参数,使用从我的类定义的map、filter和reduce。

我知道每次调用这三个方法都会创建一个新的对象。如果有任何方法可以避免多个对象的创建,请告诉我。


0
你也可以创建自己的类,就像这样。 你将可迭代项传递给此流类,并创建方法,将所有必需的操作委托给现有的 map、filter 函数等。
class stream:
    def __init__(self, iterable):
        try:
            self.iterator = iter(iterable)
        except Exception:
            raise TypeError(f'{iterable} is not iterable but {type(iterable)}')

    def map(self, f):
        self.iterator = map(f, self.iterator)
        return self

    def filter(self, f):
        self.iterator = filter(f, self.iterator)
        return self

    def foreach(self, f):
        for x in self.iterator:
            f(x)

if __name__ == '__main__':
    stream([1,2,3,4,5]).map(lambda x: x*2)\
                       .filter(lambda x:x>4)\
                       .foreach(lambda x: print(x))

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接