<p>我有预感你是哈斯克尔人,对吗?(我猜是因为您使用<code>f</code>和<code>xs</code>作为变量名。)在Haskell中,您的问题的答案是“是的,它被称为<code>fmap</code>,但它只适用于定义了<a href="http://hackage.haskell.org/package/base-4.12.0.0/docs/Data-Functor.html" rel="nofollow noreferrer">Functor</a>实例的类型。”</p>
<p>另一方面,Python没有“Functor”的一般概念,所以严格地说,答案是否定的。要得到这样的东西,就必须依赖Python提供的其他抽象。你知道吗</p>
<h3>ABCs救援</h3>
<p>一种相当普遍的方法是使用<a href="https://docs.python.org/3/library/collections.abc.html#module-collections.abc" rel="nofollow noreferrer">abstract base classes</a>。它们提供了一种结构化的方法来指定和检查特定的接口。Functor typeclass的Pythonic版本是一个抽象基类,它定义了一个特殊的<code>fmap</code>方法,允许单个类指定如何映射它们。但是没有这样的东西存在。(不过,我认为这将是对Python的一个非常酷的补充!)你知道吗</p>
<p>现在,您可以定义自己的抽象基类,这样就可以创建一个需要<code>fmap</code>接口的函子ABC,但是您仍然需要编写自己的<code>list</code>、<code>dict</code>等的所有函数化子类,所以这并不是很理想。你知道吗</p>
<p>更好的方法是使用现有接口拼凑出一个似乎合理的通用映射定义。您必须非常仔细地考虑需要组合现有接口的哪些方面。仅仅检查一个类型是否定义了<code>__iter__</code>是不够的,因为正如您已经看到的,类型的迭代定义不一定转化为构造定义。例如,对字典进行迭代只会得到关键字,但要以这种精确的方式映射字典,则需要对<em>项</em>进行迭代。你知道吗</p>
<h3>具体例子</h3>
<p>这里是一个抽象基类方法,它包括<code>namedtuple</code>的特殊情况和三个抽象基类<code>Sequence</code>、<code>Mapping</code>和<code>Set</code>。对于以预期方式定义上述任何接口的任何类型,它都将按预期的方式运行。然后返回到iterables的一般行为。在后一种情况下,输出的类型与输入的类型不同,但至少可以工作。你知道吗</p>
<pre><code>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)
</code></pre>
<p>我将其定义为ABC,因为这样可以创建从它继承的新类。但您也可以在任何类的现有实例上调用它,它的行为将与预期的一样。您也可以将上面的<code>map</code>方法用作独立函数。你知道吗</p>
<pre><code>>>> 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}
</code></pre>
<p>定义ABC最酷的一点是,您可以将它用作“mix-in”。下面是一个<code>MappablePoint</code>派生自<code>Point</code>名称为duple的<code>MappablePoint</code>:</p>
<pre><code>>>> class MappablePoint(Point, Mappable):
... pass
...
>>> p = MappablePoint(5, 10)
>>> p.map(double)
MappablePoint(x=10, y=20)
</code></pre>
<p>您还可以根据<a href="https://stackoverflow.com/a/53876929/577088">Azat Ibrakov's answer</a>,使用<code>functools.singledispatch</code>装饰器稍微修改这种方法。(这对我来说是个新鲜事,他应该得到这部分答案的全部信任,但为了完整起见,我想我还是把它写下来吧。)</p>
<p>这看起来像下面这样。注意,我们仍然必须使用特殊情况<code>namedtuple</code>,因为它们破坏了元组构造函数接口。以前我并不介意,但现在我觉得这是一个非常恼人的设计缺陷。另外,我设置了一些东西,以便最终的<code>fmap</code>函数使用预期的参数顺序。(我想用<code>mmap</code>而不是<code>fmap</code>,因为“Mappable”比“Functor”IMO更像python,但是<code>mmap</code>已经是一个内置库了!该死的。)</p>
<pre><code>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)
</code></pre>
<p>一些测试:</p>
<pre><code>>>> 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)
</code></pre>
<h3>关于中断接口的最后说明</h3>
<p>这两种方法都不能保证这将适用于所有被认为是<code>Sequence</code>的事物,以此类推,因为ABC机制不检查函数签名。这不仅是构造函数的问题,也是所有其他方法的问题。如果没有类型注释,这是不可避免的。你知道吗</p>
<p>但实际上,这可能并不重要。如果您发现自己使用的工具以奇怪的方式打破了接口约定,请考虑唱一首不同的歌。(实际上,我想说的是,<code>namedtuple</code>也是如此,因为我非常喜欢它们!)这就是许多Python设计决策背后的<a href="https://mail.python.org/pipermail/tutor/2003-October/025932.html" rel="nofollow noreferrer">consenting adults</a>哲学,在过去的几十年中,它一直运行得很好。你知道吗</p>