符号和模糊布尔值

本页描述了SymPy中的符号 Boolean 是什么,以及它与SymPy许多部分中使用的三值模糊布尔值的关系。还讨论了在使用三值逻辑编写代码时出现的常见问题,以及如何正确处理这些问题。

符号布尔值 vs 三值布尔值

假设查询如 x.ispositive 给出模糊布尔 TrueFalseNone 结果 [1]。这些是低级 Python 对象,而不是 SymPy 的符号 Boolean 表达式。

>>> from sympy import Symbol, symbols
>>> xpos = Symbol('xpos', positive=True)
>>> xneg = Symbol('xneg', negative=True)
>>> x = Symbol('x')
>>> print(xpos.is_positive)
True
>>> print(xneg.is_positive)
False
>>> print(x.is_positive)
None

作为模糊布尔值的 None 结果应被解释为“可能”或“未知”。

在 SymPy 中,使用不等式时可以找到一个符号 Boolean 类的示例。当一个不等式不知道是真还是假时,Boolean 可以符号化地表示不确定的结果:

>>> xpos > 0
True
>>> xneg > 0
False
>>> x > 0
x > 0
>>> type(x > 0)
<class 'sympy.core.relational.StrictGreaterThan'>

最后一个示例展示了当不等式不确定时会发生什么:我们得到一个 StrictGreaterThan 的实例,它将不等式表示为一个符号表达式。在内部,当尝试评估像 a > b 这样的不等式时,SymPy 将计算 (a - b).is_extended_positive。如果结果是 TrueFalse,那么 SymPy 的符号 S.trueS.false 将被返回。如果结果是 None,那么将返回一个未评估的 StrictGreaterThan,如上面的 x > 0 所示。

xpos > 0 这样的查询返回 S.true 而不是 True 并不明显,因为这两个对象显示方式相同,但我们可以使用 Python 的 is 运算符来检查这一点:

>>> from sympy import S
>>> xpos.is_positive is True
True
>>> xpos.is_positive is S.true
False
>>> (xpos > 0) is True
False
>>> (xpos > 0) is S.true
True

在 SymPy 中没有 None 的通用符号类比。在低级假设查询返回 None 的情况下,符号查询将导致未评估的符号 Boolean`(例如,``x > 0`)。我们可以将符号 Boolean 用作符号表达式的一部分,例如 Piecewise

>>> from sympy import Piecewise
>>> p = Piecewise((1, x > 0), (2, True))
>>> p
Piecewise((1, x > 0), (2, True))
>>> p.subs(x, 3)
1

这里 p 表示一个表达式,如果 x > 0 则等于 1,否则等于 2。未求值的 Boolean 不等式 x > 0 表示决定表达式符号值的条件。当我们为 x 代入一个值时,不等式将解析为 S.true,然后 Piecewise 可以评估为 12

当使用模糊布尔值而不是符号 Boolean 时,同样的方法将不起作用:

>>> p2 = Piecewise((1, x.is_positive), (2, True))
Traceback (most recent call last):
...
TypeError: Second argument must be a Boolean, not `NoneType`

不能将 None 用作 Piecewise 的条件,因为与不等式 x > 0 不同,它不提供任何信息。对于不等式,一旦知道 x 的值,就可以在将来决定条件是否可能为 TrueFalseNone 的值不能以这种方式使用,因此被拒绝。

备注

我们可以在 Piecewise 中使用 True,因为 True 符号化后变为 S.true。符号化 None 只会再次得到 None,这不是一个有效的符号 SymPy 对象。

SymPy 中还有许多其他的符号 Boolean 类型。关于模糊布尔和符号 Boolean 之间差异的考虑同样适用于所有其他 SymPy Boolean 类型。举个不同的例子,有 Contains,它表示一个对象包含在一个集合中的陈述:

>>> from sympy import Reals, Contains
>>> x = Symbol('x', real=True)
>>> y = Symbol('y')
>>> Contains(x, Reals)
True
>>> Contains(y, Reals)
Contains(y, Reals)
>>> Contains(y, Reals).subs(y, 1)
True

对应于 Contains 的 Python 运算符是 inin 的一个特点是它只能评估为 bool``(``TrueFalse),因此如果结果不确定,则会引发异常:

>>> from sympy import I
>>> 2 in Reals
True
>>> I in Reals
False
>>> x in Reals
True
>>> y in Reals
Traceback (most recent call last):
...
TypeError: did not evaluate to a bool: (-oo < y) & (y < oo)

可以通过使用 Contains(x, Reals)Reals.contains(x) 而不是 x in Reals 来避免异常。

