Duplicating Python int * numpy.array behavior - python

I'm attempting to build a class representation of a matrix complete with most of the normal mathematical operations. I've hit a snag with the scalar multiplication operation.
The relevant part of the code is as follows:
import numpy
class Matrix(object):
def __init__(self, array):
self.array = numpy.array(array, dtype=int)
def __mul__(self, other):
if type(other) == int:
return Matrix(other*self.array)
else:
raise ValueError("Can not multiply a matrix with {0}".format(type(other)))
The standard way the scalar multiplication is expressed is cA where c is a scalar and A is a matrix, so c*A in Python. However, this fails with a TypeError: unsupported operand type(s) for *: 'int' and 'Matrix' while A*c runs as expected (note the other*self.array). Thus I conclude that the * operand is defined for int and numpy.array.
What is this magic and how can I replicate the behavior?

You need a __rmul__ in your calss. For example if you add
def __rmul__(self, other):
return self.__mul__(other)
then:
>>> A = Matrix(np.arange(12).reshape(3, 4))
>>> (2 * A).array
array([[ 0, 2, 4, 6],
[ 8, 10, 12, 14],
[16, 18, 20, 22]])
As in the docs, the __r***__
are called to implement the binary arithmetic operations with reflected (swapped) operands. These functions are only called if the left operand does not support the corresponding operation and the operands are of different types.

Related

Overloading operators for operands of different types in Python

