符号布尔值和模糊布尔值

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

符号布尔值与三值布尔值

假设查询(如 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`

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

注意

我们可以使用 TruePiecewise 中,因为 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 查询给出 None,则 both_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 可以生成更简单的代码,并且还可以减少引入逻辑错误的可能性,因为代码看起来更像是普通二元逻辑中的代码。

带符号布尔的 3 值逻辑

在使用符号 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,而在这两种情况下,都没有被认为是正确的。

当使用符号 Boolean 时,也会出现类似的问题,例如使用 andornot。解决方案是使用 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)

现在使用这个版本,我们不会遇到任何异常,如果结果是不确定的,我们会得到一个表示语句“ab 都为正”为真的条件的符号 Boolean

>>> 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)可以为您处理逻辑。

脚注