Сделайте что-нибудь с коллекцией и затем верните тот же тип, что и коллекция - PullRequest
0 голосов
/ 20 декабря 2018

Я хочу применить функцию f к коллекции xs, но сохранить ее тип.Если я использую map, я получаю «объект карты»:

def apply1(xs, f):
  return map(f, xs)

Если я знаю, что xs является чем-то вроде list или tuple, я могу заставить его иметь тот жевведите:

def apply2(xs, f):
  return type(xs)(map(f, xs))

Однако, это быстро ломается для namedtuple (которым я в настоящее время пользуюсь) - потому что, насколько я знаю, namedtuple должен быть создан с синтаксисом распаковки иливызывая его _make функцию.Кроме того, namedtuple является константой, поэтому я не могу перебрать все записи и просто изменить их.

Дополнительные проблемы возникают из-за использования dict.

Существует ли универсальный способ выражениятакая apply функция, которая работает для всего, что можно повторять?

Ответы [ 2 ]

0 голосов
/ 21 декабря 2018

Выглядит как идеальное задание для functools.singledispatch декоратора :

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 можно просто использовать как

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

IЯ не знаю, что такое желаемое поведение для dict объектов, но величие этого подхода в том, что его легко расширить.

0 голосов
/ 21 декабря 2018

У меня есть предчувствие, что вы пришли из Хаскелла - это правильно?(Я предполагаю, потому что вы используете 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, и она довольно хорошо сработала дляпоследние пару десятилетий.

...