最佳实践

本页概述了 SymPy 用户的一些最佳实践。此处提供的最佳实践将有助于避免在使用 SymPy 时可能出现的一些常见错误和陷阱。

本页主要侧重于普遍适用于 SymPy 所有部分的最佳实践。特定于某些 SymPy 子模块或函数的最佳实践在这些特定函数的文档中概述。

基本用法

定义符号

  • 使用 symbols()Symbol() 定义符号。 symbols() 函数是创建符号最便捷的方式。它支持一次创建一个或多个符号

    >>> from sympy import symbols
    >>> x = symbols('x')
    >>> a, b, c = symbols('a b c')
    

    此外,它支持向符号添加假设

    >>> i, j, k = symbols('i j k', integer=True)
    

    并定义 Function 对象

    >>> from sympy import Function
    >>> f, g, h = symbols('f g h', cls=Function)
    

    它还支持定义多个编号符号的简写形式

    >>> symbols('x:10')
    (x0, x1, x2, x3, x4, x5, x6, x7, x8, x9)
    

    也可以直接使用 Symbol() 构造函数。与 symbols() 不同,Symbol() 始终创建一个符号。如果您想创建名称中包含特殊字符的符号,或者以编程方式创建符号,则这是最佳选择。

    >>> from sympy import Symbol
    >>> x_y = Symbol('x y') # This creates a single symbol named 'x y'
    

    应避免使用 var() 函数,除非在交互模式下。它与 symbols() 函数类似,只是它会自动将符号名称注入调用命名空间。此函数专门设计用于交互式输入的便利性,不建议在程序中使用。

    不要使用 sympify()S() 来创建符号。这似乎可以工作

    >>> from sympy import S
    >>> x = S("x") # DO NOT DO THIS
    

    但是,S()/sympify() 不是用来创建符号的。它们被设计用来解析整个表达式。如果输入字符串不是有效的 Python,则此方法会失败。如果字符串解析为更大的表达式,它也会失败。

    >>> # These both fail
    >>> x = S("0x") 
    Traceback (most recent call last):
    ...
    SyntaxError: invalid syntax (<string>, line 1)
    >>> x = S("x+") 
    Traceback (most recent call last):
    ...
    SyntaxError: invalid syntax (<string>, line 1)
    

    任何 Python 字符串都可以用作有效的 Symbol 名称。

    此外,下面 避免使用字符串输入 部分中描述的所有问题都适用于此。

  • 在已知符号的假设时,为符号添加假设。 假设 可以通过将相关的关键字传递给 symbols() 来添加。最常见的假设是 real=Truepositive=True(或 nonnegative=True)和 integer=True

    假设永远不是必需的,但如果已知,始终建议包含它们,因为这将允许某些操作简化。如果没有提供假设,符号被假定为一般的复数,并且除非它们对所有复数都为真,否则不会进行简化。

    例如

    >>> from sympy import integrate, exp, oo
    >>> a = symbols('a') # no assumptions
    >>> integrate(exp(-a*x), (x, 0, oo))
    Piecewise((1/a, Abs(arg(a)) < pi/2), (Integral(exp(-a*x), (x, 0, oo)), True))
    
    >>> a = symbols('a', positive=True)
    >>> integrate(exp(-a*x), (x, 0, oo))
    1/a
    

    这里,\(\int_0^\infty e^{-ax}\,dx\)a 定义为没有假设时给出分段结果,因为积分只有在 a 为正时才收敛。将 a 设置为正会删除此分段。

    当你使用假设时,最佳实践是始终对每个符号名称使用相同的假设。SymPy 允许对同一个符号名称定义不同的假设,但这些符号将被视为彼此不相等。

    >>> z1 = symbols('z')
    >>> z2 = symbols('z', positive=True)
    >>> z1 == z2
    False
    >>> z1 + z2
    z + z
    

另请参阅 避免使用字符串输入不要在 Python 函数中硬编码符号名称,了解有关定义符号的相关最佳实践。

避免使用字符串输入

不要将字符串用作函数的输入。相反,使用 Symbols 和适当的 SymPy 函数以符号方式创建对象,并对其进行操作。

不要

>>> from sympy import expand
>>> expand("(x**2 + x)/x")
x + 1

>>> from sympy import symbols
>>> x = symbols('x')
>>> expand((x**2 + x)/x)
x + 1

始终使用 Python 运算符显式地创建表达式是最好的,但有时你确实会从字符串输入开始,例如如果你从用户那里接受表达式。如果你确实有一个字符串,你应该使用 parse_expr() 显式地解析它。最好尽早解析所有字符串,并且从那时起只使用符号操作。

>>> from sympy import parse_expr
>>> string_input = "(x**2 + x)/x"
>>> expr = parse_expr(string_input)
>>> expand(expr)
x + 1

原因

