.. include:: definitions.def Basics of expressions in SymPy ============================== SymPy is all about construction and manipulation of *expressions*. By the term expression we mean mathematical expressions represented in the Python language using SymPy's classes and objects. Expressions may consist of symbols, numbers, functions and function applications (and many other) and operators binding them together (addiction, subtraction, multiplication, division, exponentiation). Suppose we want to construct an expression for `x + 1`:: >>> x = Symbol('x') >>> x + 1 x + 1 >>> type(_) Entering ``x + 1`` gave us an instance of :class:`Add` class. This expression consists of a symbol (``x``), a number (``1``) and addition operator, which is represented by the topmost class (:class:`Add`). This was the simplest way of entering an expression for `x + 1`. We could also enter:: >>> y = Symbol('y') >>> x - y + 17 + y - 16 + sin(pi) x + 1 In this case SymPy automatically rewrote the input expression and gave its canonical form, which is ``x + 1`` once again. This is a very important behavior: all expressions are subject to automatic evaluation, during which SymPy tries to find a canonical form for expressions, but it doesn't apply "heroic" measures to achieve this goal. For example the following expression:: >>> (x**2 - 1)/(x - 1) 2 x - 1 ────── x - 1 is left unsimplified. This is because automatic canonicalization would lose important information about this expression (`x \not= 1`). We can use :func:`cancel` remove common factors from the numerator and the denominator:: >>> cancel(_) x + 1 SymPy never applies any transformations automatically that could cause information loss or that would result in results that are valid only almost everywhere. Consider the following expression:: >>> log(x*y) log(x⋅y) We know that `\log(x y)` is equivalent to `\log x + \log y` and there is :func:`expand` that is supposed be able to do this:: >>> expand(_) log(x⋅y) Unfortunately nothing interesting happened. This is because the formula stated above is not universally valid, e.g.:: >>> log((-2)*(-3)) log(6) >>> log(-2) + log(-3) log(2) + log(3) + 2⋅ⅈ⋅π It is possible to ignore such cases and expand forcibly:: >>> expand(log(x*y), force=True) log(x) + log(y) Many other expression manipulation function also support ``force`` option. Usually a better way is to assign additional knowledge with an expression:: >>> var('a,b', positive=True) (a, b) >>> log(a*b) log(a⋅b) >>> expand(_) log(a) + log(b) In this case ``force=True`` wasn't necessary, because we gave sufficient information to :func:`expand` so that it was able to decide that the expansion rule is valid universally for this expression. Arithmetic operators -------------------- Arithmetic operators ``+``, ``-``, ``*``, ``/``, ``**`` are mapped to combinations of three core SymPy's classes: :class:`Add`, :class:`Mul` and :class:`Pow`, and work the following way: * ``x + y`` uses :class:`Add` class and ``__add__`` method:: >>> x + y x + y >>> type(_) >>> x.__add__(y) x + y >>> type(_) >>> Add(x, y) x + y >>> type(_) * ``x - y`` uses :class:`Add` and :class:`Mul` classes, and ``__sub__`` method:: >>> x - y x - y >>> type(_) >>> __.args (-y, x) >>> type(_[0]) >>> x.__sub__(y) x - y >>> type(_) >>> __.args (-y, x) >>> type(_[0]) >>> Add(x, -y)) x - y >>> type(_) >>> __.args (-y, x) >>> type(_[0]) * ``x*y`` uses :class:`Mul` class and ``__mul__`` method:: >>> x*y x*y >>> type(_) >>> x.__mul__(y) x*y >>> type(_) >>> Mul(x, y) x*y >>> type(_) * ``x/y`` uses :class:`Pow` and :class:`Mul` classes and ``__div__`` method:: >>> x/y x ─ y >>> type(_) >>> __.args ⎛ 1⎞ ⎜x, ─⎟ ⎝ y⎠ >>> type(_[1]) >>> x.__div__(y) x ─ y >>> type(_) >>> __.args ⎛ 1⎞ ⎜x, ─⎟ ⎝ y⎠ >>> type(_[1]) >>> Mul(x, 1/y) x ─ y >>> type(_) >>> __.args ⎛ 1⎞ ⎜x, ─⎟ ⎝ y⎠ >>> type(_[1]) * ``x**y`` uses :class:`Pow` class and ``__pow__`` method:: >>> x**y y x >>> type(_) >>> x.__pow__(y) y x >>> type(_) >>> Pow(x, y) y x >>> type(_) When the first argument is not an instance SymPy's class, e.g. as in ``1 - x``, then Python falls back to ``__r*__`` methods, which are also implemented in all SymPy's classes:: >>> (1).__sub__(x) NotImplemented >>> x.__rsub__(1) -x + 1 >>> 1 - x -x + 1 Tasks ~~~~~ 1. Construct an expression for `1 + x + x^2 + \ldots + x^{10}`. Can you construct this expression in a different way? Write a function that could generate an expression for `1 + x + x^2 + \ldots + x^n` for any integer `n >= 0`. Extend this function to allow `n < 0`. (:ref:`solution `) 2. Write a function that can compute nested powers, e.g. `x^x`, `x^{x^x}` and so on. The function should take two parameters: an expression and a positive integer `n` that specifies the depth. (:ref:`solution `) Building blocks of expressions ------------------------------ Expressions can consist of instances of subclasses of :class:`Expr` class. This includes: * numbers:: >>> Integer(2) 2 >>> Rational(1, 2) 1/2 >>> Float("1e-1000") 1.00000000000000e-1000 * symbols:: >>> Symbol('x') x >>> Dummy('y') y * numer symbols:: >>> pi π >>> E ℯ >>> Catalan Catalan * functions:: >>> Function('f') f >>> sin sin >>> cos cos * function applications:: >>> Function('f')(x) f(x) >>> sin(x) sin(x) >>> cos(x) cos(x) * operators:: >>> Add(x, y, z) x + y + z >>> Mul(x, y, z) x⋅y⋅z >>> Pow(x, y) y x >>> Or(x, y, z) x ∨ y ∨ z >>> And(x, y, z) x ∧ y ∧ z * unevaluated operators:: >>> Derivative(1/x, x) d ⎛1⎞ ──⎜─⎟ dx⎝x⎠ >>> Integral(1/x, x) ⌠ ⎮ 1 ⎮ ─ dx ⎮ x ⌡ >>> Sum(1/k, (k, 1, n)) n ___ \ ` \ 1 ) ─ / k /__, k = 1 * other:: >>> Poly(x**2 + y, x) Poly(x**2 + y, x, domain='ZZ[y]') >>> RootOf(z**5 + z + 3, 2) ⎛ 5 ⎞ RootOf⎝z + z + 3, 2⎠ This list isn't at all complete and we included only few classes that SymPy implements that can be used as expression building blocks. Besides those, SymPy has also very many classes that represent entities that can't be used for constructing expressions, but can be useful as containers of expressions or as utilities for expression building blocks. Tasks ~~~~~ 1. Expressions implement a :func:`doit` method. For most types expressions it doesn't do anything useful, but in the case of unevaluated operators, it executes an action assigned to to an unevaluated operator (it differentiates, integrates, etc.). Take advantage of :func:`doit` and write a function that generates integral tables for a few polynomials, rational functions and elementary functions. (:ref:`solution `) Foreign types in SymPy ---------------------- SymPy internally expects that all objects it works with are instances of subclasses of :class:`Basic` class. So why ``x + 1`` works without raising an exception? The number ``1`` is not a SymPy's type, but:: >>> type(1) it's a built-in type. SymPy implements :func:`sympify` function for the task of converting foreign types to SymPy's types (yes, Python's built-in types are also considered as foreign). All SymPy's classes, methods and functions use :func:`sympify` and this is the reason why you can safely write ``x + 1`` instead of more verbose and less convenient ``x + Integer(1)``. Note that not all functions return instances of SymPy's types. Usually, if a function is supposed to return a property of an expression, it will use built-in Python's types, e.g.:: >>> Poly(x**2 + y).degree(y) 1 >>> type(_) Now see what :func:`sympify` can do. Let's start with built-ins:: >>> sympify(1) 1 >>> type(_) >>> sympify(117) 117 >>> type(_) >>> sympify(0.5) 0.500000000000000 >>> type(_) >>> from fractions import Fraction >>> sympify(Fraction(1, 2)) 1/2 >>> type(_) SymPy implements explicit sympification rules, heuristics based on ``__int__``, ``__float__`` and other attributes, and in the worst case scenario it falls back to parsing string representation of an object. This usually works fine, but sometimes :func:`sympify` can be wrong:: >>> from gmpy import mpz, mpq >>> sympify(mpz(117)) 117.000000000000 >>>> type(_) >>> sympify(mpq(1, 2)) 0.500000000000000 >>>> type(_) This happens because :func:`sympify` doesn't know about either ``mpz`` or ``mpq``, and it first looks for ``__float__`` attribute, which is implemented by both those types. Getting float for exact value isn't very useful so let's extend :func:`sympify` and add support for ``mpz``. The way to achieve this is to add a new entry to ``converter`` dictionary. ``converter`` takes types as keys and sympification functions as values. Before we extend this ``dict``, we have to resolve a little problem with ``mpz``:: >>> mpz which isn't a type but a function. We can use a little trick here and take the type of some ``mpz`` object:: >>> type(mpz(1)) Let's now add an entry to ``converter`` for ``mpz``:: >>> from sympy.core.sympify import converter >>> def mpz_to_Integer(obj): ... return Integer(int(obj)) ... ... >>> converter[type(mpz(1))] = mpz_to_Integer We could use ``lambda`` as well. Now we can sympify ``mpz``:: >>> sympify(mpz(117)) 117 >>> type(_) Similar things should be done for ``mpq``. Let's try one more type:: >>> import numpy >>> ar = numpy.array([1, 2, 3]) >>> sympify(ar) >>> sympify(ar) Traceback (most recent call last): ... SympifyError: SympifyError: "could not parse u'[1 2 3]'" :func:`sympify` isn't aware of ``numpy.ndarray`` and heuristics didn't work, so it computed string representation of ``ar`` using :func:`str` and tried to parse is, which failed because:: >>> str(ar) [1 2 3] We might be tempted to add support for ``numpy.ndarray`` to :func:`sympify` by treating NumPy's arrays (at least a subset of) as SymPy's matrices, but matrices aren't sympifiable:: >>> Matrix(3, 3, lambda i, j: i + j) ⎡0 1 2⎤ ⎢ ⎥ ⎢1 2 3⎥ ⎢ ⎥ ⎣2 3 4⎦ >>> sympify(_) Traceback (most recent call last): ... SympifyError: SympifyError: 'Matrix cannot be sympified' We will explain this odd behavior later. Tasks ~~~~~ 1. Add support for ``mpq`` to :func:`sympify`. (:ref:`solution `) 2. SymPy implements :class:`Tuple` class, which provides functionality of Python's built-in ``tuple``, but is a subclass of :class:`Basic`. Take advantage of this and make :func:`sympify` work for row NumPy arrays, for which it should return instances of :class:`Tuple`. Raise :exc:`SympifyError` for other classes of arrays. (:ref:`solution `) The role of symbols ------------------- Let's now talk about the most important part of expressions: symbols. Symbols are placeholders, abstract entities that can be filled in with whatever content we want (unless there are explicit restrictions given). For example in expression ``x + 1`` we have one symbol ``x``. Let's start fresh Python's interpreter and issue:: >>> from sympy import * >>> init_printing() We want to start work with our very advanced ``x + 1`` expression, so we may be tempted to simply write:: >>> x + 1 Traceback (most recent call last): ... NameError: name 'x' is not defined For users that come from other symbolic mathematics systems, this behavior may seem odd, because in those systems, symbols are constructed implicitly when necessary. In general purpose programming language like Python, we have to define all objects we want to use before we actually use them. So, the first thing we have to always do is to construct symbols and assign them to Python's variables:: >>> x = Symbol('x') >>> x + 1 x + 1 Now it worked. Symbols are independent of variables, so nothing prevents you from issuing:: >>> t = Symbol('a') Well, besides taste. It's also perfectly valid to create symbols containing special characters:: >>> Symbol('+') + ``_`` and ``^`` characters in symbols have special meaning and are used to denote subscripts and superscripts, respectively:: >>> Symbol('x_1') x₁ >>> Symbol('x^1') x¹ If you need more symbols in your expression, you have to define and assign them all before using them. Later you can reuse existing symbols for other purposes. To make life easier, SymPy provides several methods for constructing symbols. The most low-level method is to use :class:`Symbol` class, as we have been doing it before. However, if you need more symbols, then your can use :func:`symbols`:: >>> symbols('x,y,z') (x, y, z) It takes a textual specification of symbols and returns a ``tuple`` with constructed symbols. :func:`symbols` supports several syntaxes and can make your life much simpler, when it comes to constructing symbols. First of all, commas can be followed by or completely replaced by whitespace:: >>> symbols('x, y, z') (x, y, z) >>> symbols('x y z') (x, y, z) If you need indexed symbols, then use range syntax:: >>> symbols("x:5") (x₀, x₁, x₂, x₃, x₄) >>> symbols('x5:10') (x₅, x₆, x₇, x₈, x₉) You can also create consecutive symbols with lexicographic syntax:: >>> symbols('a:d') (a, b, c, d) Note that range syntax simulates :func:`range`'s behavior, so it is exclusive, lexicographic syntax is inclusive, because it makes more sense in this case. When we issue:: >>> symbols('u,v') (u, v) we may be tempted to use ``u`` and ``v``:: >>> u Traceback (most recent call last): ... NameError: name 'u' is not defined >>> v Traceback (most recent call last): ... NameError: name 'v' is not defined We got :exc:`NameError`, because we constructed those symbols, but we didn't assign them to any variables. This solves the problem:: >>> u, v = symbols('u,v') >>> u, v u, v but is a little redundant, because we have to repeat the same information twice. To save time and typing effort, SymPy has another function :func:`var` for constructing symbols, which has exactly the same syntax and semantics as :func:`symbols`, but it also injects constructed symbols into the global namespace, making this function very useful in interactive sessions:: >>> del u, v >>> var('u,v) (u, v) >>> u + v u + v We don't allow to use :func:`var` in SymPy's library code. There is one more way of constructing symbols, which is related to indexed symbols. Sometimes we don't know in advance how many symbols will be required to solve a certain problem. For this case, SymPy has :func:`numbered_symbols` generator:: >>> X = numbered_symbols('x') >>> X.next() x₀ >>> [ X.next() for i in xrange(5) ] [x₁, x₂, x₃, x₄, x₅] Tasks ~~~~~ 1. Implement a function that would generate an expression for `x_1^1 + x_2^2 + \ldots + x_n^n`. This function would take two arguments: base name for indexed symbols and integer exponent `n >= 1`. What's the best approach among the four presented above? (:ref:`solution `) Obtaining parts of expressions ------------------------------ We already know how to construct expressions, but how to get parts of complex expressions? The most basic and low-level way of decomposing expressions is to use ``args`` property:: >>> x + y + 1 x + y + 1 >>> _.args (1, y, x) >>> map(type) [, , ] ``args`` always gives a ``tuple`` of instances of SymPy's classes. One should notice the weird order of elements, which doesn't match printing order. This happens for classes that in which order of arguments is insignificant. The most notable examples of such class are :class:`Add` and :class:`Mul` (for commutative part). In this particular case we can use :func:`as_ordered_terms` method to get ``args`` in printing order:: >>> (x + y + 1).as_ordered_terms() [x, y, 1] When dealing which classes that have fixed order of arguments, printing order and ``args`` order match:: >>> Derivative(sin(x), x, x) 2 d ─────(sin(x)) dx dx >>> _.args (sin(x), x, x) Lets suppose that :class:`Cls` represents any SymPy's class and ``expr`` is an instance of this class (``expr = Cls()``). Then the following holds:: Cls(*expr.args) == expr This is very useful invariant, because we can easily decompose, modify and rebuild expressions of various kinds in SymPy exactly the same way. This invariant is being used in all functions that manipulation expressions. Let's now use ``args`` to something a little more interesting than simple decomposition of expressions. Working with expressions, one may be interested in the depth of such expressions. By viewing expressions as n-ary trees, by depth we understand the longest path in a tree. Trees consist of branches and leafs. In SymPy, leafs of expressions are instances of subclasses of :class:`Atom` class (numbers, symbols, special constants):: >>> Integer(10) 10 >>> isinstance(_, Atom) True >>> pi π >>> isinstance(_, Atom) True Atoms can be also recognized by the fact that their ``args`` are empty. Note, however, that this is an implementation detail, and one should use either :func:`isinstance` built-in function or ``is_Atom`` property to recognize atoms properly. Everything else than an :class:`Atom` is a branch. Let's implement :func:`depth` function: .. literalinclude:: python/depth.py The implementation is straightforward. First we check if the input expression is an atom. In this case we return ``1`` and terminate recursion. Otherwise :func:`depth` recurses for every argument of ``expr`` and returns ``1`` plus maximum of depths of all branches. Let's see :func:`depth` in action:: >>> depth(x) 1 >>> depth(x + 1) 2 >>> depth(x + sin(x)) 3 >>> depth(x + sin(x) + sin(cos(x))) 4 All those examples work as expected. However, not everything is perfect with this function. Let's look at the following phenomenon:: >>> depth(Integer(117)) 1 >>> depth(117) Traceback (most recent call last): ... AttributeError: 'int' object has no attribute 'args' ``117`` is an instance of Python's built-in type :class:`int`, but this type is not a subclass of :class:`Atom`, so Python choses the other branch in :func:`depth` and this must fail. Before the last example we pass only instances of SymPy's expression to :func:`depth`. If we want :func:`depth` to work also for non-SymPy types, we have to sympify ``expr`` with :func:`sympify` before using it. Tasks ~~~~~ 1. Change :func:`depth` so that it sympifies its input argument. Rewrite :func:`depth` so that is calls :func:`sympify` only once. (:ref:`solution `) 2. Add support for iterable containers to :func:`depth`. Containers should be treated as branches and have depth defined the same way. (:ref:`solution `) Immutability of expressions --------------------------- Expressions in SymPy are immutable and cannot be modified by an in-place operation. This means that a function will always return an object, and the original expression will not be modified. Consider the following code:: >>> var('x,y,a,b') (x, y, a, b) >>> original = 3*x + 4*y >>> modified = original.subs({x: a, y: b}) >>> original 3*x + 4*y >>> modified 3*a + 4*b The output shows that the :func:`subs` method gave a new expression with symbol ``x`` replaced with symbol ``a`` and symbol ``y`` replaced with symbol ``b``. The original expression wasn't modified. This behavior applies to all classes that are subclasses of :class:`Basic`. An exception to immutability rule is :class:`Matrix`, which allows in-place modifications, but it is not a subclass of :class:`Basic`:: >>> Matrix.mro() [, ] Be also aware of the fact that SymPy's symbols aren't Python's variables (they just can be assigned to Python's variables), so if you issue:: >>> u = Symbol('u') >>> v = u >>> v += 1 >>> v u + 1 then in-place operator ``+=`` constructed an new instance of :class:`Add` and left the original expression stored in variable ``u`` unchanged:: >>> u u For efficiency reason, any in-place operator used on elements of a matrix, modifies the matrix in-place and doesn't waste memory for unnecessary copies. Tasks ~~~~~ 1. This is the first time we used :func:`subs`. This is a very important method and we will talk more about it later. However, we can also use :func:`subs` to generate some cool looking expressions. Start with ``x**x`` expression and substitute in it ``x**x`` for ``x``. What do you get? (make sure you use pretty printer) Can you achieve the same effect without :func:`subs`? (:ref:`solution `) Comparing expressions with ``==`` --------------------------------- Consider the following two expressions:: >>> f = (x + 1)**2 >>> f 2 (x + y) >>> g = x**2 + 2*x + 1 >>> g 2 x + 2⋅x + 1 We should remember from calculus 101 that those two expressions are equivalent, because we can use binomial theorem to expand ``f`` and we will get ``g``. However in SymPy:: >>> f == g False This is correct result, because SymPy implements structural understanding of ``==`` operator, not semantic. So, for SymPy ``f`` and ``g`` are very different expressions. What to do if we have two variables and we want to know if their contents are equivalent, but not necessarily structurally equal? There is no simple answer to this question in general. In the particular case of ``f`` and ``g``, it is sufficient to issue:: >>> expand(f) == expand(g) True or, based on `f = g \equiv f - g = 0` equivalence:: >>> expand(f - g) == 0 True In case of more complicated expression, e.g. those involving elementary or special functions, this approach may be insufficient. For example:: >>> u = sin(x)**2 - 1 >>> v = cos(x)**2 >>> u == v False >>> expand(u - v) == 0 False In this case we have to use more advanced term rewriting function:: >>> simplify(u - v) == 0 True The meaning of expressions -------------------------- Expressions don't have any meaning assigned to them by default. Thus `x + 1` is simply an expression, not a function or a univariate polynomial. Meaning is assigned when we use expressions in a context, e.g.:: >>> div(x**2 - y, x - y) ⎛ 2 ⎞ ⎝x + y, y - y⎠ In this case, ``x**2 - y`` and ``x - y`` where treated as multivariate polynomials in variables ``x`` and ``y`` (in this order). We could change this understanding and ask explicitly for polynomials in variables ``y`` and ``x``. This makes :func:`div` return a different result:: >>> div(x**2 - y, x - y, y, x) ⎛ 2 ⎞ ⎝1, x - x⎠ Quite often SymPy is capable of deriving the most useful understanding of expressions in a given context. However, there are situations when expressions simply don't carry enough information to make SymPy perform computations without telling it explicitly what to do:: >>> roots(x**2 - y) Traceback (most recent call last): ... PolynomialError: multivariate polynomials are not supported Here we have to tell :func:`roots` in which variable roots should be computed:: >>> roots(x**2 - y, x) ⎧ ⎽⎽⎽ ⎽⎽⎽ ⎫ ⎨-╲╱ y : 1, ╲╱ y : 1⎬ ⎩ ⎭ Of course the choice of ``y`` is also a valid one, assuming that this is what you really want. This of course doesn't apply only to polynomials. Turning strings into expressions -------------------------------- Suppose we saved the following expression:: >>> var('x,y') >>> expr = x**2 + sin(y) + S(1)/2 >>> expr 2 1 x + sin(y) + ─ 2 by printing it with :func:`sstr` printer and storing to a file:: >>> sstr(expr) x**2 + sin(y) + 1/2 >>> with open("expression.txt", "w") as f: ... f.write(_) ... ... We used this kind of printer because we wanted the file to be fairly readable. Now we want to restore the original expression. First we have to read the text form from the file:: >>> with open("expression.txt") as f: ... text_form = f.read() ... ... >>> text_form x**2 + sin(y) + 1/2 >>> type(_) We could try to try to use :func:`eval` on ``text_form`` but this doesn't give expected results:: >>> eval(text_form) 2 x + sin(y) + 0.5 This happens because ``1/2`` isn't understood by Python as rational number and is equivalent to a problem we had when entering expressions of this kind in interactive sessions. To overcome this problem we have to use :func:`sympify`, which implements :mod:`tokenize`--based parser that allows us to handle this issue:: >>> sympify(text_form) 2 1 x + sin(y) + ─ 2 >>> _ == expr True Let's now consider a more interesting problem. Suppose we define our own function:: >>> class my_func(Function): ... """Returns zero for integer values. """ ... ... @classmethod ... def eval(cls, arg): ... if arg.is_Number: ... return 2*arg ... ... This function gives twice the input argument if the argument is a number and doesn't do anything for all other classes of arguments:: >>> my_func(117) 234 >>> my_func(S(1)/2) 1 >>> my_func(x) my_func(x) >>> _.subs(x, 2.1) 4.20000000000000 >>> my_func(1) + 1 3 Let's create an expression that contains :func:`my_func`:: >>> expr = my_func(x) + 1 >>> expr my_func(x) + 1 >>> _.subs(x, 1) 3 Now we will print it using :func:`sstr` printer and sympify the result:: >>> sympified = sympify(sstr(expr)) >>> sympified my_func(x) + 1 We can use :func:`subs` method to quickly verify the expression is correct:: >>> sympified.subs(x, 1) my_func(1) + 1 This is not exactly what we expected. This happens because:: >>> expr == sympified False >>> expr.args (1, my_func(x)) >>> type(_[1]) is my_func True >>> sympified.args (1, my_func(x)) >>> type(_[1]) is my_func False :func:`sympify` evaluates the given string in the context of ``from sympy import *`` and is not aware of user defined names. We can explicitly pass a mapping between names and values to it:: >>> sympify(sstr(expr), {'my_func': my_func}) my_func(x) + 1 >>> _.subs(x, 1) 3 This time we got the desired result. This shows that we have to be careful when working with expressions encoded as strings. This happens to be even more tricky when we put assumptions on symbols. Do you remember the example in which we tried to expand `\log(a b)`? Lets do it once again:: >>> var('a,b', positive=True) (a, b) >>> log(a*b).expand() log(a) + log(b) This worked as previously. However, let's now print `\log(a b)`, sympify the resulting string and expand the restored expression:: >>> sympify(sstr(log(a*b))).expand() log(a⋅b) This didn't work, because :func:`sympify` doesn't know what ``a`` and ``b`` are, so it assumed that those are symbols and it created them implicitly. This issue is similar to what we already experienced with :func:`my_func`. The most reliable approach to storing expression is to use :mod:`pickle` module. In the case of `\log(a b)` it works like this:: >>> import pickle >>> pickled = pickle.dumps(log(a*b)) >>> expr = pickle.loads(pickled) >>> expr.expand() log(a) + log(b) Unfortunately, due to :mod:`pickle`'s limitations, this doesn't work for user defined functions like :func:`my_func`:: >>> pickle.dumps(my_func(x)) Traceback (most recent call last): ... PicklingError: Can't pickle my_func: it's not found as __main__.my_func Tasks ~~~~~ 1. Construct a polynomial of degree, let's say, 1000. Use both techniques to save and restore this expression. Compare speed of those approaches. Verify that the result is correct. (:ref:`solution `)