陷阱

首先,我们应该澄清 SymPy 的一些问题。SymPy 只是一个 Python 库,就像 NumPyDjango,甚至是 Python 标准库中的模块 sysre。这意味着 SymPy 不会向 Python 语言添加任何内容。Python 语言固有的限制在 SymPy 中也存在。这也意味着 SymPy 尽可能地使用 Python 习惯用法,使那些熟悉 Python 编程的人更容易使用 SymPy 进行编程。举个简单的例子,SymPy 使用 Python 语法来构建表达式。Python 中不允许隐式乘法(如 3x3 x),因此 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 变量名称始终一致。此外,“符号”一词将指代 SymPy 符号,“变量”一词将指代 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,但对 SymPy 符号 x 没有影响,而我们正是使用它来创建 expr 的。当我们创建 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 int 1 中。然后,Python 的运算符规则允许 SymPy 告诉 Python,SymPy 对象知道如何与 Python int 相加,因此 1 会自动转换为 SymPy Integer 对象。

这种运算符魔法会自动在后台发生,您很少需要知道它正在发生。但是,有一个例外。无论何时您将 SymPy 对象和 SymPy 对象组合在一起,或者将 SymPy 对象和 Python 对象组合在一起,您都会得到一个 SymPy 对象,但是无论何时您将两个 Python 对象组合在一起,SymPy 永远不会起作用,因此您会得到一个 Python 对象。

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

这通常不是什么大问题。Python int 与 SymPy Integers 的工作方式基本相同,但有一个重要的例外:除法。在 SymPy 中,两个 Integers 的除法会得到一个 Rational

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

但在 Python 中,/ 表示整数除法或浮点除法,具体取决于您是在 Python 2 中还是 Python 3 中,以及您是否在 Python 2 中运行了 from __future__ import division,而 Python 2 在 SymPy 1.5.1 以上版本中不再受支持。

>>> from __future__ import division
>>> 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

陷阱和缺陷文档中,提供了一些关于如何避免这种情况的技巧。

进一步阅读

有关本节中介绍的主题的更多讨论,请参阅陷阱和缺陷