陷阱

首先,我们应该对 SymPy 有所了解。SymPy 只不过是一个 Python 库,就像 NumPyDjango,甚至是 Python 标准库中的模块 sysre。这意味着 SymPy 并没有为 Python 语言添加任何东西。Python 语言固有的限制在 SymPy 中也同样存在。这也意味着 SymPy 尽可能地使用 Python 的习惯用法,使得那些已经熟悉 Python 编程的人使用 SymPy 编程变得容易。作为一个简单的例子,SymPy 使用 Python 语法来构建表达式。隐式乘法(如 3x3 x)在 Python 中是不允许的,因此在 SymPy 中也不允许。要乘以 3x,你必须用 * 输入 3*x

符号

这一事实的一个结果是,SymPy 可以在任何可以使用 Python 的环境中使用。我们只需像导入其他库一样导入它:

>>> from sympy import *

这将导入SymPy中的所有函数和类到我们的交互式Python会话中。现在,假设我们开始进行计算。

>>> x + 1
Traceback (most recent call last):
...
NameError: name 'x' is not defined

哎呀!这里发生了什么?我们尝试使用变量 x,但它告诉我们 x 未定义。在 Python 中,变量在被定义之前没有任何意义。SymPy 也不例外。与您可能使用过的许多符号处理系统不同,在 SymPy 中,变量不会自动定义。要定义变量,我们必须使用 symbols

>>> x = symbols('x')
>>> x + 1
x + 1

symbols 接受一个由空格或逗号分隔的变量名组成的字符串,并从中创建符号。然后,我们可以将这些符号赋值给变量名。稍后,我们将探讨一些可以解决这个问题的便捷方法。现在,让我们只为本节其余部分定义最常见的变量名 xyz

>>> x, y, z = symbols('x y z')

最后,我们注意到符号的名称和它所赋值的变量名称之间不需要有任何关系。

>>> a, b = symbols('b a')
>>> a
b
>>> b
a

这里我们做了一件非常令人困惑的事情,将名为 a 的符号赋值给变量 b,并将名为 b 的符号赋值给变量 a。现在,名为 a 的 Python 变量指向名为 b 的 SymPy 符号,反之亦然。多么令人困惑。我们也可以做类似的事情。

>>> crazy = symbols('unrelated')
>>> crazy + 1
unrelated + 1

这也表明,如果我们愿意,符号可以有超过一个字符的名字。

通常,最佳实践是将符号分配给同名的 Python 变量,尽管也有例外:符号名称可以包含 Python 变量名称中不允许的字符,或者可能只是希望通过将长名称的符号分配给单字母 Python 变量来避免输入长名称。

为了避免混淆,在本教程中,符号名称和Python变量名称将始终一致。此外,“Symbol”一词将指代SymPy符号,而“variable”一词将指代Python变量。

最后,让我们确保理解 SymPy 符号和 Python 变量之间的区别。考虑以下:

x = symbols('x')
expr = x + 1
x = 2
print(expr)

你认为这段代码的输出会是什么?如果你认为是 3,那就错了。让我们看看实际会发生什么。

>>> x = symbols('x')
>>> expr = x + 1
>>> x = 2
>>> print(expr)
x + 1

x 改为 2expr 没有影响。这是因为 x = 2 将 Python 变量 x 改为 2,但对我们在创建 expr 时使用的 SymPy 符号 x 没有影响。当我们创建 expr 时,Python 变量 x 是一个符号。创建后,我们将 Python 变量 x 改为 2。但 expr 保持不变。这种行为并不是 SymPy 独有的。所有 Python 程序都是这样工作的:如果一个变量被改变,已经用该变量创建的表达式不会自动改变。例如

>>> x = 'abc'
>>> expr = x + 'def'
>>> expr
'abcdef'
>>> x = 'ABC'
>>> expr
'abcdef'

在这个例子中,如果我们想知道 exprx 的新值下是什么,我们需要重新评估创建 expr 的代码,即 expr = x + 1。如果多行代码创建了 expr,这可能会变得复杂。使用像 SymPy 这样的符号计算系统的一个优点是,我们可以为 expr 构建一个符号表示,然后用值替换 x。在 SymPy 中正确执行此操作的方法是使用 subs,这将在后面更详细地讨论。

>>> x = symbols('x')
>>> expr = x + 1
>>> expr.subs(x, 2)
3

等号

另一个非常重要的结果是,SymPy 不扩展 Python 语法,这意味着 = 在 SymPy 中不代表相等。相反,它是 Python 变量赋值。这是 Python 语言中硬编码的,SymPy 没有尝试改变这一点。

然而,你可能会认为 == ,在 Python 中用于相等性测试,在 SymPy 中也是用于相等性。这也不完全正确。让我们看看当我们使用 == 时会发生什么。