带有模糊布尔值的三值逻辑

无论我们使用模糊布尔值还是符号 Boolean,我们始终需要意识到查询可能是不确定的。然而,在这两种情况下,如何编写处理这种情况的代码是不同的。我们将首先查看模糊布尔值。

考虑以下函数:

>>> def both_positive(a, b):
...     """ask whether a and b are both positive"""
...     if a.is_positive and b.is_positive:
...         return True
...     else:
...         return False

both_positive 函数应该告诉我们 ab 是否都为正。然而,如果任一 is_positive 查询返回 Noneboth_positive 函数将会失败:

>>> print(both_positive(S(1), S(1)))
True
>>> print(both_positive(S(1), S(-1)))
False
>>> print(both_positive(S(-1), S(-1)))
False
>>> x = Symbol('x') # may or may not be positive
>>> print(both_positive(S(1), x))
False

备注

我们需要使用 S 来简化此函数的参数,因为假设仅在 SymPy 对象上定义,而不是在常规 Python int 对象上定义。

这里 False 是错误的,因为 x 可能是正数,在这种情况下,两个参数都将是正数。我们在这里得到 False 是因为 x.is_positive 返回 None,而 Python 会将 None 视为“假”。

为了正确处理所有可能的情况,我们需要将识别 TrueFalse 情况的逻辑分开。一个改进的函数可能是:

>>> def both_positive_better(a, b):
...     """ask whether a and b are both positive"""
...     if a.is_positive is False or b.is_positive is False:
...         return False
...     elif a.is_positive is True and b.is_positive is True:
...         return True
...     else:
...         return None

此函数现在可以处理 ab 的所有 TrueFalseNone 情况,并且将始终返回一个模糊布尔值,表示“ab 均为正”这一陈述是真、假还是未知:

>>> print(both_positive_better(S(1), S(1)))
True
>>> print(both_positive_better(S(1), S(-1)))
False
>>> x = Symbol('x')
>>> y = Symbol('y', positive=True)
>>> print(both_positive_better(S(1), x))
None
>>> print(both_positive_better(S(-1), x))
False
>>> print(both_positive_better(S(1), y))
True

在使用模糊布尔值时,我们需要小心处理的另一种情况是使用Python的``not``运算符进行否定,例如:

>>> x = Symbol('x')
>>> print(x.is_positive)
None
>>> not x.is_positive
True

模糊布尔值 None 的正确否定仍然是 None。如果我们不知道陈述“x 是正的”是 True 还是 False,那么我们也不知道它的否定“x 不是正的”是 True 还是 False。我们得到 True 的原因再次是因为 None 被视为“假”。当 None 与逻辑运算符(如 not)一起使用时,它首先会被转换为 bool,然后被否定:

>>> bool(None)
False
>>> not bool(None)
True
>>> not None
True

None 被视为假值这一事实,如果使用得当,可能会很有用。例如,我们可能只想在 x 已知为正数时执行某些操作,在这种情况下,我们可以这样做

>>> x = Symbol('x', positive=True)
>>> if x.is_positive:
...     print("x is definitely positive")
... else:
...     print("x may or may not be positive")
x is definitely positive

如果我们理解一个替代条件分支指的是两种情况(FalseNone),那么这是一种有用的编写条件语句的方式。当我们确实需要区分所有情况时,我们需要使用类似 x.is_positive is False 的方法。然而,我们需要注意的是,使用 Python 的二元逻辑运算符如 notand 与模糊布尔值一起时,它们将无法正确处理不确定的情况。

事实上,SymPy 有内部函数,这些函数设计用来正确处理模糊布尔值:

>>> from sympy.core.logic import fuzzy_not, fuzzy_and
>>> print(fuzzy_not(True))
False
>>> print(fuzzy_not(False))
True
>>> print(fuzzy_not(None))
None
>>> print(fuzzy_and([True, True]))
True
>>> print(fuzzy_and([True, None]))
None
>>> print(fuzzy_and([False, None]))
False

使用 fuzzy_and 函数,我们可以更简单地编写 both_positive 函数:

>>> def both_positive_best(a, b):
...     """ask whether a and b are both positive"""
...     return fuzzy_and([a.is_positive, b.is_positive])

使用 fuzzy_andfuzzy_orfuzzy_not 可以简化代码,并且由于代码看起来更像普通二进制逻辑的情况,因此还可以减少引入逻辑错误的机会。

带有符号布尔值的三值逻辑

