常见问题和陷阱

介绍

SymPy 在 Python 编程语言 下运行,因此某些行为可能与其他独立的计算机代数系统(如 Maple 或 Mathematica)不同。以下是一些在使用 SymPy 时可能遇到的常见问题和陷阱。另请参阅 入门教程、SymPy 文档的其余部分以及 官方 Python 教程

如果您已熟悉 C 或 Java,您可能还想查看此 4 分钟 Python 教程

忽略示例中的 #doctest: +SKIP。这与示例的内部测试有关。

等号 (=)

单个等号

等号 (=) 是赋值运算符,而不是相等运算符。如果您要进行 \(x = y\),请使用 Eq(x, y) 表示相等。或者,所有表达式都被认为等于零,因此您可以只减去一边并使用 x - y

等号的正确用法是将表达式赋给变量。

例如

>>> from sympy.abc import x, y
>>> a = x - y
>>> print(a)
x - y

双等号

双等号 (==) 用于测试相等性。但是,这会精确地测试表达式,而不是符号地。例如

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

如果你想要测试符号等式,一种方法是从另一个表达式中减去一个表达式,并将其通过函数运行,比如 expand()simplify()trigsimp(),看看方程是否简化为 0。

>>> from sympy import simplify, cos, sin, expand
>>> simplify((x + 1)**2 - (x**2 + 2*x + 1))
0
>>> eq = sin(2*x) - 2*sin(x)*cos(x)
>>> simplify(eq)
0
>>> expand(eq, trig=True)
0

注意

另请参见 结构等式术语表 中。

变量

变量赋值不会在表达式之间创建关系

当你使用 = 进行赋值时,请记住,在 Python 中,就像在大多数编程语言中一样,如果你改变你分配给它的值,变量不会改变。你输入的方程使用创建时的值来“填充”值,就像普通的 Python 定义一样。它们不会被之后所做的更改所改变。考虑以下情况

>>> from sympy import Symbol
>>> a = Symbol('a')  # Symbol, `a`, stored as variable "a"
>>> b = a + 1        # an expression involving `a` stored as variable "b"
>>> print(b)
a + 1
>>> a = 4            # "a" now points to literal integer 4, not Symbol('a')
>>> print(a)
4
>>> print(b)          # "b" is still pointing at the expression involving `a`
a + 1

改变数量 a 不会改变 b;你并不是在处理一组联立方程。记住,当你打印一个引用 SymPy 对象的变量时,打印出来的字符串是它在创建时被赋予的字符串;该字符串不必与你分配给它的变量相同,这可能会有所帮助。

>>> from sympy import var
>>> r, t, d = var('rate time short_life')
>>> d = r*t
>>> print(d)
rate*time
>>> r = 80
>>> t = 2
>>> print(d)        # We haven't changed d, only r and t
rate*time
>>> d = r*t
>>> print(d)        # Now d is using the current values of r and t
160

如果你需要相互依赖的变量,你可以定义函数。使用 def 运算符。缩进函数体。有关定义函数的更多信息,请参见 Python 文档。

>>> c, d = var('c d')
>>> print(c)
c
>>> print(d)
d
>>> def ctimesd():
...     """
...     This function returns whatever c is times whatever d is.
...     """
...     return c*d
...
>>> ctimesd()
c*d
>>> c = 2
>>> print(c)
2
>>> ctimesd()
2*d

如果你定义了一个循环关系,你将得到一个 RuntimeError

>>> def a():
...     return b()
...
>>> def b():
...     return a()
...
>>> a() 
Traceback (most recent call last):
  File "...", line ..., in ...
    compileflags, 1) in test.globs
  File "<...>", line 1, in <module>
    a()
  File "<...>", line 2, in a
    return b()
  File "<...>", line 2, in b
    return a()
  File "<...>", line 2, in a
    return b()
...
RuntimeError: maximum recursion depth exceeded

注意

另请参见 不可变术语表 中。

符号

符号是变量,与所有其他变量一样,它们需要在使用之前被赋值。例如

>>> import sympy
>>> z**2  # z is not defined yet 
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'z' is not defined
>>> sympy.var('z')  # This is the easiest way to define z as a standard symbol
z
>>> z**2
z**2

如果你使用 isympy,它会为你运行以下命令,为你提供一些默认的符号和函数。