将字符串用作 SymPy 函数输入有很多缺点。

  • 它是非 Python 的,并且使代码更难阅读。参见 Python 之禅“显式胜于隐式”。

  • 在一般 SymPy 函数中对字符串输入的支持大多是偶然的。之所以会发生这种情况,是因为这些函数会调用 sympify() 来处理它们的输入,以便将诸如 Python int 之类的东西转换为 SymPy Integer。但是,除非使用 strict=True 标志,否则 sympify() 还会将字符串解析为 SymPy 表达式。对于一般 SymPy 函数(除了 sympify()parse_expr())自动解析字符串 可能会在 SymPy 的未来版本中消失

  • 符号或函数名称中的拼写错误可能不会被注意到。这是因为字符串中所有未定义的名称将自动解析为 Symbols 或 Functions。如果输入有拼写错误,字符串仍然可以正确解析,但输出不会是预期的结果。例如

    >>> from sympy import expand_trig
    >>> expand_trig("sine(x + y)")
    sine(x + y)
    

    将此与在不使用字符串时得到的显式错误进行比较

    >>> from sympy import sin, symbols
    >>> x, y = symbols('x y')
    >>> expand_trig(sine(x + y)) # The typo is caught by a NameError
    Traceback (most recent call last):
    ...
    NameError: name 'sine' is not defined
    >>> expand_trig(sin(x + y))
    sin(x)*cos(y) + sin(y)*cos(x)
    

    在第一个示例中,sinesin 的拼写错误)被解析为 Function("sine"),并且似乎 expand_trig 无法处理它。在第二种情况下,我们立即从未定义的名称 sine 中得到一个错误,并且修复了我们的拼写错误,我们看到 expand_trig 确实可以做我们想做的事情。

  • 使用字符串输入时,最大的问题来自使用假设。在 SymPy 中,如果两个符号具有相同的名称但假设不同,则它们被视为不相等。

    >>> z1 = symbols('z')
    >>> z2 = symbols('z', positive=True)
    >>> z1 == z2
    False
    >>> z1 + z2
    z + z
    

    通常建议避免这样做,因为它会导致像上面的表达式一样的混淆(参见上面的 定义符号)。

    但是,字符串输入将始终创建没有假设的符号。因此,如果你有一个具有假设的符号,然后尝试使用它的字符串版本,你最终会得到令人困惑的结果。

    >>> from sympy import diff
    >>> z = symbols('z', positive=True)
    >>> diff('z**2', z)
    0
    

    这里的答案显然是错误的,但发生的事情是 "z**2" 中的 z 解析为 Symbol('z') 且没有假设,SymPy 认为它与 z = Symbol('z', positive=True) 不同,后者用作 diff() 的第二个参数。因此,就 diff 而言,该表达式是常数,结果为 0。

    这种事情特别糟糕,因为它通常不会导致任何错误。它只会静默地给出“错误”的答案,因为 SymPy 会将你认为相同的符号视为不同的符号。通过不使用字符串输入来避免这种情况。

    如果你正在解析字符串,并且希望其中的一些符号具有某些假设,你应该创建这些符号并将它们传递给 parse_expr() 的字典。例如

    不要

    >>> a, b, c = symbols('a b c', real=True)
    >>> # a, b, and c in expr are different symbols without assumptions
    >>> expr = parse_expr('a**2 + b - c')
    >>> expr.subs({a: 1, b: 1, c: 1}) # The substitution (apparently) doesn't work
    a**2 + b - c
    

    >>> # a, b, and c are the same as the a, b, c with real=True defined above
    >>> expr = parse_expr('a**2 + b - c', {'a': a, 'b': b, 'c': c})
    >>> expr.subs({a: 1, b: 1, c: 1})
    1
    
  • 许多 SymPy 操作被定义为方法,而不是函数,也就是说,它们像 sympy_obj.method_name() 一样被调用。这些方法在字符串上不起作用,因为它们还不是 SymPy 对象。例如

    >>> "x + 1".subs("x", "y")
    Traceback (most recent call last):
    ...
    AttributeError: 'str' object has no attribute 'subs'
    

    与之形成对比的是

    >>> x, y = symbols('x y')
    >>> (x + 1).subs(x, y)
    y + 1
    
  • Symbol 名称可以包含任何字符,包括那些不是有效的 Python 的字符。但是如果你使用字符串作为输入,就不可能使用这些符号。例如

    >>> from sympy import solve
    >>> solve('x_{2} - 1') 
    ValueError: Error from parse_expr with transformed code: "Symbol ('x_' ){Integer (2 )}-Integer (1 )"
    ...
    SyntaxError: invalid syntax (<string>, line 1)
    

    这不起作用,因为 x_{2} 不是有效的 Python。但完全可以使用它作为 Symbol 名称。

    >>> x2 = symbols('x_{2}')
    >>> solve(x2 - 1, x2)
    [1]
    

    实际上,上面的情况是最好的情况,你得到一个错误。你也有可能得到意想不到的结果

    >>> solve('x^1_2 - 1')
    [-1, 1, -I, I, -1/2 - sqrt(3)*I/2, -1/2 + sqrt(3)*I/2, 1/2 - sqrt(3)*I/2, 1/2 + sqrt(3)*I/2, -sqrt(3)/2 - I/2, -sqrt(3)/2 + I/2, sqrt(3)/2 - I/2, sqrt(3)/2 + I/2]
    

    这里发生的事情是,它不是将 x^1_2 解析为 \(x^1_2\),而是解析为 x**12^ 被转换为 **,并且 _ 在 Python 中被忽略在数字文本中)。

    如果我们改为创建一个 Symbol,则符号名称的实际内容会被忽略。它总是表示为单个符号。

    >>> x12 = symbols('x^1_2')
    >>> solve(x12 - 1, x12)
    [1]
    
  • 如果你使用字符串,语法错误直到运行该行才会被捕获。如果你构建表达式,语法错误会在任何内容运行之前立即被捕获。

  • 代码编辑器中的语法高亮通常不会识别和用颜色编码字符串的内容,而它可以识别 Python 表达式。

避免将表达式作为字符串进行操作

