LINUX.ORG.RU

@multimethod vs if_stmt

 


0

0

if_stmt:

def foo(a, b):
    if isinstance(a, int) and isinstance(b, int):
        ...code for two ints...
    elif isinstance(a, float) and isinstance(b, float):
        ...code for two floats...
    elif isinstance(a, str) and isinstance(b, str):
        ...code for two strings...
    else:
        raise TypeError("unsupported argument types (%s, %s)" % (type(a), type(b)))

или @multimethod:

from multimethod import multimethod

@multimethod(int, int)
def foo(a, b):
    ...code for two ints...

@multimethod(float, float):
def foo(a, b):
    ...code for two floats...

@multimethod(str, str):
def foo(a, b):
    ...code for two strings...

А что выберешь ты?

Deleted

Декораторы выглядят приятнее, чем лапша.

Octagon
()

Multimethod, но после замера скорости (и если она важна).

AlexKiriukha ★★★★
()

Я бы задумался зачем такое вообще понадобилось. У нас такой код в проекте появился коде метода из-за того что предыдущий девелопер не умел в ООП и напихал всё подряд в один класс.

Ещё иногда такое бывшие пхп-ники пишут. Там обычно перлы типа

if isinstance(a, str):
  a = int(a)

Короче, это нездоровая фигня в коде.

true_admin ★★★★★
()
def foo(a, b):
    if isinstance(a, int) and isinstance(b, int):
        ...code for two ints...
    elif isinstance(a, float) and isinstance(b, float):
        ...code for two floats...
    elif isinstance(a, str) and isinstance(b, str):
        ...code for two strings...

Так нельзя. Зачем перебирать все возможные варианты типов (а если их будет 100?), когда можно сразу прыгать от типа объекта к нужной реализации? Например, где-то у нас хранится словарь foo_instances со всеми реализациями foo для разных типов, и

def foo(a, b):
    ...
    return foo_instances[(type(a), type(b))](a, b)

Ну я надеюсь, что этот ваш @multimethod как-то так и работает (в смысле - сразу вызывает нужную реализацию по типу объекта, а не городит if-ы). Тогда конечно я за @multimethod

Crocodoom ★★★★★
()

По моему опыту писания на питоне, если мне надо проверять типы при помощи isinstance, то я где-то ошибся в дизайне и моя функция делает что-то не то.

Yur4eg ★★
()
Ответ на: комментарий от true_admin

Я бы задумался зачем такое вообще понадобилось.

Это классический ad-hoc полиморфизм, он часто бывает нужен.

Другое дело, что конкретно в питоне ad-hoc полиморфизм неизбежно будет выглядеть убого (что мы и наблюдаем). Может быть, просто ошиблись с языком под задачу.

Crocodoom ★★★★★
()
Ответ на: комментарий от Crocodoom

конкретно в питоне ad-hoc полиморфизм неизбежно будет выглядеть убого

Это должно наводить на мысли. Нет, не на что питон говно :)

true_admin ★★★★★
()
Ответ на: комментарий от Crocodoom

Другое дело, что конкретно в питоне ad-hoc полиморфизм неизбежно будет выглядеть убого (что мы и наблюдаем).

Почему?

Octagon
()

Второе однозначно. Но надо смотреть на код multimethod. Где-то видел простой вариант такой штуки сделаной исключительно на стандартной либе.

Deleted
()

Безотносительно питона а вообще. Выбирать надо от задачи. Мультиметоды нужны тогда, когда нужно менять/расширять/сужать в рантайме поведение объекта, ветвления обычно тривиально не заменишь на лету, поэтому вопрос, собственно ниачем

somequest1
()
Ответ на: комментарий от Crocodoom

Это классический ad-hoc полиморфизм, он часто бывает нужен.

если сравнивать с обычной статической перегрузкой, то как раз наоборот, мультиметоды позволяют манипулировать перегрузкой в рантайме, или где ты имеешь в виду не убого? в лиспе? Ты о чем вообще?

somequest1
()
Ответ на: комментарий от Crocodoom

Да и «классический» — тут это наврядли. «классический» — это как раз перегрузка

somequest1
()

А так, вообще, если задача не сложная, то лучше мультиметоды заменить на визитор, так будет ясней и проще.