>>> from __future__ import division
>>> from sympy import *
>>> x, y, z, t = symbols('x y z t')
>>> k, m, n = symbols('k m n', integer=True)
>>> f, g, h = symbols('f g h', cls=Function)

你也可以从 sympy.abc 中导入常见的符号名称。

>>> from sympy.abc import w
>>> w
w
>>> import sympy
>>> dir(sympy.abc)  
['A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O',
'P', 'Q', 'R', 'S', 'Symbol', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
'__builtins__', '__doc__', '__file__', '__name__', '__package__', '_greek',
'_latin', 'a', 'alpha', 'b', 'beta', 'c', 'chi', 'd', 'delta', 'e',
'epsilon', 'eta', 'f', 'g', 'gamma', 'h', 'i', 'iota', 'j', 'k', 'kappa',
'l', 'm', 'mu', 'n', 'nu', 'o', 'omega', 'omicron', 'p', 'phi', 'pi',
'psi', 'q', 'r', 'rho', 's', 'sigma', 't', 'tau', 'theta', 'u', 'upsilon',
'v', 'w', 'x', 'xi', 'y', 'z', 'zeta']

如果你想要控制变量的假设,请使用 Symbolsymbols()。请参阅下文的 关键字参数

最后,建议不要使用 IESNCOQ 作为变量或符号名称,因为它们分别用于虚数单位 (\(i\))、自然对数的底 (\(e\))、sympify() 函数(见下文的 符号表达式)、数值求值(N() 等效于 evalf())、大 O 阶符号(如 \(O(n\log{n})\))以及保存支持的询问键列表(如 Q.real)的假设对象。你可以使用助记符 OSINEQ 来记住 SymPy 中默认定义了哪些符号。或者更好的是,始终使用小写字母作为符号名称。Python 不会阻止你覆盖默认的 SymPy 名称或函数,所以要小心。

>>> cos(pi)  # cos and pi are a built-in sympy names.
-1
>>> pi = 3   # Notice that there is no warning for overriding pi.
>>> cos(pi)
cos(3)
>>> def cos(x):  # No warning for overriding built-in functions either.
...     return 5*x
...
>>> cos(pi)
15
>>> from sympy import cos  # reimport to restore normal behavior

要获得 SymPy 中所有默认名称的完整列表,请执行以下操作

>>> import sympy
>>> dir(sympy)  
# A big list of all default sympy names and functions follows.
# Ignore everything that starts and ends with __.

如果你安装了 IPython 并使用 isympy,你也可以按 TAB 键来获取所有内置名称的列表并自动完成。此外,请参阅 此页面,了解在常规 Python 控制台中获取制表符完成的技巧。

注意

另请参阅 定义符号 部分的 最佳实践 页面。

函数

一个像 f(x) 这样的函数可以通过定义函数和变量来创建

>>> from sympy import Function
>>> f = Function('f')
>>> x = Symbol('x')
>>> f(x)
f(x)

如果你将 f(x) 赋值给 Python 变量 \(f\),你将失去复制和粘贴该函数或创建具有不同参数的函数的能力:Function('f') 是可调用的,但 Function('f')(x) 不是

>>> f1 = Function('f1')
>>> f2 = Function('f2')('x')
>>> f1
f1
>>> f2
f2(x)
>>> f1(1)
f1(1)
>>> f2(1)
Traceback (most recent call last):
...
TypeError: 'f2' object is not callable
>>> f2.subs(x, 1)
f2(1)

符号表达式

Python 数字与 SymPy 数字

SymPy 使用它自己的类来表示整数、有理数和浮点数,而不是默认的 Python intfloat 类型,因为它允许更多的控制。但你必须小心。如果你输入一个只包含数字的表达式,它将默认为 Python 表达式。使用 sympify() 函数,或者只是 S,以确保某物是 SymPy 表达式。

>>> 6.2  # Python float. Notice the floating point accuracy problems.
6.2000000000000002
>>> type(6.2)  # <type 'float'> in Python 2.x,  <class 'float'> in Py3k
<... 'float'>
>>> S(6.2)  # SymPy Float has no such problems because of arbitrary precision.
6.20000000000000
>>> type(S(6.2))
<class 'sympy.core.numbers.Float'>