如果你发现自己对符号表达式进行了大量的字符串或正则表达式操作,这通常意味着你错误地使用了 SymPy。最好使用诸如 +-*/ 之类的运算符以及 SymPy 的各种函数和方法来直接构建表达式。基于字符串的操作会导致错误,会快速变得复杂,并失去符号表达式结构的优势。

之所以会发生这种情况,是因为在字符串中没有符号表达式的概念。对于 Python 来说,"(x + y)/z""/x+)(y z " 没有区别,后者是同一个字符串,但字符顺序不同。相比之下,SymPy 表达式实际上知道它代表什么类型的数学对象。SymPy 具有许多用于构建和操作表达式的函数和方法,它们都对 SymPy 对象进行操作,而不是字符串。

例如

不要

>>> expression_str = '+'.join([f'{i}*x_{i}' for i in range(10)])
>>> expr = parse_expr(expression_str)
>>> expr
x_1 + 2*x_2 + 3*x_3 + 4*x_4 + 5*x_5 + 6*x_6 + 7*x_7 + 8*x_8 + 9*x_9

>>> from sympy import Add, Symbol
>>> expr = Add(*[i*Symbol(f'x_{i}') for i in range(10)])
>>> expr
x_1 + 2*x_2 + 3*x_3 + 4*x_4 + 5*x_5 + 6*x_6 + 7*x_7 + 8*x_8 + 9*x_9

另请参阅 之前关于避免将字符串用作函数输入的部分

精确的有理数与浮点数

如果已知某个数字完全等于某个量,请避免将其定义为浮点数。

例如,

不要

>>> expression = x**2 + 0.5*x + 1

>>> from sympy import Rational
>>> expression = x**2 + Rational(1, 2)*x + 1
>>> expression = x**2 + x/2 + 1 # Equivalently

然而,这并不是说你不应该在 SymPy 中使用浮点数,只是说如果已知更精确的值,则应优先使用它。SymPy 确实支持任意精度浮点数,但某些操作可能在使用它们时表现不佳。

这也适用于可以精确表示的非有理数。例如,应避免使用 math.pi 并优先使用 sympy.pi,因为前者是 \(\pi\) 的数值近似值,而后者是精确的 \(\pi\)(另请参见下面的 分离符号和数值代码;通常,在使用 SymPy 时应避免导入 math)。

不要

>>> import math
>>> import sympy
>>> math.pi
3.141592653589793
>>> sympy.sin(math.pi)
1.22464679914735e-16

>>> sympy.pi
pi
>>> sympy.sin(sympy.pi)
0

这里 sympy.sin(math.pi) 不完全等于 0,因为 math.pi 不完全等于 \(\pi\)

还应注意避免编写 integer/integer,其中两个整数都是显式整数。这是因为 Python 将在 SymPy 解析它之前将其评估为浮点值。

不要

>>> x + 2/7 # The exact value of 2/7 is lost
x + 0.2857142857142857

在这种情况下,使用 Rational 创建有理数,或者使用 S() 简写,如果你想节省输入。

>>> from sympy import Rational, S
>>> x + Rational(2, 7)
x + 2/7
>>> x + S(2)/7 # Equivalently
x + 2/7

原因

如果已知精确值,则应优先于浮点数,原因如下:

  • 精确的符号值通常可以进行符号化简或操作。浮点数表示对精确实数的近似值,因此不能被精确化简。例如,在上面的示例中,sin(math.pi) 不会产生 0,因为 math.pi 不完全等于 \(\pi\)。它只是一个浮点数,它将 \(\pi\) 近似到 15 位数字(实际上,它是对 \(\pi\) 的一个接近的有理数近似值,但并不完全等于 \(\pi\))。

  • 如果存在浮点值,某些算法将无法计算结果,但如果值是有理数,则可以计算结果。这是因为有理数具有使这些算法更容易处理它们的属性。例如,对于浮点数,可能存在一种情况,即一个数字应该为 0,但由于近似误差,它并不完全等于 0。

    一个特别值得注意的例子是浮点指数。例如,

    >>> from sympy import factor
    >>> factor(x**2.0 - 1)
    x**2.0 - 1
    >>> factor(x**2 - 1)
    (x - 1)*(x + 1)
    
  • SymPy 浮点数具有与使用有限精度浮点近似值可能发生的相同显著性取消问题

    >>> from sympy import expand
    >>> expand((x + 1.0)*(x - 1e-16)) # the coefficient of x should be slightly less than 1
    x**2 + 1.0*x - 1.0e-16
    >>> expand((x + 1)*(x - Rational('1e-16'))) # Using rational numbers gives the coefficient of x exactly
    x**2 + 9999999999999999*x/10000000000000000 - 1/10000000000000000
    

    在 SymPy 中,可以通过仔细使用 evalf 及其以任意精度进行评估的能力,在许多情况下避免这些问题。这通常涉及以符号值计算表达式,并在以后用 expr.evalf(subs=...) 代替它们,或者以高于默认值 15 位数字的精度开始使用 Float

    >>> from sympy import Float
    >>> expand((x + 1.0)*(x - Float('1e-16', 20)))
    x**2 + 0.9999999999999999*x - 1.0e-16
    

一个 Float 数字可以通过将其传递给 Rational 来转换为其精确的有理数等价物。或者,你可以使用 nsimplify 找到最漂亮的有理数近似值。如果数字应该是有理数,这有时可以重现预期的数字(尽管同样,如果可以,最好一开始就使用有理数)