当使用符号 Boolean 而不是模糊布尔值时,None 被静默处理为假的问题不会出现,因此更容易避免逻辑错误。然而,如果不小心处理,不确定的情况通常会导致异常被抛出。

这次我们将尝试使用符号 Boolean 来实现 both_positive 函数。

>>> def both_positive(a, b):
...     """ask whether a and b are both positive"""
...     if a > 0 and b > 0:
...         return S.true
...     else:
...         return S.false

第一个区别是,我们返回符号化的 Boolean 对象 S.trueS.false,而不是 TrueFalse。第二个区别是,我们测试例如 a > 0,而不是 a.is_positive。尝试一下,我们会得到

>>> both_positive(1, 2)
True
>>> both_positive(-1, 1)
False
>>> x = Symbol('x')  # may or may not be positive
>>> both_positive(x, 1)
Traceback (most recent call last):
...
TypeError: cannot determine truth value of Relational

现在发生的情况是,当 x 是否为正数未知时,测试 x > 0 会引发异常。更准确地说,x > 0 本身不会引发异常,但 if x > 0 会引发异常,这是因为 if 语句隐式调用了 bool(x > 0),而后者会引发异常。

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

Python 表达式 x > 0 创建了一个 SymPy Boolean。由于在这种情况下 Boolean 不能评估为 TrueFalse,我们得到一个未评估的 StrictGreaterThan。尝试使用 bool(x > 0) 将其强制转换为 bool 会引发异常。这是因为常规的 Python bool 必须是 TrueFalse,而这两种情况在本例中都无法确定。

当使用 andornot 与符号 Boolean 时,会出现类似的问题。解决方法是使用 SymPy 的符号 AndOrNot,或者等效地使用 Python 的按位逻辑运算符 &|~

>>> from sympy import And, Or, Not
>>> x > 0
x > 0
>>> x > 0 and x < 1
Traceback (most recent call last):
...
TypeError: cannot determine truth value of Relational
>>> And(x > 0, x < 1)
(x > 0) & (x < 1)
>>> (x > 0) & (x < 1)
(x > 0) & (x < 1)
>>> Or(x < 0, x > 1)
(x > 1) | (x < 0)
>>> Not(x < 0)
x >= 0
>>> ~(x < 0)
x >= 0

如前所述,如果我们避免在 ifandornot 中直接使用 SymPy Boolean,我们可以改进 both_positive 的版本。相反,我们可以测试 Boolean 是否已评估为 S.trueS.false

>>> def both_positive_better(a, b):
...     """ask whether a and b are both positive"""
...     if (a > 0) is S.false or (b > 0) is S.false:
...         return S.false
...     elif (a > 0) is S.true and (b > 0) is S.true:
...         return S.true
...     else:
...         return And(a > 0, b > 0)

现在在这个版本中,我们不会遇到任何异常,如果结果是不确定的,我们将得到一个符号化的 Boolean,它表示在什么条件下陈述“ab 都是正的”会是真的:

>>> both_positive_better(S(1), S(2))
True
>>> both_positive_better(S(1), S(-1))
False
>>> x, y = symbols("x, y")
>>> both_positive_better(x, y + 1)
(x > 0) & (y + 1 > 0)
>>> both_positive_better(x, S(3))
x > 0

最后一个例子表明,实际上使用 And 和一个已知为真的条件可以简化 And 。事实上,我们有

>>> And(x > 0, 3 > 0)
x > 0
>>> And(4 > 0, 3 > 0)
True
>>> And(-1 > 0, 3 > 0)
False

这意味着我们可以改进 both_positive_better。根本不需要不同的案例。相反,我们可以简单地返回 And 并在可能的情况下让它简化:

>>> def both_positive_best(a, b):
...     """ask whether a and b are both positive"""
...     return And(a > 0, b > 0)

现在这将适用于任何符号实数对象并生成一个符号结果。我们还可以将特定值代入结果中,以查看其工作原理:

>>> both_positive_best(2, 1)
True
>>> both_positive_best(-1, 2)
False
>>> both_positive_best(x, 3)
x > 0
>>> condition = both_positive_best(x/y, x + y)
>>> condition
(x + y > 0) & (x/y > 0)
>>> condition.subs(x, 1)
(1/y > 0) & (y + 1 > 0)
>>> condition.subs(x, 1).subs(y, 2)
True

在使用符号 Boolean 对象时,尽可能避免使用 if/else 和其他逻辑运算符如 and 等对其进行分支处理。相反,应考虑计算一个条件并将其作为变量传递。基本的符号操作如 AndOrNot 可以为你处理逻辑。

脚注