Today I found this
>>> type(1)
<class 'sympy.core.numbers.One'>
>>> type(0)
<class 'sympy.core.numbers.Zero'>
>>> type(-1)
<class 'sympy.core.numbers.NegativeOne'>
>>> type(2)
<class 'sympy.core.numbers.Integer'>
I looked the documentation from sympy about those types, but it doesn't say anything about why they exist. Is there a reason to have 3 special singleton classes for -1, 0 and 1?
Edit: I saw this at the SymPy online shell
SymPy keeps track of the precision of Float objects. The default precision is 15 digits. When an expression involving a Float is evaluated, the result will be expressed to 15 digits of precision but those digits (depending on the numbers involved with the calculation) may not all be significant.
SymPy also has a Symbols() function that can define multiple symbols at once. String contains names of variables separated by comma or space. In SymPy's abc module, all Latin and Greek alphabets are defined as symbols. Hence, instead of instantiating Symbol object, this method is convenient.
To evaluate a numerical expression into a floating point number, use evalf . SymPy can evaluate floating point expressions to arbitrary precision. By default, 15 digits of precision are used, but you can pass any number as the argument to evalf .
sympy. Function is for undefined functions. Like if f = Function('f') then f(x) remains unevaluated in expressions. Then f(Symbol('x')) will give a symbolic x**2 + 1 and f(1) will give 2 .
Every number in SymPy is represented by an instance of the class
Number
.
Float
s, Integer
s and Rational
s are subclasses of Number
. Zero
is a
subclass of Integer
.
You can inspect the full class lineage of an object by calling its class's mro
(method resolution order) method:
In [34]: from sympy import S
In [38]: type(S.Zero).mro()
Out[38]:
[sympy.core.numbers.Zero,
sympy.core.numbers.IntegerConstant,
sympy.core.numbers.Integer, <-- Zero is a kind of Integer
sympy.core.numbers.Rational,
sympy.core.numbers.Number,
sympy.core.expr.AtomicExpr,
sympy.core.basic.Atom,
sympy.core.expr.Expr,
sympy.core.basic.Basic,
sympy.core.evalf.EvalfMixin,
object]
These subclasses "teach" SymPy how to manipulate and simplify expressions symbolically. As a example, instances of the Rational class are negated this way:
def __neg__(self):
return Rational(-self.p, self.q)
That is to say, if x
is an instance of Rational
, then -x
causes x.__neg__()
to be called. Meanwhile, instances of the Integer
class, are negated by
def __neg__(self):
return Integer(-self.p)
And if the object is, in particular, an instance of Zero
, then its negation is
defined by:
@staticmethod
def __neg__():
return S.Zero # the negation of Zero is still Zero
Zero
, One
and MinusOne
also implement a _eval_power
method which
"teaches" these objects how to evaluate x
raised to a power (where x
is
Zero
, One
or MinusOne
). For example, Zero
raised to a positive
expression equals itself:
def _eval_power(self, expt):
if expt.is_positive:
return self
...
One
raised to anything equals itself:
def _eval_power(self, expt):
return self
If you peruse the source code for the sympy.core.numbers
module, you'll find
loads of definitions which are in effect teaching SymPy how to do symbolic
arithmetic. It's not too different from what children are taught in math class, except that it is expressed in computer-ese.
You might be wondering why there isn't a special class for every integer.
Integers
besides Zero
, One
and MinusOne
are treated as instances of the
general Integer
class. Their rules of addition and multiplication and so on are laid out there. Unlike Zero
, One
and MinusOne
which are instantated when the module is loaded, other Integers are cached only as needed:
def __new__(cls, i):
...
try:
return _intcache[ival] # <-- return the cached Integer if seen before
except KeyError:
obj = Expr.__new__(cls) # <-- create a new Integer if ival not in _intcache
obj.p = ival
_intcache[ival] = obj
return obj
First, note that type(1)
gave you type(Integer(1))
because SymPy Live wraps integer literals in Integer()
automatically (this is to avoid a gotcha where 1/2
evaluates to 0.5
instead of Rational(1, 2)
). But note that in a regular Python session type(1)
is int
.
There are several objects in SymPy which are implemented as singletons, meaning only one instance will ever exist. You can see these all on the S
object
In [13]: dir(S)
Out[13]:
['Catalan',
'ComplexInfinity',
'Complexes',
'EulerGamma',
'Exp1',
'GoldenRatio',
'Half',
'IdentityFunction',
'ImaginaryUnit',
'Infinity',
'NaN',
'Naturals0',
'NegativeInfinity',
'NegativeOne',
'One',
'Pi',
'Reals',
'Zero',
'__call__',
'__class__',
'__delattr__',
'__doc__',
'__format__',
'__getattr__',
'__getattribute__',
'__hash__',
'__init__',
'__module__',
'__new__',
'__reduce__',
'__reduce_ex__',
'__repr__',
'__setattr__',
'__sizeof__',
'__slots__',
'__str__',
'__subclasshook__',
'_classes_to_install',
'false',
'register',
'true']
(ignore the ones that start with _
; those are Python internal methods)
The reason this is done is that these objects are used a lot. 0, 1, and -1 are very common objects. Every time you write 1/x
it is represented internally as Pow(x, -1)
. x - y
is represented as Add(x, Mul(-1, y))
. For 0, it appears quite often in all sorts of symbolic calculations. 1 is also common. By having a single instance, SymPy enables two optimizations. First, it saves memory. Second, you can compare against these objects using is
comparison, like x is S.One
. Because only one instance can ever exist Integer(1)
is always the same as S.One
.
(also, I should note that some of the objects in S
aren't actually that common, like Catalan
and EulerGamma
. I guess they were added more for convenience than anything)
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With