Idioms and Antipatterns

Joachim Durchholz edited this page Apr 11, 2015 · 17 revisions
Clone this wiki locally

Idioms and Antipatterns

Here is a list of idioms and antipatterns in SymPy. You should use the former, and avoid the latter.

Please edit this page with any idioms or antipatterns that you know of. Don't worry about organization too much.

For most antipatterns, try to add an example of how it should be done instead.

Idioms

Rebuilding an expression

Rebuild an expression using expr.func(*expr.args).

Args invariants

When creating custom subclasses of Basic or Expr, make sure that it satisfies expr == expr.func(*expr.args). Many very basic functions in SymPy rely on this idiom to modify an expression and rebuild it, such as subs.

Subclassing

TODO: How to properly subclass Basic and Expr (and when to).

Antipatterns

Wild and match

When using Wild, be sure to use the exclude keyword to make the pattern deterministic.

Don't

x, y = Symbols("x y")
a, b = Wild('a'), Wild('b')
(2*x + 3*y).match(a*x + b*y)

Do

x, y = Symbols("x y")
a, b = Wild('a', exclude=[x, y]), Wild('b', exclude=[x, y])
(2*x + 3*y).match(a*x + b*y)

Reason

Without the exclude pattern, you may get matches that are technically correct, but not what you wanted. This is especially likely if the expression you are matching doesn't actually match your pattern. For example, using the above without exclude:

>>> (2 + 3*y).match(a*x + b*y)
{a: 2/x, b: 3}

This is technically correct, because (2/x)*x + 3*y == 2 + 3*y, but you probably wanted it to not match at all. The issue is that you really didn't want a and b to include x and y, and the exclude parameter lets you specify exactly this. With the exclude parameter, the above match gives None, meaning it did not match.

Strings as input

Don't use strings as input to functions. Rather, create the objects symbolically using Symbols and the appropriate SymPy functions, and manipulate them.

Don't

>>> simplify("(x**2 + x)/x")
x + 1

Do

>>> x = Symbol('x')
>>> simplify((x**2 + x)/x)
x + 1

Reason

Support for string input is in many ways accidental. It only happens because functions call sympify() on their input to ensure that it is a SymPy object, and sympify() translates strings. Support for this may go away in a future version.

There are many disadvantages to using strings:

  • They are not explicit. They make code much harder to read.

  • sympify() automatically turns all undefined names into Symbols of Functions, so if you have a typo, the string will still parse correctly, but the output will not be what you expect. For example

>>> expand_trig("sine(x + y)")
sine(x + y)
>>> expand_trig(sine(x + y))
Traceback (most recent call last):
File "<ipython-input-41-a8617eceeca5>", line 1, in <module>
expand_trig(sine(x + y))
NameError: name 'sine' is not defined
>>> expand_trig(sin(x + y))
sin(x)*cos(y) + sin(y)*cos(x)

In the first example, sine, a typo for sin is parsed into Function("sine"), and it appears that expand_trig cannot handle it. In the second case, we immediately get an error from the undefined name sine, and fixing our typo, we see that expand_trig can indeed do what we want.

  • See also the section on S('x') in the "Creating Symbols" section below. Symbol names can contain any character, including things that aren't valid Python. But using strings as input will pass the string to sympify, even the argument to the function requires a Symbol. For example,
>>> x = Symbol('2x')
>>> solve(x - 1, x)
[1]
>>> solve(x - 1, '2x')
Traceback (most recent call last):
  File "<ipython-input-12-71e92ee34319>", line 1, in <module>
    solve(Symbol('2d') - 1, '2d ')
  File "/Users/aaronmeurer/Documents/Python/sympy/sympy/sympy/solvers/solvers.py", line 672, in solve
    f, symbols = (_sympified_list(w) for w in [f, symbols])
  File "/Users/aaronmeurer/Documents/Python/sympy/sympy/sympy/solvers/solvers.py", line 672, in <genexpr>
    f, symbols = (_sympified_list(w) for w in [f, symbols])
  File "/Users/aaronmeurer/Documents/Python/sympy/sympy/sympy/solvers/solvers.py", line 663, in _sympified_list
    return map(sympify, w if iterable(w) else [w])
  File "/Users/aaronmeurer/Documents/Python/sympy/sympy/sympy/core/sympify.py", line 297, in sympify
    raise SympifyError('could not parse %r' % a, exc)
