如何编写一个函数fmap来返回输入的同一类型的iterable?

2024-05-14 04:30:46 发布

您现在位置:Python中文网/ 问答频道 /正文

如何编写具有以下属性的函数“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和通用?公司名称:

^{pr2}$

Tags: lambda函数名称属性haskell公司解决方案python3
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子句中为所有其他iterable值添加一个case:

^{pr2}$

但这将为set的子类返回iterable,这可能不是您想要的。在

请注意,我故意使用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))

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

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

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

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

^{pr2}$

herethe docs中所述:

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

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

测试iterable

您可以用几种方法检查/测试iterable。使用try/except捕获{}:

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

或使用^{}

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等,可以通过lazy-iterable来实例化实例。存在异常:例如,str(map(str.upper, 'hello'))将无法像您预期的那样工作,即使str实例是可编辑的。在

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

首先,让我们来讨论Haskell的fmap来理解它为什么会这样做,尽管我假设您对Haskell考虑这个问题相当熟悉。fmap是在{a1}中定义的泛型方法:

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

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

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

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

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

^{pr2}$

这是一个演示

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}

相关问题 更多 >