Python将映射和过滤器链接起来的方法是什么?



我目前正在学习Python(来自其他语言,如JavaScript和Ruby)。我非常习惯链接一堆转换/过滤器,但我很确定这不是在Python中这样做的正确方式:filter在可枚举对象之前接受lambda,因此编写长/多行函数看起来非常奇怪,链接它们意味着将它们按相反顺序排列,这是不可读的。

在这个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);
        }
    });

我知道我可能会对第一个映射和接下来的两个过滤器使用列表推导式,但我不确定如何执行最后一个映射以及如何将所有内容放在一起。

谢谢!

如果您不介意使用包,这是另一种方法,使用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中的clojure那样灵活,因为它们不能有多个语句。另一个令人讨厌的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_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)

直接把地图分开会更清楚)

使用迭代器(在python3中所有的函数都是迭代器,在python2中你需要使用itertools。Imap和itertools. filter)

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

定义自己的功能组合元函数非常容易。一旦你有了这些,将函数链接在一起也很容易。

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/这样的东西很好)。

我们可以使用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)。

您可以使用生成器推导式中的walrus操作符来实现这一点。

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),因为您不能使用walrus操作符分配给现有变量。


完整的示例

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

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的类,它扩展了class list。我将用my_list包装我的列表,然后通过传递一个函数作为参数,使用map, filter和reduce从我的类中定义。

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

您也可以像这样创建自己的类。你可以向这个流类传递一个可迭代项,并创建方法,将所有需要的操作委托给现有的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))

相关内容

最新更新