somequest1
()
Ответ на: комментарий от true_admin

Я бы задумался зачем такое вообще понадобилось.

Ну это диспетчеризация по типу, довольно обыденная задача, что тут думать.

a = int(a)

нездоровая фигня в коде.

ты имеешь в виду, что надо без проверки приводить? Ну так ведь не любой тип можно к нужному автоматом привести. Да и не сводится тут все к преобразованию типов

somequest1
()
Ответ на: комментарий от Crocodoom

прыгать от типа объекта к нужной реализации?

это тебе только кажется, что там где то что-то прыгает, на самом деле все равно идет перебор под капотом

somequest1
()
Ответ на: комментарий от somequest1

Перебор типов никуда не убрать (в этом и убогость - ответ на твой вопрос выше). Но можно убрать тупой перебор.

Crocodoom ★★★★★
()
Ответ на: комментарий от somequest1

Всё верно, однако найти нужный тип в множестве всех вариантов можно быстрее, чем сравнением с каждым. Я предложил решение в первом своём сообщении: словарь, где ключом выступает тип, а значением - экземпляр функции для этого типа.

Crocodoom ★★★★★
()
Ответ на: комментарий от Crocodoom

не факт что это будет быстрей. Я ж говорю, под капотам там все равно то же самое происходит.

somequest1
()

Ребзя, чтобы не гадать, что там да как реализовано в модуле multimethod, вот его исходник:

import sys
import types
try:
    from future_builtins import map, zip
except ImportError:
    pass

__version__ = '0.6'


class DispatchError(TypeError):
    pass


class signature(tuple):
    """A tuple of types that supports partial ordering."""
    def __le__(self, other):
        return len(self) <= len(other) and all(map(issubclass, other, self))

    def __lt__(self, other):
        return self != other and self <= other

    def __sub__(self, other):
        """Return relative distances, assuming self >= other."""
        return [left.__mro__.index(right if right in left.__mro__ else object) for left, right in zip(self, other)]


class multimethod(dict):
    """A callable directed acyclic graph of methods."""
    @classmethod
    def new(cls, name='', strict=False):
        """Explicitly create a new multimethod.  Assign to local name in order to use decorator."""
        self = dict.__new__(cls)
        self.__name__, self.strict = name, strict
        return self

    def __new__(cls, *types):
        """Return a decorator which will add the function."""
        namespace = sys._getframe(1).f_locals

        def decorator(func):
            if isinstance(func, cls):
                self, func = func, func.last
            else:
                self = namespace.get(func.__name__, cls.new(func.__name__))
            self[types] = self.last = func
            return self
        if len(types) == 1 and hasattr(types[0], '__annotations__'):
            func, = types
            types = tuple(map(func.__annotations__.__getitem__, func.__code__.co_varnames[:len(func.__annotations__)]))
            return decorator(func)
        return decorator

    def __init__(self, *types):
        dict.__init__(self)

    def __get__(self, instance, owner):
        return self if instance is None else types.MethodType(self, instance)

    def parents(self, types):
        """Find immediate parents of potential key."""
        parents = {key for key in self if isinstance(key, signature) and key < types}
        return parents - {ancestor for parent in parents for ancestor in parent.parents}

    def clean(self):
        """Empty the cache."""
        for key in list(self):
            if not isinstance(key, signature):
                dict.__delitem__(self, key)

    def __setitem__(self, types, func):
        self.clean()
        types = signature(types)
        parents = types.parents = self.parents(types)
        for key in self:
            if types < key and (not parents or parents & key.parents):
                key.parents -= parents
                key.parents.add(types)
        dict.__setitem__(self, types, func)

    def __delitem__(self, types):
        self.clean()
        dict.__delitem__(self, types)
        for key in self:
            if types in key.parents:
                key.parents = self.parents(key)

    def __missing__(self, types):
        """Find and cache the next applicable method of given types."""
        keys = self.parents(types)
        if (len(keys) == 1 if self.strict else keys):
            return self.setdefault(types, self[min(keys, key=signature(types).__sub__)])
        raise DispatchError("{}{}: {} methods found".format(self.__name__, types, len(keys)))

    def __call__(self, *args, **kwargs):
        """Resolve and dispatch to best method."""
        return self[tuple(map(type, args))](*args, **kwargs)

    def register(self, *types):
        """A decorator for registering in the style of singledispatch."""
        return lambda func: self.__setitem__(types, func) or func