Consider the following example of a 'wrapper' class to represent vectors:
class Vector:
def __init__(self, value):
self._vals = value.copy()
def __add__(self, other):
if isinstance(other, list):
result = [x+y for (x, y) in zip(self._vals, other)]
elif isinstance(other, Vector):
result = [x+y for (x, y) in zip(self._vals, other._vals)]
else:
# assume other is scalar
result = [x+other for x in self._vals]
return Vector(result)
def __str__(self):
return str(self._vals)
The __add__ method takes care of adding two vectors as well as adding a vector with a scalar. However, the second case is not complete as the following examples show:
>>> a = Vector([1.2, 3, 4])
>>> print(a)
[1.2, 3, 4]
>>> print(a+a)
[2.4, 6, 8]
>>> print(a+5)
[6.2, 8, 9]
>>> print(5+a)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +: 'int' and 'Vector'
To my understanding the reason is that the overloaded operator only tells Python what to do when it sees a + x where a is an instance of Vector, but there is no indication of what to do for x + a (with a an instance of Vector and x a scalar).
How one should overload the operators in such circumstances to cover all cases (i.e., to support the case that self is not an instance of Vector but other is)?
Ok. I guess I found the answer: one has to overload __radd__ operator as well:
class Vector:
def __init__(self, value):
self._vals = value.copy()
def __add__(self, other):
if isinstance(other, list):
result = [x+y for (x, y) in zip(self._vals, other)]
elif isinstance(other, Vector):
result = [x+y for (x, y) in zip(self._vals, other._vals)]
else:
# assume other is scalar
result = [x+other for x in self._vals]
return Vector(result)
def __radd__(self, other):
return self + other
def __str__(self):
return str(self._vals)
Although to me this looks a bit redundant. (Why Python does not use the commutativity of addition by default, assuming __radd__(self, other) always returns self + other? Of course for special cases the user can override __radd__.)
You could define a Scalar class that has int as its base class.
Then override __add__ to do what you want.
class Scalar(int):
def __add__(self):
# do stuff
You already figured out you need to implement __radd__. This is an answer as to why this is so, and why you need to do this in addition to implementing __add__, as a Both quotes are taken from Python Docs (Data Model - 3.3.8 Emulating numeric types), starting with the obvious:
These methods are called to implement the binary arithmetic operations (+, -, *, #, /, //, %, divmod(), pow(), **, <<, >>, &, ^, |). For instance, to evaluate the expression x + y, where x is an instance of a class that has an __add__() method, x.__add__(y) is called.
So order determines which object's implementation of __add__ is called. When the method doesn't support the operation with the passed argument NotImplemented should be returned. That's where the so-called "reflected methods" come into play:
These functions are only called if the left operand does not support the corresponding operation and the operands are of different types. For instance, to evaluate the expression x - y, where y is an instance of a class that has an __rsub__() method, y.__rsub__(x) is called if x.__sub__(y) returns NotImplemented [sic].
Now, why wouldn't __radd__(self, other) just fall back to __add__(self, other)? While ring addition is always commutative (see this and this math.stackexchange answers), you could have algebraic structures that are do not satisfy this assumption (e.g., near-rings). But my guess as a non-mathematician would be that it's just desirable to have a consistent data model across different numerical methods. While addition might be commonly commutative, multiplication is less so. (Think matrices and vectors! Although, admittedly this is not the best example, given __matmul__). I also prefer to see there being no exceptions, especially if I had to read about rings, etc. in a language documentation.

How to make "class1 * class2" behave like "class2 * class1"?

I've been watching Linear Algebra series made by 3Blue1Brown and I came up with this idea that I should write programs to compute the math, so I started to do it.
I wrote some methods. I wrote mul method for it. This method stretch out the vector by a given factor.
class Vector:
def __init__(self, data):
# [1]
# [1, 2, 3] --> [2]
# [3]
self.data = data
def __mul__(self, other):
if not isinstance(other, (int, float)):
raise TypeError("The second object(item) is not a number(integer, float)")
return Vector(list(map(lambda x: x * other, self.data)))
For example:
sample = Vector([1, 2])
When I execute this, it executes without errors:
print(sample * 10)
# it returns Vector([10, 20])
But when I execute this:
print(10 * sample)
It throws an error:
Traceback (most recent call last):
File "/home/jackson/Desktop/Matrices/MM.py", line 139, in <module>
print(10 * a)
TypeError: unsupported operand type(s) for *: 'int' and 'Vector'
I know the second one runs int.mul . So is there any way for the second one to behave like the first one?? Because technically there shouldn't be any difference between "Vector * int" and "int * Vector".
Here is the full code if you need --> link
Yes, you need to implement __rmul__ etc. See https://docs.python.org/3.7/reference/datamodel.html#emulating-numeric-types
Also, a really good linear algebra library already exists for python called numpy (but if you are implementing it yourself for learning purposes, just ignore that and have fun instead).

Magic methods with > 1 operands between MyClass and standard Class

Imagine I want to define a new number class. Say, RationalFractions or GaussIntegers, whatever. Of course, I can easily define a + b for two objects of MyClass. But i would like to be able to add an object of MyClass with some existing, like "integer" or "float". With the result having a relevant type (from my point of view). E.g. the result of GaussInteger + float = GaussInteger, RationalFraction + integer = RationalFraction, etc.
I guess I should somehow alter add for Object class, or "integer", "float"? Or there is a way to do it without meddling with the existing classes?
Edit. So, an example:
class RatFr:
def __init__(self, m, n=1):
self.m = m
self.n = n
def __add__(self, other):
temp = (RatFr(other) if type(other) == int else other)
return RatFr(self.m * temp.n + self.n * temp.m, self.n * temp.n)
def __str__(self):
return f'{self.m}/{self.n}'
a = RatFr(5,3)
b = 1
print(a)
print(a + b)
print(b + a)
I get as a result:
5/3
8/3
Traceback (most recent call last):
File "/Users/aleksej/PycharmProjects/Alex2/playaround.py", line 19, in <module>
print(b + a)
TypeError: unsupported operand type(s) for +: 'int' and 'RatFr'
Trying to convert self does nothing good. As soon as the first operand is int, python obviously looks for integer add method.
Yes. You will want to override __add__ from object, taking self and say x as parameters. You can then deal with x according to its type. Here you have a few options. You can do explicit type checking, but this is not very Pythonic. I would probably make a conversion function to convert from int, float, etc to your type and call it on x. Then you can do whatever addition would do between two objects of your type. This sort of call to a conversion function before doing an operation is done in the mpmath library, in the backend. Remember that you will need to check if the thing you are converting is already the right type.

Python: What does it mean when two objects are "multiplied" together using an asterisk (*)?

I am a bit of a Python newbie regarding classes. I have seen that some scripts include lines which appear to "multiply" instances of classes together. For example:
Z = X * Y
where X and Y are instances of two different classes.
What does the * symbol mean and how is it applied to classes (or instances of classes) in Python?
In most cases * pertains to multiplication, but I don't understand what it means to "multiply" classes.
Any info is appreciated.
The * operator just means "multiply".
But what it means for two objects to be multiplied is up to those objects' types to decide.
For all of the builtin and stdlib types, it means multiplication if that makes sense, or it's a TypeError if that doesn't make sense. For example:
>>> 2 * 3
6
>>> (1, 2, 3) * 3
(1, 2, 3, 1, 2, 3, 1, 2, 3)
>>> "abc" * "def"
TypeError: can't multiply sequence by non-int of type 'str'
Since multiplication doesn't make sense for class objects, they're one of the kinds of things where it's a TypeError:
>>> class C: pass
>>> C * 2
TypeError: unsupported operand type(s) for *: 'type' and 'int'
If you (or a third-party library author) create a new type, you can decide what multiplication means. The way you do this is covered under Emulating numeric types in the documentation, but the short version is that you define a __mul__ method:
class MyNumberyThing:
def __init__(self, n):
self.n = n
def __repr__(self):
return f'MyNumberyType({self.n})'
def __mul__(self, other):
if isinstance(other, MyNumberyThing):
return MyNumberyThing(self.n * other.n)
elif isinstance(other, int):
return MyNumberyThing(self.n * other)
elif isinstance(other, float):
raise TypeError("sorry, can't multiply by float because of precision issues")
else:
raise TypeError(f"sorry, don't know how to multiply by {type(other).__name__}")
Notice that this makes instances of MyNumberyThing multiplicable. It doesn't make MyNumberyThing itself multiplicable (MyNumberyThing isn't a MyNumberyThing, it's a type):
>>> n = MyNumberyThing(2)
>>> n * 3
MyNumberyType(6)
>>> MyNumberyThing * 3
TypeError: unsupported operand type(s) for *: 'type' and 'int'
Of course nothing's stopping you from defining something ridiculous:
class MySillyThing:
def __mul__(self, other):
self.storage = -other
print(f'I took your {other}')
>>> silly = MySillyThing()
>>> silly * 3
I took your 3
>>> silly.storage
-3
… except, of course, for the fact that nobody would understand your code. (And that "nobody" includes you, 6 months later trying to debug something you thought you were done with…)
As a side note, Python actually has two ways to spell "multiply": * and #.
The builtin types all ignore #, so you'll just get a TypeError if you try to use it. What it's there for is, basically, so NumPy can use * for elementwise multiplication, and # for matrix multiplication. (The # operator's magic method is even called __matmul__.) But of course other third-party libraries that similarly needed to do two different kinds of multiplication could use # for the second kind.

Adding zero to my complex number class

I'm creating my own complex number class (not using Python's built in one) and I'm running into a problem when I try to add zero to my complex number. For reference this is the error:
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "display.py", line 25, in __init__
t.color(mandelbrot(c).getColor())
File "C:\Users\Joe\Desktop\Python\mandelbrot.py", line 10, in __init__
z = z*z + self.__starting_value
TypeError: unsupported operand type(s) for +: 'int' and 'complex'
Where self.__starting_value is the complex number.
The addition definition goes as follows:
class complex:
def __init__(self, a = 0, b = 0):
self.__real = float(a)
self.__imag = float(b)
def __add__(self, other):
return complex(self.__real + other.__real, self.__imag + other.__imag)
The solution should be simple enough but I am still learning Python and could use the help.
Two problems:
You are only handling addition of a complex object to a complex object, when you want to also be able to handle complex object + int.
When adding in this order (int + custom type), you need to add a reflected addition method (__radd__)
class complex:
def __init__(self, a = 0, b = 0):
self.__real = float(a)
self.__imag = float(b)
def __add__(self, other):
if isinstance(other, complex):
return complex(self.__real + other.__real, self.__imag + other.__imag)
else:
return complex(self.__real + other, self.__imag)
def __radd__(self, other):
return self + other
N.B.: It is considered bad style to shadow built-in names (like complex).
int + complex will first try to useint.__add__. complex + int will first try to use complex.__add__.
You need to implement complex.__radd__. See the related note in Emulating numeric types:
These functions are only called if the left operand does not support the corresponding operation and the operands are of different types.
You will need to handle the case where other is int in both complex.__add__ and complex.__radd__.
__add__ is for addition when the left-hand-side operand is of the type you're writing. When Python fails to call the int __add__ method, it tries __radd__. If you define __radd__ (with the same behavior), you'll get the result you want.

Categories

Resources