Заставить операторов перегружать меньше избыточных в python?

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

class Vector:
    def __mul__(self, other):
        #Vector([1, 2, 3]) * 5 => Vector([5, 10, 15])
        if isinstance(other, int) or isinstance(other, float):
            tmp = list()
            for i in self.l:
                tmp.append(i * other)
            return Vector(tmp)
        raise VectorException("We can only mul a Vector by a scalar")

    def __truediv__(self, other):
        #Vector([1, 2, 3]) / 5 => Vector([0.2, 0.4, 0.6])
        if isinstance(other, int) or isinstance(other, float):
            tmp = list()
            for i in self.l:
                tmp.append(i / other)
            return Vector(tmp)
        raise VectorException("We can only div a Vector by a Scalar")

    def __floordiv__(self, other):
        #Vector([1, 2, 3]) // 2 => Vector([0, 1, 1])
        if isinstance(other, int) or isinstance(other, float):
            tmp = list()
            for i in self.l:
                tmp.append(i // other)
            return Vector(tmp)
        raise VectorException("We can only div a Vector by a Scalar")

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

Ответ 1

Что вы хотите сделать, это динамически генерировать методы. Существует несколько способов сделать это: от супердинамики и создания их на лету в метаклассе __getattribute__ (хотя это не работает для некоторых специальных методов - см. документы) для генерации исходного текста для сохранения в файле .py, который вы можете затем import. Но самое простое решение - создать их в определении класса, что-то вроде этого:

class MyVector:
    # ...

    def _make_op_method(op):
        def _op(self, other):
            if isinstance(other, int) or isinstance(other, float):
                tmp = list()
                for i in self.l:
                    tmp.append(op(i. other))
                return Vector(tmp)
            raise VectorException("We can only {} a Vector by a scalar".format(
                op.__name__.strip('_'))
        _.op.__name__ = op.__name__
        return _op

    __mul__ = _make_op(operator.__mul__)
    __truediv__ = _make_op(operator.__truediv__)
    # and so on

-

Вы можете получить fancier и установить _op.__doc__ в соответствующую docstring, которую вы создаете (см. functools.wraps в stdlib для некоторых релевантных код) и постройте __rmul__ и __imul__ так же, как вы создаете __mul__, и так далее. И вы можете написать метакласс, класс декоратора или генератор функций, который завершает некоторые детали, если вы собираетесь делать много вариантов одной и той же вещи. Но это основная идея.

Фактически, перемещение его вне класса упрощает устранение еще большего повторения. Просто определите этот метод _op(self, other, op) в классе вместо локально внутри _make_op и украсите класс @numeric_ops, который вы можете определить следующим образом:

def numeric_ops(cls):
    for op in ‘mul truediv floordiv ...’.split():
        def _op(self, other):
            return self._op(other, getattr(operator, op)
        _op.__name__ = f’__{op}__`
        setattr(cls, f’__{op}__’, _op)

Если вы посмотрите, например, functions.total_ordering, он делает что-то похожее, чтобы генерировать любые отсутствующие порядковые опционы из тех, которые там есть.

-

operator.mul и т.д., поступают из модуля operator в stdlib - это просто тривиальные функции, где operator.__mul__(x, y) в основном просто вызывает x * y и т.д., сделанные для того, когда вам нужно передать операторное выражение как функцию.

В stdlib есть примеры такого кода, хотя гораздо больше примеров связанных, но гораздо более простых __rmul__ = __mul__.

-

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

-

Итак, если вы это делаете?

Ну, СУХОЙ важен. Если вы скопируете-вставьте-отредактируете дюжину раз, это вряд ли приведет к тому, что вы ввернете одно из изменений и в итоге получите метод mod, который на самом деле кратно и что (и unit test, который его не поймает). И затем, если позже вы обнаружите недостаток в реализации, который вы скопировали и вставили дюжину раз (как между исходной, так и отредактированной версией вопроса), вы должны устранить тот же недостаток в десятке мест, что является еще одной потенциальной ошибкой магнит.

С другой стороны, читаемость подсчитывается. Если вы не понимаете, как это работает, вы, вероятно, не должны этого делать, и должны согласиться на ответ Рамазана Полата. (Это не совсем так компактно или эффективно, но, конечно, его легче понять). В конце концов, если код не очевиден для вас, тот факт, что вам нужно только исправить ошибку один раз, а не десяток раз, факт, что вы не знаете, как это исправить. И даже если вы это понимаете, стоимость умения часто может перевесить преимущества DRY.

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

Ответ 2

Это другой подход:

class Vector:
    def __do_it(self, other, func):
        if isinstance(other, int) or isinstance(other, float):
            tmp = list()
            for i in self.l:
                tmp.append(func(i, other))
            return Vector(tmp)
        raise ValueError("We can only operate a Vector by a scalar")

    def __mul__(self, other):
        return self.__do_it(other, lambda i, o: i * o)

    def __truediv__(self, other):
        return self.__do_it(other, lambda i, o: i / o)

    def __floordiv__(self, other):
        return self.__do_it(other, lambda i, o: i // o)

Ответ 3

Ваш код может быть таким же компактным, как ниже (juanpa.arrivillaga предложил return NotImplemented вместо создания исключения):

def __mul__(self, other):
    #Vector([1, 2, 3]) * 5 => Vector([5, 10, 15])
    try:
        return Vector([i * other for i in self.l])
    except TypeError:
        return NotImplemented

Ответ 4

Шаблон стратегии - ваш друг здесь. Я также коснусь еще нескольких способов очистки кода.

Здесь вы можете прочитать о шаблоне стратегии: https://en.wikipedia.org/wiki/Strategy_pattern

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

class Vector:
    def _arithmitize(self, other, f, error_msg):
        if isinstance(other, int) or isinstance(other, float):
            tmp = list()
            for a in self.l:
                tmp.append(func(a, other))
            return Vector(tmp)
        raise ValueError(error_msg)

    def _err_msg(self, op_name):
        return "We can only {} a vector by a scalar".format(opp_name)

    def __mul__(self, other):
        return self._arithmitize(
            other, 
            lambda a, b: a * b, 
            self._err_msg('mul'))

    def __div__(self, other):
        return self._arithmitize(
            other, 
            lambda a, b: a / b, 
            self._err_msg('div'))
    # and so on ...

Мы можем очистить это немного подробнее с пониманием списка

class Vector:
    def _arithmetize(self, other, f, error_msg):
        if isinstance(other, int) or isinstance(other, float):
            return Vector([f(a, other) for a in self.l])
        raise ValueError(error_msg)

    def _err_msg(self, op_name):
        return "We can only {} a vector by a scalar".format(opp_name)

    def __mul__(self, other):
        return self._arithmetize(
            other, 
            lambda a, b: a * b, 
            self._err_msg('mul'))

    def __div__(self, other):
        return self._arithmetize(
            other, 
            lambda a, b: a / b, 
            self._err_msg('div'))

Мы можем улучшить проверку типа

import numbers

class Vector:
    def _arithmetize(self, other, f, error_msg):
        if isinstance(other, number.Numbers):
            return Vector([f(a, other) for a in self.l])
        raise ValueError(error_msg)

Мы можем использовать операторы вместо записи lambdas:

import operators as op

class Vector:
    # snip ...
    def __mul__(self, other):
        return self._arithmetize(other, op.mul, self._err_msg('mul'))

Итак, у нас получилось что-то вроде этого:

import numbers
import operators as op

class Vector(object):
    def _arithmetize(self, other, f, err_msg):
        if isinstance(other, numbers.Number):
            return Vector([f(a, other) for a in self.l])
        raise ValueError(self._error_msg(err_msg))
    def _error_msg(self, msg):
        return "We can only {} a vector by a scalar".format(opp_name)

    def __mul__(self, other):
        return self._arithmetize(op.mul, other, 'mul')

    def __truediv__(self, other):
        return self._arithmetize(op.truediv, other, 'truediv')

    def __floordiv__(self, other):
        return self._arithmetize(op.floordiv, other, 'floordiv')

    def __mod__(self, other):
        return self._arithmetize(op.mod, other, 'mod')

    def __pow__(self, other):
        return self._arithmetize(op.pow, other, 'pow')

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

Если вам нужно генерировать их динамически, попробуйте что-то вроде этого:

class Vector(object):
    def _arithmetize(....):
        # you've seen this already 

    def __getattr__(self, name):
        funcs = {
            '__mul__': op.mul, # note: this may not actually work with dunder methods. YMMV
            '__mod__': op.mod,
            ...
        }
        def g(self, other):
            try:
                return self._arithmetize(funcs[name],...)
             except:
                 raise NotImplementedError(...)
        return g

Если вы обнаружите, что этот динамический пример не работает, проверьте чтобы операторы перегружали меньше избыточных в python?, который обрабатывает случай динамического создания dunder_methods в большинстве реализаций python.