def multidispatch(func):
    """A decorator which creates a new multimethod from a base function in the style of singledispatch."""
    mm = multimethod.new(func.__name__)
    mm[()] = func
    return mm

Virtuos86 ★★★★★
()
Ответ на: комментарий от somequest1

ты имеешь в виду, что надо без проверки приводить?

Не, я к тому что если в программе так прыгают типы и в функцию то строка прилетает то число то это обычно неправильно. Во всяком случае в том коде что я видел. Т.е., от того что это кто-то называет это дисперизацией, мультиметодом или «ad-hoc полиморфизмом» плохой код не станет хорошим.

В общем, предлагаю ТС показать проект целиком. Но я не удивлюсь если вопрос был чисто теоретическим.

true_admin ★★★★★
()
Ответ на: комментарий от Crocodoom

Всё верно, однако найти нужный тип в множестве всех вариантов можно быстрее, чем сравнением с каждым.

Имхо это микрооптимизация.

true_admin ★★★★★
()
Ответ на: комментарий от somequest1

Я их привожу к нужному типу внутри функции, то есть у меня будут объекты которые умеют выдавать нужный интерфейс, а если не приводятся, то значит у меня ошибка на этапе проектирования. Для isinstance я вижу только такое применение

def foo(a):
    if not isinstance(a, MyType):
        raise TypeError("err msg")
    #...
Ветвление внутри функции в зависимости от типа это не pythonic way, питон все таки язык с утиной типизацией и надо пользоваться этим его свойством. Оно эффектно реализовано в модуле zope.interface, там приведения типов сделаны через адептеры. Адептер это клас, который принимает в __init__() один аргумент: объект, для которого он реализует нужный интерфейс. Адаптеры собраны в реестр адаптеров и для пользователя запрос к этому реестру выглядит весьма лаконично Как запрос интерфейса у объекта. Рекомендую всем zope.interface
class Ifoo(Interface):
    pass

def foo(a, b):
    fooa = Ifoo(a)
    foob = Ifoo(b)
    #... далее код идентичен для всех объектов ...

Yur4eg ★★
()
Ответ на: комментарий от Yur4eg

Че-то я не совсем понял. В случае, если аргументы — типы и подтипы, они полиморфны и поддерживаеют единый интерфейс, естественно диспетчеризация по типам не нужна, но это ведь не значит, что такая диспетчеризация не нужна совсем? То что ты описал, это, по-моему, не замена, а просто что-то вроде полиморфизма подтипов или что-то в этом роде.

Вот классический пример:

(defgeneric collide (x y))

(defmethod collide ((x asteroid) (y asteroid))
  ;;астероид сталкивается с астероидом
  )

(defmethod collide ((x asteroid) (y spaceship))
  ;;астероид сталкивается с космическим кораблем
  )

(defmethod collide ((x spaceship) (y asteroid))
  ;;космический корабль сталкивается с астероидом
  )

(defmethod collide ((x spaceship) (y spaceship))
  ;;космический корабль сталкивается с космическим кораблем
  )

somequest1
()
Ответ на: комментарий от somequest1

Я не силен в теории ООП, но по логике, которой я стараюсь придерживаться при написании кода, метод collide(a, b) будет принимать любые объекты, которые имеют следующий интерфейс: координаты в пространстве, объем и могут получить повреждение при столкновении. Получается что конкретно тип объекта мне не важен, пока мои объекты поддерживают этот интерфейс.

Но в реальной жизни может получится так, что функция collide() будет использована в том числе и для моделирования стокновений абсолютно упругих тел, которые получать повреждения не могут по определению и вот тогда и понадобится костылик с проверкой типа, так как есть уже написанная функция collide(), код которой подходит, но не до конца.

Yur4eg ★★
()
Ответ на: комментарий от Yur4eg

Получается что конкретно тип объекта мне не важен, пока мои объекты поддерживают этот интерфейс.

Не получается. Ты делаешь диспетчеризацию по типам именно ради того, чтобы сконфигурировать разное поведение для разных типов, независимо от их интерфейса

somequest1
()
Вы не можете добавлять комментарии в эту тему. Тема перемещена в архив.