>>> from sympy import nsimplify
>>> Rational(0.7)
3152519739159347/4503599627370496
>>> nsimplify(0.7)
7/10

避免 simplify()

simplify()(不要与 sympify() 混淆)被设计为通用启发式方法。它尝试对输入表达式应用各种简化算法,并根据某种度量返回看起来“最简单”的结果。

simplify() 非常适合交互式使用,在这种情况下,你只想让 SymPy 对表达式做它所能做的一切。但是,在程序化使用中,最好避免使用 simplify(),而是使用更 有针对性的简化函数(例如,cancel()expand()collect())。

通常首选这样做有几个原因:

  • 由于其启发式性质,simplify() 可能很慢,因为它尝试了许多不同的方法来尝试找到最佳简化。

  • 无法保证表达式在通过 simplify() 后将采用什么形式。它实际上可能最终变得“不那么简单”,无论你希望使用什么度量。相比之下,有针对性的简化函数对它们的执行行为以及对输出的保证非常明确。例如,

    • factor() 将始终将多项式分解为不可约因子。

    • cancel() 将始终将有理函数转换为 \(p/q\) 的形式,其中 \(p\)\(q\) 是没有公因式的展开多项式。

    每个函数的文档都描述了它对输入表达式将执行的确切行为。

  • 如果表达式包含意外形式或意外子表达式,有针对性的简化不会执行意外操作。如果简化函数使用 deep=False 应用,则这种情况尤其如此,以便仅将简化应用于顶层表达式。

其他一些简化函数本质上是启发式的,因此也应谨慎使用。例如,trigsimp() 函数是针对三角函数的启发式方法,但 sympy.simplify.fu 子模块中的例程允许应用特定的三角恒等式。

教程的简化部分 和该 简化模块参考 列出了各种有针对性的简化函数。

在某些情况下,你可能确切地知道要对表达式应用哪些简化操作,但可能没有一组确切的简化函数来执行它们。当这种情况发生时,你可以使用 replace() 创建你自己的有针对性的简化,或者通常,使用 高级表达式操作 手动执行。

不要在 Python 函数中硬编码符号名称

不要在函数定义中硬编码 Symbol 名称,而是将符号作为函数的参数。

例如,考虑一个函数 theta_operator,它计算 theta 算子 \(\theta = zD_z\)

不要

def theta_operator(expr):
    z = symbols('z')
    return z*expr.diff(z)

def theta_operator(expr, z):
    return z*expr.diff(z)

硬编码的符号名称的缺点是要求所有表达式都使用该确切的符号名称。在上面的示例中,无法计算 \(\theta = xD_x\),因为它被硬编码为 \(zD_z\)。更糟糕的是,尝试这样做会默默地导致错误的结果而不是错误,因为 x 被视为常量表达式

>>> def theta_operator(expr):
...     z = symbols('z')
...     return z*expr.diff(z)
>>> theta_operator(x**2) # The expected answer is 2*x**2
0

如果函数接受任意用户输入,这将特别成问题,因为用户可能在他们的数学上下文中使用不同的变量名。并且,如果用户已经使用了符号 z 但将其用作常量,则他们需要用 subs 将它们互换,然后才能使用该函数。

这种反模式存在问题的另一个原因是,具有假设的符号被认为与没有假设的符号不相等。如果有人使用

>>> z = symbols('z', positive=True)

例如,为了使进一步简化成为可能(参见上面的 定义符号),函数硬编码 Symbol('z') 而不带假设将不起作用

>>> theta_operator(z**2)
0

通过将符号作为函数参数,例如 theta_operator(expr, z),这些问题都会消失。

分离符号和数值代码

SymPy 将自己与 Python 生态系统中的大多数其他库区分开来,因为它以符号方式运行,而其他库,例如 NumPy,以数值方式运行。这两种范式足够不同,因此最好尽可能地将它们分开。

重要的是,SymPy 不是为与 NumPy 数组一起使用而设计的,反之,NumPy 不会直接与 SymPy 对象一起使用。

>>> import numpy as np
>>> import sympy
>>> a = np.array([0., 1., 2.])
>>> sympy.sin(a)
Traceback (most recent call last):
...
AttributeError: 'ImmutableDenseNDimArray' object has no attribute 'as_coefficient'
>>> x = Symbol('x')
>>> np.sin(x) # NumPy functions do not know how to handle SymPy expressions
Traceback (most recent call last):
...
TypeError: loop of ufunc does not support argument 0 of type Symbol which has no callable sin method

如果您想同时使用 SymPy 和 NumPy,您应该使用 lambdify() 将 SymPy 表达式显式地转换为 NumPy 函数。在 SymPy 中,典型的工作流程是使用 SymPy 对问题进行符号建模,然后使用 lambdify() 将结果转换为可以对 NumPy 数组进行求值的数值函数。对于高级用例,lambdify()/NumPy 可能不够用,您可能需要使用 SymPy 的更通用的 代码生成 例程来为其他快速数值语言(如 Fortran 或 C)生成代码。

>>> # First symbolically construct the expression you are interested in with SymPy
>>> from sympy import diff, sin, exp, lambdify, symbols
>>> x = symbols('x')
>>> expr = diff(sin(x)*exp(x**2), x)

>>> # Then convert it to a numeric function with lambdify()
>>> f = lambdify(x, expr)

>>> # Now use this function with NumPy
>>> import numpy as np
>>> a = np.linspace(0, 10)
>>> f(a) 
[ 1.00000000e+00  1.10713341e+00  1.46699555e+00 ... -3.15033720e+44]

