4

Let say I have a python class A:

class A:
    def __init__(self, matrix, metadata: list):
        self.matrix = np.array(matrix)
        self.metadata = metadata
    #... 

Now I want all arithmetic operations work for my class. They supposed to simply translate the operation to matrix, i.e. like so:

    def __add__(self, other):
        if isinstance(other, type(self)):
            raise ValueError("Not allowed.")
        else:
            return A(
                matrix=self.matrix.__add__(other),
                metadata=self.metadata,
            )

Now the problem is that I have to repeate almost the same code for each arithmetic magic function, i.e __add__, __sub__, __mul__, __truediv__, __pow__, __radd__, __rsub__, __rmul__, __rtruediv__, __iadd__, __isub__, __imul__, __itruediv__, __abs__, __round__, __floor__, __ceil__, __trunc__. Which leads to a lot of repeating code.

How can I define them dynamically in a for loop? like

magic_functions = ["__add__", "__sub__", ...]
for magic_function in magic_functions:
    # define the method and add it to the class
Rustam Guliev
  • 936
  • 10
  • 15
  • Maybe this answer will help you https://stackoverflow.com/questions/3431676/creating-functions-in-a-loop – Dan Jun 16 '19 at 11:12
  • I suggest to forget the loop, and explicitly define and test these matrix arithmetic methods: the next guy to read your code will thank you for it! – Reblochon Masque Jun 16 '19 at 11:54
  • @Dan Thank you for the link. As far as I understand, it is not applicable here. – Rustam Guliev Jun 16 '19 at 12:04
  • @ReblochonMasque Thank you for the suggestion. I see your point and it does make a sence. And that is how I implemented it for now. On the other hand, having ~15 almoust the same functions is kinda contradicts to DRY principle and makes it painful to add some minor changes. – Rustam Guliev Jun 16 '19 at 12:11
  • Agreed, `__add__` and `__sub__` have only one character diff (`+` i/o `-`); yet, this difference is important enough to justify two full implementations of the methods. – Reblochon Masque Jun 16 '19 at 12:15
  • Well, let's not confuse function name and function definition:) Infact, `__sub__` uses `__add__` method inside to not duplicate the code. For example, in python's source code https://github.com/python/cpython/blob/e42b705188271da108de42b55d9344642170aa2b/Lib/numbers.py#L91 – Rustam Guliev Jun 16 '19 at 12:25
  • Use a decorator on the class or a metaclass... or maybe [`exec`](https://docs.python.org/3/library/functions.html#exec) to dynamically define the functions (but that is ugly) – Giacomo Alzetta Jul 26 '19 at 09:35

2 Answers2

1

This (broad) sort of problem is the purpose of the operator module:

import operator
def mkop(f):    # the usual scope for a closure
  def op(self,o):
    if isinstance(o,type(self)): raise …
    return type(self)(matrix=f(self.matrix,o),
                      metadata=self.metadata)
  return op
for op in ['add','sub',…]:
  setattr(A,"__%s__"%op,mkop(getattr(operator,op)))

You can also use locals()[…]=mkop(…) (in one of its rare safe uses) to do the above while defining the class.

Davis Herring
  • 36,443
  • 4
  • 48
  • 76
0

I would like to suggest to you use the decorator in this situation. May be this is not so short but you will save the readability of your code.

import numpy as np

def decorator(fn):
    def ret_fn(*args, **kwargs):
        if isinstance(args[1], type(args[0])):
            raise ValueError("Not allowed.")
        else:
            return fn(*args, **kwargs)

    return ret_fn

class A:
    def __init__(self, matrix, metadata: list):
        self.matrix = np.array(matrix)
        self.metadata = metadata

    @decorator
    def __add__(self, other):
        return A(
            matrix=self.matrix.__add__(othe),
            metadata=self.metadata,
        )

The result:

>>> a1 = A([[1], [2]], [])
>>> a2 = a1 + [[3], [4]]
>>> print(a2.matrix)
[[4]
 [6]]
>>> a1 + a1
Traceback (most recent call last):
...
    raise ValueError("Not allowed.")
ValueError: Not allowed.

I don't know how many difference between your functions but you may rewrite decorator and the function pretty minimalistic:

decorator

def decorator(fn):
    def ret_fn(*args, **kwargs):
        if isinstance(args[1], type(args[0])):
            raise ValueError("Not allowed.")
        else:
            return A(
                    matrix=fn(*args, **kwargs),
                    metadata=args[0].metadata,
                )

    return ret_fn

the method

@decorator
def __add__(self, other):
    return self.matrix.__add__(other)
MartenCatcher
  • 2,713
  • 8
  • 26
  • 39