SympifyError: Sympify of expression 'could not parse u'2d'' failed, because of exception being raised:
SyntaxError: invalid syntax (<string>, line 1)
  • If you use strings, syntax errors won't be caught until the line is run. If you build up the expressions, syntax errors will be caught when Python compiles the script before any of it runs.

  • In code editors that do syntax highlighting, strings will be highlighted all one color, whereas Python expressions will be highlighted according to their actual content.

  • As mentioned, they are not officially supported or tested, and so may go away at any time.

Creating Symbols

There are several ways to create Symbols:

  1. x = Symbol('x').
  2. x, y, z = map(Symbol, 'xyz')
  3. var('x y z')
  4. from sympy.abc import x, y, z
  5. x = S('x') # or sympify('x')
  6. Using isympy -a
  7. x, y, z = symbols('x y z')

There are also some variations on the above. For example, 7 can also be spelled symbols('x,y,z') or symbols('x:z').

The recommended way to create Symbols is to use symbols, i.e., number 7. Some of the other methods are useful when working interactively, but should be avoided when writing more permanent code. Here is a discussion on each of the above:

  1. x = Symbol('x')

    This way is actually the next best way, and if you are only creating a single Symbol, it is fine. Symbol is the class of Symbol, so creating a Symbol this way calls the constructor directly. The primary disadvantage of this method is that it does not generalize easily to creating multiple Symbols. The one situation where this method is convenient is if you wish to create a Symbol with a character in the name that will be parsed by symbols, such as a space, ,, or :. (To do this with symbols requires escaping the special characters.)

  2. x, y, z = map(Symbol, 'xyz')

    There is no reason to do this. Just use symbols instead. It is easier to read, and has nice syntax for things like numbered symbols (see below). Also, map works differently than symbols (if you put in space, you will get the wrong thing), and it is harder to create multicharacter Symbols with this method.

  3. var('x y z')

    var uses the exact same syntax as symbols. The difference is that var automatically injects Symbols into the namespace. Thus, it is sufficient to type just var('x y z') instead of x, y, z = var('x y z'). There are a few reasons this should be avoided in permanent places like scripts, libraries, and notebooks. First, it is harder to read, both by humans and by libraries that parse source code. Second, it uses inspect.currentframe to inject the symbols, which is not guaranteed to always work. For example, it may not behave as expected in certain nested scopes, or in alternate Python implementations from CPython.

  4. from sympy.abc import x, y, z

    This method is convenient, and should work fine. The biggest disadvantage to using sympy.abc is that it does not generalize to Symbol names that are not single letters. On the other hand, symbols, which is about as much typing, does. Also, SymPy defines some single-letter names such as I and Q, which would get overwritten by from sympy.abc import I or from sympy.abc import *.

  5. x = S('x')

    sympify will create Symbols, but this is just a side effect of its string parsing abilities (see also the previous section). The biggest issue here is that if you mistype something (or heaven forbid you use pass user-input through this), it will create that whole expression instead of the Symbol.

  6. isympy -a

    This is the easiest way to work interactively and not have to worry about defining Symbol names. With the -a option, isympy will automatically parse input for undefined names and convert them to Symbols. This requires IPython to work. The disadvantage here is that if you later wish to move something you wrote to a script, you will need to define all the Symbols. Another disadvantage is that if you mistype a function name, it will create a Symbol for it, instead of failing with NameError as usual. There are some other subtleties with this method. See the section on -a in isympy --help for more info.

  7. x, y, z = symbols('x y z')

    This is the best way to create Symbols. It works for single symbols (x = symbols('x')), but generalizes easily to multiple symbols. It has convenient syntax for creating multiple Symbols at once. For example, symbols('x0:5'), or just symbols('x:5') will create x0, x1, x2, x3, x4. symbols('a:z') will create a Symbol for every lowercase letter in the English alphabet. symbols also generalizes to other Symbol-like classes, like Wild or Dummy (or actually, any class that takes a string argument). Just use x, y, z = symbols('x y z', cls=Dummy). See the docstring of symbols for more information on what this function can do.

In summary

Don't

Don't use map(Symbols, 'xyz') or sympify (2 or 5) to create Symbols.

Do

Do use symbols (number 7). If you are working strictly interactively, use var or isympy -a to avoid the hassle of creating symbols. Symbol('x') and from sympy.abc import x are OK, but are not as general as symbols.