如果你在 SymPy 表达式中包含数字,它们将被自动符号化,但有一个需要注意的地方。如果你在 SymPy 表达式中执行 <number>/<number>,Python 将在 SymPy 有机会获取它们之前对这两个数字进行求值。解决方法是对其中一个数字使用 sympify(),或者使用 Rational

>>> x**(1/2)  # evaluates to x**0 or x**0.5
x**0.5
>>> x**(S(1)/2)  # sympyify one of the ints
sqrt(x)
>>> x**Rational(1, 2)  # use the Rational class
sqrt(x)

对于 1/2 的幂,你也可以使用 sqrt 简写

>>> sqrt(x) == x**Rational(1, 2)
True

如果两个整数没有被除号直接隔开,那么你不必担心这个问题

>>> x**(2*x/3)
x**(2*x/3)

注意

一个常见的错误是复制打印的表达式并重复使用它。如果表达式中有一个 Rational(即 <number>/<number>),你将不会得到相同的结果,而是得到 Python 对除法的结果,而不是 SymPy 有理数。

>>> x = Symbol('x')
>>> print(solve(7*x -22, x))
[22/7]
>>> 22/7  # If we just copy and paste we get int 3 or a float
3.142857142857143
>>> # One solution is to just assign the expression to a variable
>>> # if we need to use it again.
>>> a = solve(7*x - 22, x)[0]
>>> a
22/7

另一种解决方法是在表达式周围加上引号,并通过 S()(即将其符号化)运行它

>>> S("22/7")
22/7

此外,如果你没有使用 isympy,你可以使用 from __future__ import division 来防止 / 符号执行 整数除法

>>> from __future__ import division
>>> 1/2   # With division imported it evaluates to a python float
0.5
>>> 1//2  # You can still achieve integer division with //
0

但要小心:你现在将收到浮点数,而你可能希望得到有理数

>>> x**(1/2)
x**0.5

Rational 仅适用于 number/number,并且仅适用于有理数。如果你想要一个包含符号或表达式的分数,只需使用 /。如果你执行 number/expression 或 expression/number,那么该数字将自动转换为 SymPy 数字。你只需要注意 number/number。

>>> Rational(2, x)
Traceback (most recent call last):
...
TypeError: invalid input: x
>>> 2/x
2/x

使用浮点数和有理数求值表达式

SymPy 会跟踪 Float 对象的精度。默认精度为 15 位。当包含 Float 的表达式被求值时,结果将以 15 位精度表示,但这些位数(取决于参与计算的数字)可能并不都具有意义。

首先要记住的是 Float 是如何创建的:它使用一个值和一个精度创建的。精度指示当该 Float(或它出现的表达式)被求值时,使用多精确的值。

这些值可以作为字符串、整数、浮点数或有理数给出。

  • 字符串和整数被解释为精确的

>>> Float(100)
100.000000000000
>>> Float('100', 5)
100.00
  • 为了使精度与数字位数匹配,可以使用空字符串作为精度。

>>> Float(100, '')
100.
>>> Float('12.34')
12.3400000000000
>>> Float('12.34', '')
12.34
>>> s, r = [Float(j, 3) for j in ('0.25', Rational(1, 7))]
>>> for f in [s, r]:
...     print(f)
0.250
0.143

接下来,请注意这些值中的每一个在 3 位数中看起来都是正确的。但是,如果我们尝试将它们评估到 20 位数,就会发现差异。

精度为 3 的 0.25 表示一个具有非循环二进制小数的数字;1/7 在二进制和小数中是循环的 - 它无法被准确地表示到这 3 位数之后太远(正确的小数是一个循环的 142857)。

>>> s.n(20)
0.25000000000000000000
>>> r.n(20)
0.14285278320312500000

重要的是要意识到,尽管 Float 以任意精度以小数形式显示,但它实际上是存储在二进制中的。一旦创建 Float,它的二进制信息就会以给定的精度设置。该值的精度无法随后更改;因此,精度为 3 位的 1/7 可以用二进制零填充,但这不会使它成为 1/7 的更精确值。

如果精度较低的数字参与了高精度值的计算,evalf 引擎会提高精度较低的值的精度,并将得到不精确的结果。这是有限精度计算的一个特征。

>>> Float('0.1', 10) + Float('0.1', 3)
0.2000061035

