对集合进行处理,然后返回与集合相同的类型

时间:2018-12-20 18:55:00

标签: python python-3.x type-conversion namedtuple

我想将函数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需要使用解包语法或通过调用其{{ 1}}函数。另外,_make是const,因此我无法遍历所有条目并仅对其进行更改。

使用namedtuple会引起更多问题。

是否存在一种通用的方式来表达这种dict函数,该函数适用于所有可迭代的事物?

2 个答案:

答案 0 :(得分:2)

我预感您来自Haskell,对吗? (我猜是因为您使用fxs作为变量名。)在Haskell中,您问题的答案是“是的,它称为fmap,但仅适用于类型具有已定义的Functor实例。”

另一方面,Python没有“ Functor”的一般概念。因此严格来说,答案是否定的。为了获得类似的东西,您必须依靠Python确实提供的其他抽象。

救援的ABC

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

现在,您可以定义自己的抽象基类,因此可以创建需要fmap接口的Functor ABC,但仍然需要编写自己的list的所有仿函数子类。 ,dict等,因此并不是很理想。

一种更好的方法是使用现有接口将看起来合理的通用映射拼凑在一起。您必须仔细考虑一下需要结合现有接口的哪些方面。仅检查类型是否定义__iter__是不够的,因为您已经知道,类型的迭代定义不一定会转换为构造定义。例如,对字典进行迭代只会给您键,但是以这种精确的方式映射字典将需要对 items 进行迭代。

具体示例

这是一个抽象基方法,其中包含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的很酷的事情是您可以将其用作“混合”。这是一个源自MappablePoint命名元组的Point

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

您还可以使用functools.singledispatch装饰器,根据Azat Ibrakov's answer稍微修改此方法。 (对我来说这是新来的-他应该得到这部分答案的全部荣誉,但我认为为完整起见,我会写出来。)

如下所示。注意,我们 still 必须使用特殊情况的namedtuple,因为它们破坏了元组的构造函数接口。以前没有打扰过我,但是现在感觉就像是一个非常烦人的设计缺陷。另外,我进行了设置,以使最终的fmap函数使用预期的参数顺序。 (我想使用mmap代替fmap,因为“ Mappable”比IMO的“ Functor”更像Python一样。但是mmap已经是内置库了!达恩。)< / p>

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设计决策背后的“ consenting adults”哲学,并且对于最近几十年。

答案 1 :(得分:2)

对于functools.singledispatch decorator来说,这似乎是一项完美的任务:

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对象需要什么行为,但是这种方法的优点是易于扩展。