【问题标题】:How can I write a function fmap that returns the same type of iterable that was inputted?如何编写一个函数 fmap 来返回与输入的相同类型的迭代?
【发布时间】:2019-01-02 10:09:57
【问题描述】:

我怎样才能用这个属性写一个函数“fmap”:

>>> 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 和通用的解决方案? :

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

【问题讨论】:

  • 在以下情况下中断:fmap(lambda x: x[0], {"A":"small","Example":"that","Does":"not","Work":"!"}) .. 以及函数更改类型的任何其他情况...

标签: python python-3.x functional-programming


【解决方案1】:

我在haskell中搜索了一种“fmap”,但是在python3中

首先,让我们讨论一下 Haskell 的 fmap,以了解它为什么会这样,尽管我假设您在考虑这个问题时对 Haskell 相当熟悉。 fmapFunctor type-class中定义的泛型方法:

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

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

总而言之,我们有几种选择:

  1. 支持所有Iterable 类型(@jpp 的解决方案)。它依靠构造函数将 Python 的 map 返回的迭代器转换回原始类型。那就是对容器内的值应用函数的职责被从容器中移除。这种方法与仿函数接口有很大不同:仿函数应该自己处理映射并处理对重构容器至关重要的其他元数据。
  2. 支持易于映射的内置可迭代类型的子集(即不携带任何重要元数据的内置)。此解决方案由 @Alfe 实现,虽然不太通用,但更安全。
  3. 采用解决方案 #2 并添加对适当的用户定义函子的支持。

这是我对第三种解决方案的看法

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

这是一个演示

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

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

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}

【讨论】:

    【解决方案2】:

    直接实例化而不是通过eval

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

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

    这消除了对eval 的需要,使用它被认为是poor practice。根据@EliKorvigo 的评论,您可能更喜欢内置type 而不是魔术方法:

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

    herethe docs 所述:

    返回值是一个类型对象,一般与object.__class__返回的对象相同。

    在新式类的情况下,“大致相同”应被视为“等效”。

    测试可迭代对象

    您可以通过多种方式检查/测试可迭代对象。要么使用try / except 捕捉TypeError

    def mymap(f, contener):
        try:
            mapper = map(f, contener)
        except TypeError:
            return 'Input object is not iterable'
        return type(contener)(mapper)
    

    或者使用collections.Iterable:

    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 等,可用于通过惰性可迭代。存在例外情况:例如,str(map(str.upper, 'hello')) 不会像您预期的那样工作,即使 str 实例是可迭代的。

    【讨论】:

    • 我想说,调用type(container)(map(...)) 比访问魔法属性要干净一些。
    • 耶普有效。在它周围放置一个 try:except: 是否明智,以防函数将 iterabletype 转换为不起作用的东西?或者如果您使用类型更改功能调用它,让它崩溃会更好吗? mymap(lambda x: x[0], {"A":"small","Example":"that","Does":"not","Work":"!"})
    • @PatrickArtner,公平点,我添加了几种可以做到这一点的方法。
    • 您的try/except 代码捕获了太多方式的情况。在 map 调用中执行 f 时抛出的每个 TypeError 都将被捕获并被误解为“输入对象不可迭代”。
    • @Alfe,你能举个例子吗?我已经解决了存在输出不符合您期望的异常的问题,例如str。还是您只是对错误消息本身提出异议?
    【解决方案3】:

    使用输入的类型作为转换器不一定适用于所有场合。 map 只是使用其输入的“可迭代性”来产生其输出。这就是在 Python3 中 map 返回生成器而不是列表的原因(这更合适)。

    因此,一个更简洁、更健壮的版本将明确期望它可以处理的各种可能的输入,并且在所有其他情况下都会引发错误:

    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 原因子句中为所有其他可迭代值添加一个案例:

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

    但那将是 e。 G。返回set 的子类的可迭代对象,这可能不是您想要的。

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

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

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

    【讨论】:

    • 您是否故意使用type(iterable) == list 而不是isinstance?我能想到的情况很少,这不是一个坏习惯。即使这样,你也会使用is 而不是==
    • @EliKorvigo 是的,我故意使用type(iterable)(请参阅我的最后一段,由于这个原因,我在后面添加了一段)。我将 == 的使用更改为 is 的使用,在这种情况下稍微好一点,你是对的。
    • 我会采用这个方案,另外一个案例:elif type(iterable) is str: return "".join(map(fun, iterable))
    • 如果您需要字符串,您可能需要考虑在 Python3 中添加 bytes 或在 Python2 中添加 unicode
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2022-01-03
    • 1970-01-01
    • 1970-01-01
    • 2018-02-23
    • 2021-12-07
    • 1970-01-01
    • 2011-04-23
    相关资源
    最近更新 更多