Explantion of implementation issues and Python syntax in mod_arith1.py
I said a lot of this in class, but I did not write it down before. Hopefully this is a better reference.
Classes are "first-class" objects in programming
language jargon, so they can be returned by functions and assigned to
variables.
I am not going to do anything fancy wih super classes. Leave that
to advance OOP courses. In this case I have AnyMod just to
provide a name. It is a trivial class, with only a name and no internal
logic. All I care is that for any version of the class Mod, an
instance of a
modular number, x, generated from a subclass of AnyMod satisfies
isinstance(x, AnyMod) .
The indentaton in ZMod for class Mod is just the indentation that comes from being defined inside
the function ZMod. Usually subclasses are not define inside other
things. In that case it would have the same indentation as any other
class, like AnyMod (class at the left margin). What makes Mod a subclass is the heading, referring to AnyMod as a super class:
class Mod(AnyMod):
By the way, Python allows multiple inheritance, and there can be a list here, but that is getting way more advanced than I mean to. This is likely to be the only reference to superclasses in the course.
An issue is that the general code in the function ZMod returns an
unnamed class. When code calls ZMod with a literal
modulus value, it makes sense to assign a logical name like
Mod26 = ZMod(26)
ZMod is the creator of classes, returning a class
object. You are used to creating an instance of a class using the name
of an explicit class, like
snake = Animal("Hissy")
so this looks similar to Java
(except without the "new"). In Python, with classes themselves as
objects, what actually happens under the hood is a bit different,
Animal is just a reference to a class object, and the class object
itself serves as the constructor function to make an object of that class.
To continue with our example, to create v = 3 mod 26
v = Mod26(3)
If I want to create
v = x mod m
on the fly, and not remember a name for the class in the middle, I can combine the steps above:
ZMod(m) returns a class of mod m arithmetic, which serves as the
constructor for numbers mod m, so to get the specific number x mod
m:
v = ZMod(m)(x)
__str__ and __repr__
As I say in the JavaVsPython.html, both __repr__ and __str__ are
similar to the toString method in Java. In both languages they
provide default conversions to a string representation for a class of
objects (used implicitly, for instance by print methods). Java's
toString is closer to __str__, since toString in Java has no specified
form for the string returned, similar to __str__. The intention
of __repr__ is much more specific: an expression that the
interpreter could evaluate and turn into an equal object.
Methods toString and __str__ just format the description of an instance
variable state. The format you choose for them is open. For
instance in the modular arithmetic classes I produce strings like "3
mod 5". A human should understand that, but Python could not
automatically parse that string back into a Mod object. On the other
hand Python can parse the string from __repr__ back into a Mod
object. "ZMod(5)(3)" is not an appealing format for humans, but
it does the trick for the Python interpreter, so a string in that form
is returned by __repr__.
You are not likel to use __str__ or __repr__ directly. They
provide the implementation details. The common functions to use
are str and repr, where str(obj) generates a call to obj.__str__() and
repr(obj) generates a call to obj.__repr__(). Again str and repr
are used implicitly in different forms of printing.
Type mixing in operations
For methods like __add__, the idea is to define the standard infix
operators: If x and y are defined as modular numbers, x+y
generates a call to x.__add__(y). In this actual method
call the actual parameters x and y map to the formal parameters in
__add__, self and other. Similarly, C++ has something like
operator '+' as a method name to define the infix '+' operation for a
particular class of object. (I'm probably not exactly right in
remembering the syntax.)
The fancier Mod package,
mod_arith1.py, allows Mod numbers to be mixed in operations with other
things - at this point just the two Python 2.X forms of integers.
I could have written the __add__ method:
def __add__(self, other): # used by + infix operand
'Return self + other, if defined'
if isinstance(other, (int, long)):
return Mod(self.value + other)
if isinstance(other, AnyMod) and other.modulus() == m:
# other is the same kind of Mod object
return Mod(self.value + other.value)
return NotImplemented
A
bunch of other operations of Mod objects could use similar code,
testing if the other operand can be converted to the same kind of
modular object, and then actually getting an integer representative of
the converted object.
This code could be repeated over and over - a red flag in
programming. In particular we might come up later with other classes
that can be compatibly be operated on with Mod objects, and then my code
would need to be changed in multiple places.
Instead, I followed good programming practice and came up with a way to isolate these operations:
- Tests for convertability ( use _isCompatibleType)
- If an object is convertible, find an integer representative to use with calculations (use __int__) .
The __int__ method is something you would not call directly. If
you use int(x), it is evaluated using x._int__(), if defined. In
my Mod classes the__int__ method just returns self.value.
With
these methods set up, the arithmetic operation methods became shorter,
and did not need to explicitly code what can be converted or how. In
particular, the __add__ method I do use:
def __add__(self, other): # used by + infix operand
'Return self + other, if defined'
if self._isCompatibleType(other):
return Mod(self.value + int(other))
return NotImplemented