对集合执行某些操作,然后返回与集合相同的类型



我想将f函数应用于集合xs但保留其类型。如果我使用map,我会得到一个"地图对象":

def apply1(xs, f):
return map(f, xs)

如果我知道xs是类似于listtuple我可以强制它具有相同的类型:

def apply2(xs, f):
return type(xs)(map(f, xs))

然而,对于namedtuple来说,这很快就会崩溃(我目前正在使用它)——因为据我所知,namedtuple需要用解包语法或通过调用其_make函数来构造。另外,namedtuple是常量,所以我不能遍历所有条目并更改它们。

使用dict会产生进一步的问题。

有没有一种通用的方式来表达这样一个适用于所有可迭代事物的apply函数?

对于装饰器来说,看起来functools.singledispatch完美的任务:

from functools import singledispatch

@singledispatch
def apply(xs, f):
return map(f, xs)

@apply.register(list)
def apply_to_list(xs, f):
return type(xs)(map(f, xs))

@apply.register(tuple)
def apply_to_tuple(xs, f):
try:
# handle `namedtuple` case
constructor = xs._make
except AttributeError:
constructor = type(xs)
return constructor(map(f, xs))

之后apply函数可以简单地使用

>>> apply([1, 2], lambda x: x + 1)
[2, 3]
>>> from collections import namedtuple
>>> Point = namedtuple('Point', ['x', 'y'])
>>> p = Point(10, 5)
>>> apply(p, lambda x: x ** 2)
Point(x=100, y=25)

我不知道dict对象所需的行为是什么,但这种方法的伟大之处在于它很容易扩展。

我有一种预感,你来自Haskell——对吗?(我猜是因为您使用fxs作为变量名称。在 Haskell 中,您的问题的答案是"是的,它被称为fmap,但它仅适用于具有已定义函子实例的类型。

另一方面,Python没有"Functor"的一般概念。所以严格来说,答案是否定的。要获得这样的东西,你必须依靠Python提供的其他抽象。

ABC救援

一种非常通用的方法是使用抽象基类。这些提供了一种结构化的方式来指定和检查特定接口。Functor 类型类的 Pythonic 版本将是一个抽象基类,它定义了一个特殊的fmap方法,允许各个类指定它们的映射方式。但是不存在这样的事情。(我认为这将是对Python的一个非常酷的补充!

现在,您可以定义自己的抽象基类,因此您可以创建一个需要fmap接口的 Functor ABC,但您仍然必须编写所有自己的listdict等子类,所以这不是很理想。

更好的方法是使用现有接口将映射的通用定义拼凑在一起,这似乎是合理的。您必须非常仔细地考虑需要组合现有接口的哪些方面。仅仅检查类型是否定义了__iter__是不够的,因为正如您已经看到的,类型的迭代定义并不一定转化为构造的定义。例如,循环访问字典只会为您提供键,但以这种精确方式映射字典需要迭代项目

具体例子

这是一个抽象基方法,包括namedtuple和三个抽象基类的特殊情况 -SequenceMappingSet。对于以预期方式定义上述任何接口的任何类型,它将按预期运行。然后,它回退到可迭代对象的泛型行为。在后一种情况下,输出不会与输入具有相同的类型,但至少它会起作用。

from abc import ABC
from collections.abc import Sequence, Mapping, Set, Iterator
class Mappable(ABC):
def map(self, f):
if hasattr(self, '_make'):
return type(self)._make(f(x) for x in self)
elif isinstance(self, Sequence) or isinstance(self, Set):
return type(self)(f(x) for x in self)
elif isinstance(self, Mapping):
return type(self)((k, f(v)) for k, v in self.items())
else:
return map(f, self)

我将其定义为 ABC,因为这样您就可以创建从它继承的新类。但是您也可以在任何类的现有实例上调用它,它将按预期运行。您也可以将上面的map方法用作独立函数。

>>> from collections import namedtuple
>>> 
>>> def double(x):
...     return x * 2
... 
>>> Point = namedtuple('Point', ['x', 'y'])
>>> p = Point(5, 10)
>>> Mappable.map(p, double)
Point(x=10, y=20)
>>> d = {'a': 5, 'b': 10}
>>> Mappable.map(d, double)
{'a': 10, 'b': 20}

定义ABC的酷之处在于,您可以将其用作"混合"。下面是从名为元组的Point派生MappablePoint

>>> class MappablePoint(Point, Mappable):
...     pass
... 
>>> p = MappablePoint(5, 10)
>>> p.map(double)
MappablePoint(x=10, y=20)

您也可以根据 Azat Ibrakov 的回答,使用functools.singledispatch装饰器稍微修改此方法。(这对我来说是新的 - 他应该为这部分答案获得所有荣誉,但我想为了完整起见,我会把它写出来。

这看起来像下面这样。请注意,我们仍然必须特殊情况namedtuples,因为它们会破坏元组构造函数接口。这以前没有困扰过我,但现在感觉这是一个非常烦人的设计缺陷。此外,我进行了设置,以便最终的fmap函数使用预期的参数顺序。(我想使用mmap而不是fmap因为"Mappable"是一个比"Functor"IMO更Pythonic的名字。但是mmap已经是一个内置库了!。

import functools
@functools.singledispatch
def _fmap(obj, f):
raise TypeError('obj is not mappable')
@_fmap.register(Sequence)
def _fmap_sequence(obj, f):
if isinstance(obj, str):
return ''.join(map(f, obj))
if hasattr(obj, '_make'):
return type(obj)._make(map(f, obj))
else:
return type(obj)(map(f, obj))
@_fmap.register(Set)
def _fmap_set(obj, f):
return type(obj)(map(f, obj))
@_fmap.register(Mapping)
def _fmap_mapping(obj, f):
return type(obj)((k, f(v)) for k, v in obj.items())
def fmap(f, obj):
return _fmap(obj, f)

一些测试:

>>> fmap(double, [1, 2, 3])
[2, 4, 6]
>>> fmap(double, {1, 2, 3})
{2, 4, 6}
>>> fmap(double, {'a': 1, 'b': 2, 'c': 3})
{'a': 2, 'b': 4, 'c': 6}
>>> fmap(double, 'double')
'ddoouubbllee'
>>> Point = namedtuple('Point', ['x', 'y', 'z'])
>>> fmap(double, Point(x=1, y=2, z=3))
Point(x=2, y=4, z=6)

关于中断接口的最后一点说明

这两种方法都不能保证这适用于所有被识别为Sequence的东西,依此类推,因为 ABC 机制不检查函数签名。这不仅是构造函数的问题,也是所有其他方法的问题。如果没有类型注释,这是不可避免的。

然而,在实践中,这可能并不重要。如果您发现自己使用的工具以奇怪的方式打破了接口约定,请考虑使用其他工具。(我实际上会说这也适用于namedtuple,就像我喜欢它们一样!这是许多Python设计决策背后的"同意的成年人"哲学,在过去的几十年里,它一直运行良好。

最新更新