关于python:如何编写一个函数fmap,它返回输入的相同类型的iterable?

How can I write a function fmap that returns the same type of iterable that was inputted?

如何编写具有以下属性的函数"fmap":

1
2
3
4
5
6
>>> l = [1, 2]; fmap(lambda x: 2*x, l)
[2, 4]
>>> l = (1, 2); fmap(lambda x: 2*x, l)
(2, 4)
>>> l = {1, 2}; fmap(lambda x: 2*x, l)
{2, 4}

(我在haskell中搜索一种"fmap",在python3中搜索)。

我有一个非常难看的解决方案,但肯定有一个更像Python和普通的解决方案?:

1
2
3
4
def fmap(f, container):
    t = container.__class__.__name__
    g = map(f, container)
    return eval(f"{t}(g)")


直接实例化,而不是通过eval实例化

__class__也可用于实例化新实例:

1
2
3
def mymap(f, contener):
    t = contener.__class__
    return t(map(f, contener))

这就消除了对eval的需求,因为使用eval被认为是不好的做法。根据@elikorigo的评论,您可能更喜欢内置的type而不是神奇的方法:

1
2
3
def mymap(f, contener):
    t = type(contener)
    return t(map(f, contener))

如本文和文档中所述:

The return value is a type object and generally the same object as returned by object.__class__.

对于新样式的类,"一般相同"应被视为"等效"。

测试iterable

您可以通过多种方式检查/测试iterable。要么用try/except抓捕TypeError

1
2
3
4
5
6
def mymap(f, contener):
    try:
        mapper = map(f, contener)
    except TypeError:
        return 'Input object is not iterable'
    return type(contener)(mapper)

或使用collections.Iterable

1
2
3
4
5
6
from collections import Iterable

def mymap(f, contener):
    if isinstance(contener, Iterable):
        return type(contener)(map(f, contener))
    return 'Input object is not iterable'

这是因为通常用作容器的内置类(如listsettuplecollections.deque等)可以通过懒惰的iterable来实例化实例。例外情况存在:例如,即使str实例是不可维护的,str(map(str.upper, 'hello'))也不会像您预期的那样工作。


在任何情况下,使用输入类型作为转换器都不一定有效。map只是利用其输入的"Iterability"来产生其输出。在python3中,这就是为什么map返回生成器而不是列表(这更合适)。

因此,更干净、更健壮的版本应该是这样一个版本,它明确地期望它可以处理各种可能的输入,并且在所有其他情况下都会引发错误:

1
2
3
4
5
6
7
8
9
10
11
12
def class_retaining_map(fun, iterable):
  if type(iterable) is list:  # not using isinstance(), see below for reasoning
    return [ fun(x) for x in iterable ]
  elif type(iterable) is set:
    return { fun(x) for x in iterable }
  elif type(iterable) is dict:
    return { k: fun(v) for k, v in iterable.items() }
  # ^^^ use .iteritems() in python2!
  # and depending on your usecase this might be more fitting:
  # return { fun(k): v for k, v in iterable.items() }
  else:
    raise TypeError("type %r not supported" % type(iterable))

您可以在"原因"的else子句中为所有其他不可重复的值添加一个事例:

1
2
  else:
    return (fun(x) for x in iterable)

但这将返回一个iterable,用于set的子类,这可能不是您想要的。

请注意,我故意不使用isinstance,因为这样会从list的子类中列出一个列表。我认为在这种情况下,这显然是不需要的。

有人可能会说,任何属于list的东西(即list的子类)都需要遵守一个构造函数,该构造函数为元素的迭代返回这种类型的东西。同样,对于setdict的子类(必须用于成对迭代)等,代码可能如下所示:

1
2
3
4
5
6
7
8
9
10
def class_retaining_map(fun, iterable):
  if isinstance(iterable, (list, set)):
    return type(iterable)(fun(x) for x in iterable)
  elif isinstance(iterable, dict):
    return type(iterable)((k, fun(v)) for k, v in iterable.items())
  # ^^^ use .iteritems() in python2!
  # and depending on your usecase this might be more fitting:
  # return type(iterable)((fun(k), v) for k, v in iterable.items())
  else:
    raise TypeError("type %r not supported" % type(iterable))


I search a kind of"fmap" in haskell, but in python3

首先,让我们讨论一下哈斯克尔的fmap来理解,为什么它的行为方式是这样的,尽管我假设你对哈斯克尔的问题相当熟悉。fmap是在Functor类型类中定义的通用方法:

