Заставить операторов перегружать меньше избыточных в 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.