以下是一些应该尽量避免的反模式

  • 不要使用 import math 几乎不需要在 SymPy(或 NumPy)旁边使用 标准库 math 模块math 中的每个函数都已存在于 SymPy 中。SymPy 可以使用 evalf 对数值进行计算,这比 math 提供更高的精度和准确性。或者更好的是,SymPy 默认情况下会对事物进行符号计算。math 中的函数和常量都是浮点数,它们是不精确的。SymPy 总是尽可能地使用精确量来进行计算。例如,

    >>> import math
    >>> math.pi # a float
    3.141592653589793
    >>> import sympy
    >>> sympy.sin(math.pi)
    1.22464679914735e-16
    

    sympy.sin(math.pi) 的结果并非如您所期望的那样是 0,因为 math.pi 只是对 \(\pi\) 的近似值,等于 16 位数。另一方面,sympy.pi 正好 等于 \(\pi\),因为它是以符号形式表示的,因此能够给出精确的答案

    >>> sympy.sin(sympy.pi)
    0
    

    因此,一般来说,应该 优先使用符号表示。但即使您确实想要一个浮点数,也最好使用 SymPy 的 evalf() 而不是 math。这样做可以避免 math 函数只能对 float 对象进行操作,而不能对符号表达式进行操作的陷阱

    >>> x = Symbol('x')
    >>> math.sin(x)
    Traceback (most recent call last):
    ...
    TypeError: Cannot convert expression to float
    

    此外,SymPy 的 evalf()math 更准确,因为它使用任意精度算法,并允许您指定任意位数。

    >>> sympy.sin(1).evalf(30)
    0.841470984807896506652502321630
    >>> math.sin(1)
    0.8414709848078965
    

    即使使用 NumPy,也应该避免使用 math。NumPy 函数比其 math 等效函数更快,支持更多种数值数据类型,并且可以对值数组进行操作,而 math 函数一次只能对单个标量进行操作。

  • 不要将 SymPy 表达式传递给 NumPy 函数。 您不应该将 SymPy 表达式传递给 NumPy 函数。这包括 numpyscipy 命名空间中的任何内容,以及来自其他 Python 库(如 matplotlib)的大多数函数。这些函数只设计用于处理具有数值的 NumPy 数组。

  • 不要将 SymPy 表达式传递给 lambdified 函数。 与前一点类似,您不应该将 SymPy 表达式传递给使用 lambdify 创建的函数。实际上,由 lambdify 返回的函数 NumPy 函数,因此这里的情况完全相同。在某些情况下,使用 lambdify() 创建的函数可能会与 SymPy 表达式一起使用,但这只是其工作方式的偶然结果。请参阅 lambdify() 文档的“工作原理”部分,以了解有关此问题发生原因的更多详细信息。

  • 避免将 SymPy 表达式存储在 NumPy 数组中。 虽然在技术上可以将 SymPy 表达式存储在 NumPy 数组中,但这通常是一个错误。如果 NumPy 数组的 dtypeobject(而不是像 float64int64 这样的数值数据类型),这表明这种情况正在发生。

    就像在使用 SymPy 进行符号计算时应该避免使用 NumPy 一样,一旦计算转移到 NumPy 的数值方面,就应该停止使用 SymPy。

    包含 SymPy 表达式的 NumPy 数组实际上与直接在 SymPy 表达式上调用 NumPy 函数具有相同的问题。它们不知道如何对 SymPy 对象进行操作,因此会失败。即使 SymPy 对象都是 SymPy Floats,情况也是如此。

    >>> import numpy as np
    >>> import sympy
    >>> a = np.asarray([sympy.Float(1.0), sympy.Float(0.0)]) # Do not do this
    >>> print(repr(a)) # Note that the dtype is 'object'
    array([1.00000000000000, 0.0], dtype=object)
    >>> np.sin(a)
    Traceback (most recent call last):
    ...
    TypeError: loop of ufunc does not support argument 0 of type Float which has no callable sin method
    

    如果您正在这样做,您可能应该使用本机 NumPy 浮点数,或者,如果您确实想要存储一个 SymPy 表达式数组,您应该使用 SymPy 的 MatrixNDimArray 类。

高级用法

小心比较和排序符号对象

小心使用对数值进行比较的编程代码,无论是直接使用不等式(<<=>>=)还是间接使用类似 sorted 的方法。问题在于,如果不等式未知,结果将是符号,例如

>>> x > 0
x > 0

如果在符号不等式上调用 bool(),由于存在歧义,它将引发异常

>>> bool(x > 0)
Traceback (most recent call last):
...
TypeError: cannot determine truth value of Relational

类似于

if x > 0:
    ...

的检查如果只对数值 x 进行测试,可能会运行良好。但如果 x 可能是符号,则上面的代码是错误的。它将使用 TypeError: cannot determine truth value of Relational 失败。如果您遇到此异常,这意味着此错误已在某个地方出现(有时错误在 SymPy 本身中;如果看起来确实如此,请 打开一个问题)。

使用 sorted 时也会出现完全相同的问题,因为它在内部使用 >

>>> sorted([x, 0])
Traceback (most recent call last):
...
TypeError: cannot determine truth value of Relational

