Expression trees: func, args, srepr

SymPy · Expert · 13 min

Writing your own transformations, or debugging surprising results, requires seeing the tree SymPy really built.

Every SymPy expression is a tree. Each node has a func (its class, like Add, Mul, Pow, sin) and a tuple of args. Leaves (Symbol, Integer, Rational) have empty args. The key invariant is:

expr.func(*expr.args) == expr

so you can rebuild any node from its parts, which is how recursive transformations are written. srepr(expr) prints the full tree.

The canonical forms are worth knowing: there is no Sub or Div. x - y is Add(x, Mul(-1, y)), and x/y is Mul(x, Pow(y, -1)). Arguments of Add and Mul are stored in a canonical order, so x + y and y + x are the same object, but you should never rely on a particular order of args.

Tools for working with trees:

  • preorder_traversal(e) / postorder_traversal(e) iterate over nodes.
  • e.atoms(Symbol), e.atoms(sin) collect nodes of given types.
  • e.has(x) tests containment; e.find(query) searches.
  • e.xreplace({old: new}) replaces exact subtrees with no mathematical reasoning, which is fast and predictable; subs is smarter and slower.
  • e.replace(pattern, func) rewrites with types or Wild patterns.

Because expressions are immutable and hashable, they work as dict keys and are cached aggressively. Structural == compares these trees exactly, which is fast but knows no algebra.

Example

from sympy import symbols, srepr, sin, Add, preorder_traversal, Symbol

x, y = symbols("x y")
e = x - y / 2 + sin(x)

print(srepr(x - y))   # Add(Symbol('x'), Mul(Integer(-1), Symbol('y')))
print(e.func, len(e.args))           # <class 'sympy.core.add.Add'> 3
print(e.func(*e.args) == e)          # True

print(sorted(map(str, e.atoms(Symbol))))  # ['x', 'y']
print(e.has(sin))                         # True
print(len(list(preorder_traversal(e))))   # 7

def rebuild(node):
    if not node.args:
        return node
    return node.func(*[rebuild(a) for a in node.args])

print(rebuild(e) == e)            # True: rebuilt from parts
print(e.xreplace({sin(x): 0}))       # x - y/2
print((x + 2*x*y).subs(2*x, y))      # x + y**2

Remember

  • Every node is func(*args); rebuild with expr.func(*new_args)
  • Subtraction and division are Add with -1 and Pow with -1
  • xreplace is literal; subs is mathematical; atoms/has/traversals find nodes

Watch out for

  • Relying on the order of Add or Mul args, which is canonical but not what you typed.

Check yourself

  1. What is srepr(x / y)?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: docs.sympy.org/latest/tutorials/intro-tutorial/manipulation.html