尽管 evalf 引擎试图保持 10 位精度(因为这是表示的最高精度),但所使用的 3 位精度将精度限制在约 4 位 - 您看到的并非所有数字都是有效的。evalf 不会试图跟踪有效数字的位数。

这个非常简单的表达式涉及两个具有不同精度的数字相加,希望它能帮助您理解为什么更复杂的表达式(如可能未简化的三角表达式)即使在适当的简化后,也不会评估为精确的零,因为它们应该是零。请考虑这个未简化的三角恒等式,它乘以一个很大的数字。

>>> big = 12345678901234567890
>>> big_trig_identity = big*cos(x)**2 + big*sin(x)**2 - big*1
>>> abs(big_trig_identity.subs(x, .1).n(2)) > 1000
True

\(\cos\)\(\sin\) 项以 15 位精度进行评估并乘以该大数时,它们得出了一个仅精确到 15 位(大约)的大数,当从该 20 位大数中减去时,结果不是零。

有三件事可以帮助您获得表达式更精确的数值。

1) 在调用评估时传递所需的替换。通过首先进行替换,Float 值无法根据需要更新。通过在调用 evalf 时传递所需的替换,可以获得重新评估的必要能力,并且结果令人印象深刻地更好。

>>> big_trig_identity.n(2, {x: 0.1})
-0.e-91

2) 使用有理数而不是浮点数。在评估过程中,有理数可以计算到任意精度,而 Float,一旦创建 - 默认情况下为 15 位 - 则无法。将上面 -1.4e+3 的值与在调用评估之前用表示 1/10 的有理数替换 x 时获得的几乎为零的值进行比较。

>>> big_trig_identity.subs(x, S('1/10')).n(2)
0.e-91

3) 尝试简化表达式。在本例中,SymPy 将识别三角恒等式并将其简化为零,因此您甚至不需要在数值上进行评估。

>>> big_trig_identity.simplify()
0

表达式的不可变性

SymPy 中的表达式是不可变的,无法通过就地操作进行修改。这意味着函数将始终返回一个对象,并且原始表达式不会被修改。以下示例代码段演示了它是如何工作的。

def main():
    var('x y a b')
    expr = 3*x + 4*y
    print('original =', expr)
    expr_modified = expr.subs({x: a, y: b})
    print('modified =', expr_modified)

if __name__ == "__main__":
    main()

输出显示 subs() 函数已将变量 x 替换为变量 a,将变量 y 替换为变量 b

original = 3*x + 4*y
modified = 3*a + 4*b

subs() 函数不会修改原始表达式 expr。相反,它会返回表达式的修改副本。该返回对象存储在变量 expr_modified 中。请注意,与 C/C++ 和其他高级语言不同,Python 不需要您在使用变量之前声明它。

数学运算符

SymPy 使用与 Python 相同的默认运算符。其中大多数,如 */+-,是标准的。除了上面 Python 数字与 SymPy 数字 中讨论的整除之外,您还应该注意不允许隐式乘法。您需要在希望将某个东西乘以另一个东西时使用 *。此外,要将某个东西提高到幂,使用 **,而不是 ^,因为许多计算机代数系统使用 ^。括号 () 会按预期改变运算符优先级。

isympy 中,使用 ipython shell

>>> 2x
Traceback (most recent call last):
...
SyntaxError: invalid syntax
>>> 2*x
2*x
>>> (x + 1)^2  # This is not power.  Use ** instead.
Traceback (most recent call last):
...
TypeError: unsupported operand type(s) for ^: 'Add' and 'int'
>>> (x + 1)**2
(x + 1)**2
>>> pprint(3 - x**(2*x)/(x + 1))
    2*x
   x
- ----- + 3
  x + 1

反三角函数

SymPy 使用与大多数计算机代数系统不同的函数名称。特别地,反三角函数使用 python 的名称 asinacos 等,而不是通常的 arcsinarccos。使用上面 符号 中描述的方法查看所有 SymPy 函数的名称。

Sqrt 不是函数

与存在指数函数 (exp) 不同,没有 sqrt 函数。 sqrt(x) 用于表示 Pow(x, S(1)/2),因此如果您想知道某个表达式中是否包含任何平方根,expr.has(sqrt) 将不起作用。您必须查找指数为二分之一(或如果在分母中为负二分之一,例如)的 Pow

