Python 对集合执行某些操作,然后返回与集合相同的类型
我想将函数Python 对集合执行某些操作,然后返回与集合相同的类型,python,python-3.x,type-conversion,namedtuple,Python,Python 3.x,Type Conversion,Namedtuple,我想将函数f应用于集合xs,但保留其类型。如果我使用map,我会得到一个“map对象”: def apply1(xs, f): return map(f, xs) 如果我知道xs类似于列表或元组,我可以强制它具有相同的类型: def apply2(xs, f): return type(xs)(map(f, xs)) 然而,对于namedtuple(我目前正在使用它)——这很快就会崩溃,因为据我所知namedtuple需要使用解包语法或调用它的\u make函数来构造。而且,name
f
应用于集合xs
,但保留其类型。如果我使用map
,我会得到一个“map对象”:
def apply1(xs, f):
return map(f, xs)
如果我知道xs
类似于列表
或元组
,我可以强制它具有相同的类型:
def apply2(xs, f):
return type(xs)(map(f, xs))
然而,对于namedtuple
(我目前正在使用它)——这很快就会崩溃,因为据我所知namedtuple
需要使用解包语法或调用它的\u make
函数来构造。而且,namedtuple
是const,所以我不能迭代所有条目,而只是更改它们
使用dict
会产生更多问题
有没有一种通用的方式来表达这样一个适用于一切可移植的
apply
函数呢?我有一种预感,你是哈斯凯尔人,对吗?(我猜是因为您使用f
和xs
作为变量名。)在Haskell中,您的问题的答案是“是的,它被称为fmap
,但它只适用于具有已定义实例的类型。”
另一方面,Python没有“函子”的一般概念。所以严格地说,答案是否定的。要得到这样的东西,您必须依赖Python提供的其他抽象
美国广播公司救援
一个相当普遍的方法是使用。它们提供了一种结构化的方法来指定和检查特定接口。Functor typeclass的Pythonic版本是一个抽象基类,它定义了一个特殊的fmap
方法,允许单个类指定如何映射它们。但这种事情并不存在。(不过,我认为这将是对Python的一个非常酷的补充!)
现在,您可以定义自己的抽象基类,这样您就可以创建一个函子ABC,它需要一个fmap
接口,但是您仍然需要编写自己的list
,dict
,等等的所有函数化子类,所以这并不理想
更好的方法是使用现有接口拼凑出一个似乎合理的映射通用定义。您必须非常仔细地考虑需要组合现有接口的哪些方面。仅仅检查一个类型是否定义了\uuuu iter\uuuu
是不够的,因为正如您已经看到的,类型的迭代定义不一定转化为构造定义。例如,在字典上迭代只会得到键,但要以这种精确的方式映射字典,需要在项上迭代
具体例子
这里有一个抽象基类方法,它包括命名双倍
的特殊情况和三个抽象基类--序列
,映射
,以及集合
。对于以预期方式定义上述任何接口的任何类型,它都将按照预期的方式运行。然后,它又回到了iterables的一般行为。在后一种情况下,输出的类型与输入的类型不同,但至少可以工作
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
派生自点
命名的耦合:
>>> class MappablePoint(Point, Mappable):
... pass
...
>>> p = MappablePoint(5, 10)
>>> p.map(double)
MappablePoint(x=10, y=20)
您还可以使用functools.singledispatch
decorator,根据的稍微修改此方法。(这对我来说是新鲜事——他应该得到答案这一部分的全部赞誉,但为了完整起见,我想我还是把它写下来。)
这将看起来像下面这样。请注意,我们仍然必须使用特例namedtuple
s,因为它们破坏了tuple构造函数接口。这以前并没有困扰过我,但现在我觉得这是一个非常恼人的设计缺陷。另外,我设置了一些东西,以便最终的fmap
函数使用预期的参数顺序。(我想用mmap
而不是fmap
,因为“Mappable”是一个比“Functor”更像python的名字。但是mmap
已经是一个内置库了!该死。)
一些测试:
>>> 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
s等的事物,因为ABC机制不检查函数签名。这不仅是构造函数的问题,也是所有其他方法的问题。如果没有类型注释,这是不可避免的
然而,在实践中,这可能并不重要。如果你发现自己使用了一种工具,它打破了常规的界面约定,考虑使用不同的工具。(事实上,我也很喜欢
namedtuple
s!)这是许多Python设计决策背后的“哲学”,在过去的几十年中,它运行得非常好。看起来是一项完美的任务:
在这之后,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
对象的期望行为是什么,但这种方法的伟大之处在于它易于扩展。Const不是正确的词namedtuple
对象是不可变的,但tuple对象也是不可变的。我不确定“我不能迭代所有条目并只是更改它们”是如何相关的。我认为你必须为你关心的额外类型添加特殊情况。无论如何,“有没有一种通用的方法来表达这样一个适用于所有可iterable的函数?”不,没有。您无法保证iterable对象将以一般相似的方式构造<代码>类Foo:def uu iter uu(self):y
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([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)