有几种解决此问题的方法,选择哪种方法取决于您正在做什么

  • 禁止符号输入。 如果您的函数不可能在符号输入上工作,您可以明确地禁止它们。这里的主要好处是为用户提供比 TypeError:  cannot determine truth value of Relational 更易读的错误消息。可以使用 is_number 属性来检查表达式是否可以使用 evalf() 求值为特定数字。如果您只想接受整数,您可以检查 isinstance(x, Integer)(在调用 sympify() 将 Python 整数进行转换之后)。请注意,is_integer 使用假设系统,即使对于符号对象(如 Symbol('x', integer=True))也可能为 True。

  • 使用假设系统。 如果您确实支持符号输入,您应该使用假设系统来检查诸如 x > 0 之类的东西,例如,使用 x.is_positive。这样做时,您应该始终 了解假设系统中使用的三值模糊逻辑的细微差别。也就是说,始终注意假设可能是 None,这意味着它的值未知,可以是真或假。例如,

    if x.is_positive:
        ...
    

    只会在 x.is_positiveTrue 时运行该块,但您可能希望在 x.is_positiveNone 时执行某些操作。

  • 返回一个 Piecewise 结果。 如果函数的结果取决于不等式或其他布尔条件,您可以使用 Piecewise 返回一个表示两种可能性的符号结果。当可能时,这通常是首选,因为它提供了最大的灵活性。这是因为结果是以符号形式表示的,这意味着,例如,人们可以稍后为符号代入特定值,它将评估为特定情况,即使它与其他表达式组合在一起也是如此。

    例如,而不是

    if x > 0:
        expr = 1
    else:
        expr = 0
    

    这可以用符号形式表示为

    >>> from sympy import Piecewise, pprint
    >>> expr = Piecewise((1, x > 0), (0, True))
    >>> pprint(expr, use_unicode=True)
    ⎧1  for x > 0
    
    ⎩0  otherwise
    >>> expr.subs(x, 1)
    1
    >>> expr.subs(x, -1)
    0
    
  • 使用 ordered() 将表达式排序为规范顺序。如果您试图使用 sorted 因为您想要规范排序,但并不特别关心该排序是什么,则可以使用 ordered

    >>> from sympy import ordered
    >>> list(ordered([x, 0]))
    [0, x]
    

    或者,尝试以一种结果正确性不依赖于参数处理顺序的方式编写函数。

自定义 SymPy 对象

SymPy 旨在通过自定义类进行扩展,通常通过子类化 BasicExprFunction 来实现。SymPy 中的所有符号类都是以这种方式编写的,这里介绍的要点同样适用于它们,也适用于用户定义的类。

有关如何编写 Function 子类的深入指南,请参阅 编写自定义函数的指南

参数不变性

自定义 SymPy 对象应始终满足以下不变性

  1. all(isinstance(arg, Basic) for arg in args)

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

第一个说的是 args 的所有元素都应该是 Basic 的实例。第二个说的是表达式应该可以从其 args 中重建(注意 func 通常与 type(expr) 相同,但并不总是如此)。

这两个不变性在整个 SymPy 中被假定,对于任何操作表达式的函数来说都是必不可少的。

例如,考虑这个简单的函数,它是 xreplace() 的简化版本

>>> def replace(expr, x, y):
...     """Replace x with y in expr"""
...     newargs = []
...     for arg in expr.args:
...         if arg == x:
...             newargs.append(y)
...         else:
...             newargs.append(replace(arg, x, y))
...     return expr.func(*newargs)
>>> replace(x + sin(x - 1), x, y)
y + sin(y - 1)

该函数通过递归遍历 exprargs 来工作,并重建它,除了任何 x 的实例都被替换为 y

很容易看出,如果 args 不变性不成立,这个函数将如何失效

  1. 如果一个表达式包含不是 Basic 的 args,那么它们在递归调用中会遇到 AttributeError,因为非 Basic args 不会有 .args.func 属性。

  2. 如果表达式不能从其 args 中重建,那么 return exr.func(*newargs) 行就会失败,即使在没有一个 args 被替换所改变的平凡情况下也是如此,这应该是一个无操作。

使所有 args 成为 Basic 的实例通常意味着对类的输入调用 _sympify(),以便它们成为基本实例。如果您想要在类上存储一个字符串,您应该使用 Symbolsympy.core.symbols.Str

在某些情况下,类可能会接受多种等效形式的 args。重要的是,无论哪种形式存储在 args 中,都是可以用来重建类的形式之一。只要该规范化形式被接受为输入,规范化 args 就可以了。例如,Integral 总是将变量参数存储为元组,以简化内部处理,但这种形式也被类构造函数接受

>>> from sympy import Integral
>>> expr = Integral(sin(x), x)
>>> expr.args # args are normalized
(sin(x), (x,))
>>> Integral(sin(x), (x,)) # Also accepted
Integral(sin(x), x)

请注意,大多数用户定义的自定义函数应该通过子类化 Function 来定义(请参阅 编写自定义函数的指南)。Function 类会自动处理两个 args 不变性,所以如果您正在使用它,您就不需要担心这一点。

避免过度自动求值

在定义自定义函数时,请避免进行过多的自动求值(即,在 eval__new__ 方法中的求值)。

通常,自动求值应该只在它很快的情况下进行,而且这是任何人都不会想要阻止的事情。自动求值很难撤销。一个好的经验法则是对显式的数值进行求值(isinstance(x, Number)),并将所有其他内容以符号形式保持未求值。使用更高级别的恒等式进行进一步的简化应该在特定的简化函数或 doit 中完成(请参阅 自定义函数指南,了解可以在 SymPy 对象上定义的常用简化例程列表)。