>>> (y + sqrt(x)).find(Wild('w')**S.Half)
{sqrt(x)}
>>> (y + 1/sqrt(x)).find(Wild('w')**-S.Half)
{1/sqrt(x)}

如果您对 sqrt 的任何幂感兴趣,那么以下模式将是合适的。

>>> sq = lambda s: s.is_Pow and s.exp.is_Rational and s.exp.q == 2
>>> (y + sqrt(x)**3).find(sq)
{x**(3/2)}

特殊符号

符号 []{}=() 在 Python 以及 SymPy 中具有特殊含义。有关更多信息,请参阅上面链接到的 Python 文档。

列表

方括号 [] 表示一个列表。列表是一个容器,可以容纳任意数量的不同对象。列表可以包含任何内容,包括不同类型的内容。列表是可变的,这意味着您可以在创建列表后更改列表的元素。您还可以使用方括号访问列表的元素,并将它们放在列表或列表变量之后。使用元素之前的空格对元素进行编号。

注意

列表索引从 0 开始。

示例

>>> a = [x, 1]  # A simple list of two items
>>> a
[x, 1]
>>> a[0]  # This is the first item
x
>>> a[0] = 2  # You can change values of lists after they have been created
>>> print(a)
[2, 1]
>>> print(solve(x**2 + 2*x - 1, x)) # Some functions return lists
[-1 + sqrt(2), -sqrt(2) - 1]

注意

有关列表和方括号表示法以访问列表元素的更多信息,请参阅 Python 文档。

字典

花括号 {} 表示字典,或简称 dict。字典是一个无序的非重复键和值列表。语法是 {key: value}。您可以使用方括号表示法访问键的值。

>>> d = {'a': 1, 'b': 2}  # A dictionary.
>>> d
{'a': 1, 'b': 2}
>>> d['a']  # How to access items in a dict
1
>>> roots((x - 1)**2*(x - 2), x)  # Some functions return dicts
{1: 2, 2: 1}
>>> # Some SymPy functions return dictionaries.  For example,
>>> # roots returns a dictionary of root:multiplicity items.
>>> roots((x - 5)**2*(x + 3), x)
{-3: 1, 5: 2}
>>> # This means that the root -3 occurs once and the root 5 occurs twice.

注意

有关字典的更多信息,请参阅 Python 文档。

元组

括号 () 除了改变运算符优先级及其在函数调用中的使用(如 cos(x))之外,还用于元组。 tuplelist 相同,只是它不可变。这意味着您无法在创建后更改它们的值。通常情况下,您不需要 SymPy 中的元组,但有时键入括号比键入方括号更方便。

>>> t = (1, 2, x)  # Tuples are like lists
>>> t
(1, 2, x)
>>> t[0]
1
>>> t[0] = 4  # Except you cannot change them after they have been created
Traceback (most recent call last):
  File "<console>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment

单元素元组与列表不同,必须包含一个逗号。

>>> (x,)
(x,)

如果没有逗号,没有逗号的单个表达式不是元组。

>>> (x)
x

integrate 如果您想使用限制进行积分(并且元组或列表都可以)会将一个序列作为第二个参数。

>>> integrate(x**2, (x, 0, 1))
1/3
>>> integrate(x**2, [x, 0, 1])
1/3

注意

有关元组的更多信息,请参阅 Python 文档。

关键字参数

除了上面 描述 的用法之外,等号 (=) 还用于向函数提供命名参数。任何在参数列表中具有 key=value 的函数(请参阅下面的关于如何找出这一点的信息),则默认情况下 key 设置为 value。您可以通过在函数调用中使用等号提供自己的值来更改键的值。此外,参数列表中具有 ** 后跟名称的函数(通常是 **kwargs**assumptions)允许您添加任意数量的 key=value 对,它们都将根据函数进行评估。

sqrt(x**2) 不会自动简化为 x,因为默认情况下 x 被假定为复数,例如 sqrt((-1)**2) == sqrt(1) == 1 != -1

>>> sqrt(x**2)
sqrt(x**2)

向符号提供假设是使用关键字参数的一个示例。

>>> x = Symbol('x', positive=True)

平方根现在将简化为 x,因为它知道 x >= 0

>>> sqrt(x**2)
x

powsimp 的默认参数为 combine='all'

>>> pprint(powsimp(x**n*x**m*y**n*y**m))
     m + n