1
2
3
class Functor f where
    fmap :: (a -> b) -> f a -> f b
    ...

函子服从几个重要的数学规律,并且有从fmap导出的几种方法,尽管后者对于极小的完全函子实例是足够的。换句话说,在属于Functor类型类的haskell类型中,实现自己的fmap函数(此外,haskell类型可以通过newtype定义有多个Functor实现)。在Python中,我们没有类型类,尽管我们有一些类,虽然在这种情况下不太方便,但它们允许我们模拟这种行为。不幸的是,对于类,我们不能在没有子类化的情况下向已经定义的类添加功能,这限制了我们为所有内置类型实现通用fmap的能力,尽管我们可以通过在fmap实现中显式检查可接受的不可重复类型来克服它。使用Python的类型系统来表示更高级的类类型也是不可能的,但是我离题了。

总之,我们有几个选择:

  • 支持所有Iterable类型(@jpp的解决方案)。它依赖于构造函数将Python的map返回的迭代器转换回原始类型。这是对容器内的值应用函数的职责,它将从容器中移除。这种方法与functor接口大不相同:函数应该自己处理映射,并处理对重构容器至关重要的额外元数据。
  • 支持易于映射的内置ITerable类型(即不携带任何重要元数据的内置类型)的子集。这个解决方案是由@alfe实现的,虽然不太通用,但更安全。
  • 采用解决方案2并添加对正确的用户定义函数的支持。
  • 这是我对第三个解决方案的看法

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    import abc
    from typing import Generic, TypeVar, Callable, Union, \
        Dict, List, Tuple, Set, Text

    A = TypeVar('A')
    B = TypeVar('B')


    class Functor(Generic[A], metaclass=abc.ABCMeta):

        @abc.abstractmethod
        def fmap(self, f: Callable[[A], B]) -> 'Functor[B]':
            raise NotImplemented


    FMappable = Union[Functor, List, Tuple, Set, Dict, Text]


    def fmap(f: Callable[[A], B], fmappable: FMappable) -> FMappable:
        if isinstance(fmappable, Functor):
            return fmappable.fmap(f)
        if isinstance(fmappable, (List, Tuple, Set, Text)):
            return type(fmappable)(map(f, fmappable))
        if isinstance(fmappable, Dict):
            return type(fmappable)(
                (key, f(value)) for key, value in fmappable.items()
            )
        raise TypeError('argument fmappable is not an instance of FMappable')

    这是一个演示

    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    In [20]: import pandas as pd                                                                        

    In [21]: class FSeries(pd.Series, Functor):
        ...:      
        ...:     def fmap(self, f):
        ...:         return self.apply(f).astype(self.dtype)
        ...:                                                                                            

    In [22]: fmap(lambda x: x * 2, [1, 2, 3])                                                          
    Out[22]: [2, 4, 6]

    In [23]: fmap(lambda x: x * 2, {'one': 1, 'two': 2, 'three': 3})                                    
    Out[23]: {'one': 2, 'two': 4, 'three': 6}

    In [24]: fmap(lambda x: x * 2, FSeries([1, 2, 3], index=['one', 'two', 'three']))  
    Out[24]:
    one      2
    two      4
    three    6
    dtype: int64

    In [25]: fmap(lambda x: x * 2, pd.Series([1, 2, 3], index=['one', 'two', 'three']))                
    ---------------------------------------------------------------------------
    TypeError                                 Traceback (most recent call last)
    <ipython-input-27-1c4524f8e4b1> in <module>
    ----> 1 fmap(lambda x: x * 2, pd.Series([1, 2, 3], index=['one', 'two', 'three']))

    <ipython-input-7-53b2d5fda1bf> in fmap(f, fmappable)
         34     if isinstance(fmappable, Functor):
         35         return fmappable.fmap(f)
    ---> 36     raise TypeError('argument fmappable is not an instance of FMappable')
         37
         38

    TypeError: argument fmappable is not an instance of FMappable

    此解决方案允许我们通过子类化为同一类型定义多个函数:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    In [26]: class FDict(dict, Functor):
       ...:    
       ...:     def fmap(self, f):
       ...:         return {f(key): value for key, value in self.items()}
       ...:
       ...:

    In [27]: fmap(lambda x: x * 2, FDict({'one': 1, 'two': 2, 'three': 3}))    
    Out[27]: {'oneone': 1, 'twotwo': 2, 'threethree': 3}