>>> x + 1 == 4
False

我们没有将 x + 1 == 4 符号化处理,而是直接得到了 False。 在 SymPy 中,== 表示精确的结构相等性测试。 这意味着 a == b 意味着我们 询问 是否 \(a = b\)。 我们总是得到一个 bool 作为 == 的结果。 有一个单独的对象,称为 Eq,可以用来创建符号等式。

>>> Eq(x + 1, 4)
Eq(x + 1, 4)

关于 == 还有一个额外的注意事项。假设我们想知道 \((x + 1)^2 = x^2 + 2x + 1\)。我们可能会尝试这样做:

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

我们再次得到了 False。然而,\((x + 1)^2\) 确实 等于 \(x^2 + 2x + 1\)。这里发生了什么?我们是在SymPy中发现了一个错误,还是它不足以识别这个基本的代数事实?

回顾上面提到的,== 表示 精确 的结构相等测试。这里的“精确”意味着两个表达式只有在结构上完全相等时才会通过 == 比较相等。这里,\((x + 1)^2\)\(x^2 + 2x + 1\) 在结构上并不相同。一个是两个项相加的幂,另一个是三个项的相加。

事实证明,当使用 SymPy 作为库时,== 用于测试精确的结构相等性比用于表示符号相等性或测试数学相等性要实用得多。然而,作为新用户,您可能更关心后两者。我们已经看到了一个替代方案来表示符号相等性,即 Eq。要测试两个事物是否相等,最好是记住一个基本事实:如果 \(a = b\),那么 \(a - b = 0\)。因此,检查 \(a = b\) 的最佳方法是取 \(a - b\) 并简化它,看看它是否变为 0。我们将在后面学习到,执行这个操作的函数叫做 simplify。这种方法并非万无一失——实际上,可以 理论上证明 在一般情况下无法确定两个符号表达式是否完全相等——但对于大多数常见表达式,它工作得相当好。

>>> a = (x + 1)**2
>>> b = x**2 + 2*x + 1
>>> simplify(a - b)
0
>>> c = x**2 - 2*x + 1
>>> simplify(a - c)
4*x

还有一个名为 equals 的方法,它通过在随机点上数值评估两个表达式来测试它们是否相等。

>>> a = cos(x)**2 - sin(x)**2
>>> b = cos(2*x)
>>> a.equals(b)
True

最后两点:^/

你可能已经注意到,我们一直在使用 ** 进行幂运算,而不是标准的 ^。这是因为 SymPy 遵循 Python 的惯例。在 Python 中,^ 表示逻辑异或。SymPy 遵循这一惯例:

>>> True ^ False
True
>>> True ^ True
False
>>> Xor(x, y)
x ^ y

最后,有必要对 SymPy 的工作原理进行一个小型的技术讨论。当你输入类似 x + 1 的内容时,SymPy 的符号 x 会被加到 Python 的整数 1 上。Python 的运算符规则随后允许 SymPy 告诉 Python,SymPy 对象知道如何与 Python 整数相加,因此 1 会自动转换为 SymPy 的整数对象。

这种操作符的魔法在幕后自动发生,你很少需要知道它的发生。然而,有一个例外。每当你结合一个SymPy对象和一个SymPy对象,或者一个SymPy对象和一个Python对象时,你会得到一个SymPy对象,但每当你结合两个Python对象时,SymPy永远不会介入,因此你会得到一个Python对象。

>>> type(Integer(1) + 1)
<class 'sympy.core.numbers.Integer'>
>>> type(1 + 1)
<... 'int'>

这通常不是什么大问题。Python 的整数与 SymPy 的整数工作方式大致相同,但有一个重要的例外:除法。在 SymPy 中,两个整数的除法会得到一个有理数(这与 Python 的 Fraction 类似):

>>> Integer(1)/Integer(3)
1/3
>>> type(Integer(1)/Integer(3))
<class 'sympy.core.numbers.Rational'>

但在Python 3中,/ 表示浮点数除法:

>>> 1/2
0.5

为了避免这种情况,我们可以显式地构造有理数对象。

>>> Rational(1, 2)
1/2

每当我们有一个包含 int/int 的较大符号表达式时,这个问题也会出现。例如:

>>> x + 1/2
x + 0.5

这是因为Python首先将 1/2 计算为 0.5,然后在将其添加到 x 时,该值被转换为SymPy类型。同样,我们可以通过显式创建一个Rational来解决这个问题:

>>> x + Rational(1, 2)
x + 1/2

陷阱与误区 文档中有几个避免这种情况的建议。

进一步阅读

关于本节涵盖的主题的更多讨论,请参见 陷阱与误区