(x*y)

将 combine 设置为默认值与不设置它相同。

>>> pprint(powsimp(x**n*x**m*y**n*y**m, combine='all'))
     m + n
(x*y)

非默认选项是 'exp',它组合指数…

>>> pprint(powsimp(x**n*x**m*y**n*y**m, combine='exp'))
 m + n  m + n
x     *y

…以及 'base',它组合底数。

>>> pprint(powsimp(x**n*x**m*y**n*y**m, combine='base'))
     m      n
(x*y) *(x*y)

注意

有关函数参数的更多信息,请参阅 Python 文档。

从 SymPy 内部获取帮助

help()

虽然所有文档都可以在 docs.sympy.orgSymPy Wiki 上找到,但您也可以从运行 SymPy 的 Python 解释器中获取有关函数的信息。最简单的方法是执行 help(function),或者如果您使用的是 ipython,则执行 function?

In [1]: help(powsimp)  # help() works everywhere

In [2]: # But in ipython, you can also use ?, which is better because it
In [3]: # it gives you more information
In [4]: powsimp?

这些将为您提供 powsimp() 的函数参数和文档字符串。输出将类似于以下内容

sympy.simplify.simplify.powsimp(expr, deep=False, combine='all', force=False, measure=<function count_ops>)[source]

通过组合具有相似底数和指数的幂来简化表达式。

解释

如果 deepTrue,则 powsimp() 还将简化函数的参数。默认情况下,deep 设置为 False

如果 forceTrue,则将在不检查假设的情况下组合底数,例如 sqrt(x)*sqrt(y) -> sqrt(x*y),如果 x 和 y 都为负数,则不成立。

您可以通过更改 combine=’base’ 或 combine=’exp’ 使 powsimp() 仅组合底数或仅组合指数。默认情况下,combine=’all’,它同时执行两者。combine=’base’ 将仅组合

 a   a          a                          2x      x
x * y  =>  (x*y)   as well as things like 2   =>  4

而 combine=’exp’ 将仅组合

 a   b      (a + b)
x * x  =>  x

combine=’exp’ 将严格地仅以以前自动的方式组合指数。如果需要旧行为,请使用 deep=True。

当 combine=’all’ 时,‘exp’ 首先被评估。请考虑以下第一个示例,了解何时可能存在与之相关的歧义。这样做是为了使像第二个示例这样的内容能够完全组合。如果您希望 ‘base’ 首先组合,请执行类似 powsimp(powsimp(expr, combine=’base’), combine=’exp’) 的操作。

示例

>>> from sympy import powsimp, exp, log, symbols
>>> from sympy.abc import x, y, z, n
>>> powsimp(x**y*x**z*y**z, combine='all')
x**(y + z)*y**z
>>> powsimp(x**y*x**z*y**z, combine='exp')
x**(y + z)*y**z
>>> powsimp(x**y*x**z*y**z, combine='base', force=True)
x**y*(x*y)**z
>>> powsimp(x**z*x**y*n**z*n**y, combine='all', force=True)
(n*x)**(y + z)
>>> powsimp(x**z*x**y*n**z*n**y, combine='exp')
n**(y + z)*x**(y + z)
>>> powsimp(x**z*x**y*n**z*n**y, combine='base', force=True)
(n*x)**y*(n*x)**z
>>> x, y = symbols('x y', positive=True)
>>> powsimp(log(exp(x)*exp(y)))
log(exp(x)*exp(y))
>>> powsimp(log(exp(x)*exp(y)), deep=True)
x + y

如果 combine=’exp’,则具有 Mul 底数的根式将被组合

>>> from sympy import sqrt
>>> x, y = symbols('x y')

两个根式会自动通过 Mul 连接

>>> a=sqrt(x*sqrt(y))
>>> a*a**3 == a**4
True

但如果该根式的整数幂已被自动展开,则 Mul 不会连接生成的因子

>>> a**4 # auto expands to a Mul, no longer a Pow
x**2*y
>>> _*a # so Mul doesn't combine them
x**2*y*sqrt(x*sqrt(y))
>>> powsimp(_) # but powsimp will
(x*sqrt(y))**(5/2)
>>> powsimp(x*y*a) # but won't when doing so would violate assumptions
x*y*sqrt(x*sqrt(y))