自定义函数指南 深入探讨了这一点(但请注意,此指南同样适用于所有 SymPy 对象,而不仅仅是函数)。但简而言之,原因是,防止自动求值的唯一方法是使用 evaluate=False,这很脆弱。此外,代码总是会假设由于自动求值而为真的不变性,这意味着使用 evaluate=False 创建的表达式会导致此代码的结果出错。这也意味着,以后移除自动求值会很困难。

可能很昂贵的求值(例如,应用符号恒等式)本身就很糟糕,因为它会导致即使不使用表达式也可能允许创建表达式。这也适用于检查符号假设(例如 x.is_integer),所以这也应该在类构造函数中避免。

不要

class f(Function):
    @classmethod
    def eval(cls, x):
        if x.is_integer: # Bad (checking general assumptions)
            return 0
        if isinstance(x, Add): # Bad (applying symbolic identities)
            return Add(*[f(i) for i in x.args])

class f(Function):
    @classmethod
    def eval(cls, x):
        if isinstance(x, Integer): # Good (only evaluating on explicit integers)
            return 0

    # Good (applying simplification on assumptions in doit())
    def doit(self, deep=True, **hints):
        x = self.args[0]
        if deep:
           x = x.doit(deep=deep, **hints)
        if x.is_integer:
           return S(0)
        return self

    # Good (applying symbolic identities inside of simplification functions)
    def _eval_expand_func(self, **hints):
        x = self.args[0]
        if isinstance(x, Add):
            return Add(*[f(i) for i in x.args])
        return self

请注意,并非 SymPy 中的所有类都很好地遵循了这一准则,但我们正在努力改进这一点。

不要拆解集合

接受任意数量参数的函数和类应该直接接受参数,例如 f(*args),或者作为单个参数,例如 f(args)。它们不应该试图同时支持两者。

原因是,这样做会无法表示嵌套的集合。例如,以 FiniteSet 类为例。它的构造方式是 FiniteSet(x, y, z)(即,使用 *args)。

>>> from sympy import FiniteSet
>>> FiniteSet(1, 2, 3)
{1, 2, 3}

您可能很想也支持 FiniteSet([1, 2, 3]),以匹配内置的 set。但是,这样做会无法表示包含单个 FiniteSet 的嵌套 FiniteSet,例如 \(\{\{1, 2, 3\}\}\)

>>> FiniteSet(FiniteSet(1, 2, 3)) # We don't want this to be the same as {1, 2, 3}
FiniteSet({1, 2, 3})

至于应该使用 args 还是 *args,如果只可能存在有限数量的参数,那么 *args 通常更好,因为这会让使用对象的 args 来处理事情变得更容易,因为 obj.args 将是类的直接参数。但是,如果您可能想支持除了有限集合之外的符号无限集合,例如 IntegersRange,那么最好使用 args,因为这对于 *args 是不可能的。

避免在对象上存储额外的属性

您可能想要创建一个自定义 SymPy 对象的一个常见原因是您想要在对象上存储额外的属性。但是,以一种幼稚的方式来做,即,仅仅将数据作为对象的 Python 属性存储起来,几乎总是一个坏主意。

SymPy 不期望对象在它们的 args 之外存储额外的数据。例如,这会破坏 == 检查,该检查只比较对象的 args。请参阅下面的 不要重写 __eq__ 部分,了解为什么重写 __eq__ 是一个坏主意。本节和该节密切相关。

通常,有更好的方法可以实现您想要实现的目标,具体取决于您情况的具体细节

  • 将额外的数据存储在对象的 args 中。如果要存储的额外数据是您对象的数学描述的一部分,那么这是最佳方法。

    只要数据可以使用其他 SymPy 对象表示,就可以存储在 args 中。请注意,对象的 args 应该可以用来 重新创建对象(例如,类似 YourObject(*instance.args) 应该可以重新创建 instance)。

    此外,需要说明的是,如果你打算在 args 中存储任何额外的信息,那么对 Symbol 进行子类化并不是一个好主意。 Symbol 的设计初衷是没有任何 args。你最好对 Function(参见 编写自定义函数)或 Expr 进行子类化。如果你只是想让两个符号彼此不同,最好的方法通常是给它们不同的名称。如果你担心它们的打印方式,可以在打印时用更规范的名称替换它们,或者使用 自定义打印器

  • 将有关对象的数据单独存储。如果额外的信息与对象的数学属性没有直接关系,这将是最好的方法。

    请记住,SymPy 对象是可散列的,因此它们可以轻松用作字典键。因此,维护一个单独的字典 {object: extra_data} 对非常简单。

    请注意,一些 SymPy API 已经允许重新定义它们如何对对象进行操作,而无需对对象本身进行修改。一个很好的例子是 打印器,它允许定义 自定义打印器,这些打印器可以更改任何 SymPy 对象的打印方式,而无需修改这些对象本身。函数,如 lambdify()init_printing() 允许传入自定义打印器。

  • 使用不同的子类来表示属性。如果属性只有几个可能的值(例如,布尔标志),这通常是一个好主意。通过使用公共超类,可以避免代码重复。

  • 如果你要存储的数据是一个 Python 函数,最好将其用作类中的方法。在许多情况下,该方法可能已经适合 现有的一组可覆盖的 SymPy 方法 之一。如果你想定义函数如何对其自身进行数值计算,可以使用 implemented_function()

  • 通过修改对象的 func 来表示信息。此解决方案比其他解决方案复杂得多,应仅在必要时使用。在某些极端情况下,无法仅使用 args 来表示对象的每个数学方面。例如,这种情况可能会发生,因为 args 只能包含 Basic 实例 的限制。在这种情况下,仍然可以通过使用与 type(expr) 不同的自定义 func 来创建自定义 SymPy 对象(在这种情况下,你需要覆盖 __eq__func而不是在类上)。

    但是,这种情况很少见。

