All right after playing around with it some more I think I've got it.. when I first asked the question I didn't know about operator overloading.
So, what's going on in this python session?
>>> from sympy import *
>>> x = Symbol(x)
>>> x + x
2*x
It turns out there's nothing special about how the interpreter evaluates the expression; the important thing is that python translates
x + x
into
x.__add__(x)
and Symbol inherits from the Basic class, which defines __add__(self, other)
to return Add(self, other)
. (These classes are found in sympy.core.symbol, sympy.core.basic, and sympy.core.add if you want to take a look.)
So as Jerub was saying, Symbol.__add__()
has a decorator called _sympifyit
which basically converts the second argument of a function into a sympy expression before evaluating the function, in the process returning a function called __sympifyit_wrapper
which is what I saw before.
Using objects to define operations is a pretty slick concept; by defining your own operators and string representations you can implement a trivial symbolic algebra system quite easily:
symbolic.py --
class Symbol(object):
def __init__(self, name):
self.name = name
def __add__(self, other):
return Add(self, other)
def __repr__(self):
return self.name
class Add(object):
def __init__(self, left, right):
self.left = left
self.right = right
def __repr__(self):
return self.left + '+' + self.right
Now we can do:
>>> from symbolic import *
>>> x = Symbol('x')
>>> x+x
x+x
With a bit of refactoring it can easily be extended to handle all basic arithmetic:
class Basic(object):
def __add__(self, other):
return Add(self, other)
def __radd__(self, other): # if other hasn't implemented __add__() for Symbols
return Add(other, self)
def __mul__(self, other):
return Mul(self, other)
def __rmul__(self, other):
return Mul(other, self)
# ...
class Symbol(Basic):
def __init__(self, name):
self.name = name
def __repr__(self):
return self.name
class Operator(Basic):
def __init__(self, symbol, left, right):
self.symbol = symbol
self.left = left
self.right = right
def __repr__(self):
return '{0}{1}{2}'.format(self.left, self.symbol, self.right)
class Add(Operator):
def __init__(self, left, right):
self.left = left
self.right = right
Operator.__init__(self, '+', left, right)
class Mul(Operator):
def __init__(self, left, right):
self.left = left
self.right = right
Operator.__init__(self, '*', left, right)
# ...
With just a bit more tweaking we can get the same behavior as the sympy session from the beginning.. we'll modify Add
so it returns a Mul
instance if its arguments are equal. This is a bit trickier since we have get to it before instance creation; we have to use __new__()
instead of __init__()
:
class Add(Operator):
def __new__(cls, left, right):
if left == right:
return Mul(2, left)
return Operator.__new__(cls)
...
Don't forget to implement the equality operator for Symbols:
class Symbol(Basic):
...
def __eq__(self, other):
if type(self) == type(other):
return repr(self) == repr(other)
else:
return False
...
And voila. Anyway, you can think of all kinds of other things to implement, like operator precedence, evaluation with substitution, advanced simplification, differentiation, etc., but I think it's pretty cool that the basics are so simple.