У меня есть предчувствие, что вы пришли из Хаскелла - это правильно?(Я предполагаю, потому что вы используете f
и xs
в качестве имен переменных.) Ответ на ваш вопрос в Haskell будет "да, он называется fmap
, но он работает только с типами, которые имеют определенную Functor instance. "
Python, с другой стороны, не имеет общего понятия «Функтор».Строго говоря, ответ - нет.Чтобы получить что-то подобное, вам придется прибегнуть к другим абстракциям, которые предоставляет Python.
Азбука на помощь
Одним из довольно общих подходов было бы использование абстрактных базовых классов .Они предоставляют структурированный способ указать и проверить определенные интерфейсы.Pythonic-версия класса типов Functor будет абстрактным базовым классом, который определяет специальный метод fmap
, позволяющий отдельным классам указывать, как они должны отображаться.Но такого не существует.(Я думаю, что это было бы действительно классное дополнение к Python!)
Теперь вы можете определить свои собственные абстрактные базовые классы, чтобы вы могли создать Functor ABC, который ожидает интерфейс fmap
, но вы 'Мне все еще нужно написать все ваши собственные подклассы list
, dict
и т. д., так что это не совсем идеально.
Лучшим подходом было бы использовать существующие интерфейсы для объединения общихопределение отображения, которое кажется разумным.Вы должны были бы очень тщательно подумать о том, какие аспекты существующих интерфейсов вам нужно объединить.Недостаточно просто проверить, определяет ли тип __iter__
, потому что, как вы уже видели, определение итерации для типа не обязательно переводит в определение конструкции.Например, итерация по словарю дает только ключи, но для точного сопоставления словаря потребуется итерация по items .
Конкретные примеры
Вотабстрактный базовый метод, который включает в себя специальные случаи для namedtuple
и три абстрактных базовых класса - Sequence
, Mapping
и Set
.Он будет работать так, как ожидается для любого типа, который определяет любой из вышеперечисленных интерфейсов ожидаемым образом.Затем возвращается к общему поведению для итераций.В последнем случае вывод не будет иметь тот же тип, что и ввод, но по крайней мере он будет работать.
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
, полученный из Point
namedtuple:
>>> class MappablePoint(Point, Mappable):
... pass
...
>>> p = MappablePoint(5, 10)
>>> p.map(double)
MappablePoint(x=10, y=20)
Вы также можете немного изменить этот подход в свете ответа Азата Ибракова , используя декоратор functools.singledispatch
.(Это было ново для меня - он должен был отдать должное этой части ответа, но я подумал, что напишу это ради полноты.)
Это будет выглядеть примерно так, как показано ниже.Обратите внимание, что нам все еще приходится иметь дело с namedtuple
в особом случае, потому что они нарушают интерфейс конструктора кортежей.Это не беспокоило меня раньше, но теперь это похоже на действительно раздражающий недостаток дизайна.Кроме того, я настроил все так, чтобы конечная функция fmap
использовала ожидаемый порядок аргументов.(Я хотел использовать mmap
вместо fmap
, потому что «Mappable» - это более Pythonic имя, чем «Functor» IMO. Но mmap
уже встроенная библиотека! Черт.)
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)
Несколько тестов:
>>> 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, и она довольно хорошо сработала дляпоследние пару десятилетий.