不要覆盖 __eq__

在构建自定义 SymPy 对象时,有时会倾向于覆盖 __eq__ 以定义 == 运算符的自定义逻辑。这几乎总是错误的。自定义 SymPy 类应该将 __eq__ 保持未定义,并使用 Basic 超类中的默认实现。

在 SymPy 中,== 使用 结构相等 来比较对象。也就是说,a == b 表示 ab 是完全相同的对象。它们具有相同的类型和相同的 args== 不执行任何形式的数学相等性检查。例如,

>>> x*(x - 1) == x**2 - x
False

== 始终返回一个布尔值 TrueFalse。符号方程可以用 Eq 表示。

有几个原因:

  • 数学相等性检查可能非常昂贵,而且通常来说,在计算上不可能确定

  • Python 本身在各个地方自动使用 ==,并假设它返回一个布尔值,并且计算成本低廉。例如,如果 b 是一个内置 Python 容器,如 listdictset,那么 a in b 会使用 ==[1]

  • SymPy 在内部广泛地使用 ==,无论是显式地还是隐式地通过诸如 in 或字典键之类的东西。这种用法都隐式地假设 == 在结构上运行。

事实上,结构相等意味着如果 a == bTrue,那么 ab 在所有意图和目的上都是同一个对象。这是因为所有 SymPy 对象都是 不可变的。当 a == 时,任何 SymPy 函数都可以自由地将 a 替换为 b,在任何子表达式中。

Basic 上的默认 __eq__ 方法会检查两个对象是否具有相同的类型和相同的 args。SymPy 的许多部分还隐式地假设如果两个对象相等,那么它们具有相同的 args。因此,尝试覆盖 __eq__ 作为一种避免将某些标识信息存储在对象 args 中的方法并不是一个好主意。对象的 args 应该包含所有重新创建它所需的信息(参见 args)。请注意,对象的构造函数可以接受多种形式的参数,只要它接受 args 中存储的形式即可(例如,一些 args 可以具有默认值,这完全可以接受)。

以下是可能让你想要覆盖 __eq__ 的一些原因以及首选的替代方法

  • 使 == 应用比纯粹的结构相等更智能的相等性检查。如上所述,这是一个坏主意,因为太多东西隐式地假设 == 仅在结构上起作用。相反,使用函数或方法来实现更智能的相等性检查(例如,equals 方法)。

    另一种选择是定义一个 规范化 方法,该方法将对象置于规范形式(例如,通过 doit),以便,例如,x.doit() == y.doit()xy 在数学上相等时为真。这并不总是可行的,因为并非每种类型的对象都具有可计算的规范形式,但在存在规范形式时,这是一种方便的方法。

  • 使 == 检查表达式 args 中存储的属性之外的一些其他属性。请参见上面的 避免在对象上存储额外的属性 部分,以了解为什么直接在 SymPy 对象上存储额外的属性是一个坏主意,以及最好的替代方法是什么。

  • 为了使 == 与某些非 SymPy 对象比较相等。最好扩展 sympify 以便能够将此对象转换为 SymPy 对象。默认的 __eq__ 实现将自动在另一个参数上调用 sympify,如果它不是 Basic 实例(例如,Integer(1) == int(1) 给出 True)。可以扩展 sympify 针对你控制的对象(通过定义 _sympy_ 方法)和你不控制的对象(通过扩展 converter 字典)。有关更多详细信息,请参阅 sympify() 文档。

避免假设处理程序导致的无限递归

在自定义函数(如 _eval_is_positive)上编写假设处理程序时(有关如何执行此操作的详细信息,请参阅 自定义函数指南),有两点需要注意。

首先,避免在假设处理程序中创建新的表达式。你应该始终直接拆解函数的参数。 原因是创建新的表达式本身可能会导致假设查询。这很容易导致无限递归。即使没有,创建本身会导致许多递归假设查询的新表达式对于性能来说也不如直接查询所需属性好。

这通常意味着使用像 as_independent(){meth}~.as_coeff_muland checking theargs` of expressions directly (see the custom functions guide for an example).

其次,不要在假设处理程序中递归地评估 self 上的假设。 假设处理程序应该只检查 self.args 上的假设。全局假设系统将自动处理不同假设之间的推论。

例如,你可能很想写一些类似以下内容的内容

# BAD

class f(Function):
    def _eval_is_integer(self):
        # Quick return if self is not real (do not do this).
        if self.is_real is False:
            return False
        return self.args[0].is_integer

然而,if self.is_real is False 检查是完全没有必要的。假设系统已经知道 integer 意味着 real,并且如果它已经知道 is_real 为 False,它不会费心检查 is_integer

如果你以这种方式定义函数,它将导致无限递归

>>> class f(Function):
...     def _eval_is_integer(self):
...         if self.is_real is False:
...             return False
...         return self.args[0].is_integer
>>> f(x).is_real
Traceback (most recent call last):
...
RecursionError: maximum recursion depth exceeded while calling a Python object

相反,根据函数的参数来定义处理程序

# GOOD

class f(Function):
    def _eval_is_integer(self):
        return self.args[0].is_integer