and'(布尔)与'&'(按位)- 用于列表与numpy数组时行为有何不同?
为什么布尔运算和按位运算在列表和NumPy数组上的表现不同?
我对在Python中使用 &
和 and
的合适场景感到困惑,下面的例子说明了这一点。
mylist1 = [True, True, True, False, True]
mylist2 = [False, True, False, True, False]
>>> len(mylist1) == len(mylist2)
True
# ---- Example 1 ----
>>> mylist1 and mylist2
[False, True, False, True, False]
# I would have expected [False, True, False, False, False]
# ---- Example 2 ----
>>> mylist1 & mylist2
TypeError: unsupported operand type(s) for &: 'list' and 'list'
# Why not just like example 1?
>>> import numpy as np
# ---- Example 3 ----
>>> np.array(mylist1) and np.array(mylist2)
ValueError: The truth value of an array with more than one element is ambiguous. Use a.any() or a.all()
# Why not just like Example 4?
# ---- Example 4 ----
>>> np.array(mylist1) & np.array(mylist2)
array([False, True, False, False, False], dtype=bool)
# This is the output I was expecting!
这个回答 和 这个回答 帮助我理解了 and
是布尔运算,而 &
是按位运算。
我阅读了关于 按位运算 的内容,以更好地理解这个概念,但我还是很难把这些信息应用到我上面的四个例子中。
例子4让我得到了想要的结果,这很好,但我仍然不明白我应该在什么情况下、如何以及为什么使用 and
和 &
。为什么列表和NumPy数组在使用这些运算符时表现不同呢?
有没有人能帮我理解布尔运算和按位运算之间的区别,以解释它们为什么对列表和NumPy数组的处理不同?
8 个回答
在Python中,表达式
X and Y
的结果是Y
,前提是X
的布尔值为真(即bool(X) == True
),或者X
或Y
中有任何一个是假的。例如:True and 20 >>> 20 False and 20 >>> False 20 and [] >>> []
位运算符对于列表来说是没有定义的。但它是可以用于整数的,作用于数字的二进制表示上。比如数字16(01000)和31(11111):
16 & 31 >>> 16
NumPy并不是一个有超能力的工具,它不知道你是否想让比如
[False, False]
在逻辑表达式中等于True
。在这方面,它改变了Python的标准行为,标准行为是:“任何长度为0的空集合(即len(collection) == 0
)都被视为False
”。这可能是NumPy数组的&运算符的预期行为。
例子 1:
这就是 and 运算符的工作原理。
x 和 y => 如果 x 是假(false),那么结果是 x,否则结果是 y
换句话说,因为 mylist1
不是 False
,所以这个表达式的结果是 mylist2
。(只有空列表会被认为是 False
。)
例子 2:
&
运算符是用来做按位与的,正如你提到的。按位操作只适用于数字。a & b 的结果是一个数字,这个数字的二进制位中,只有在 a 和 b 都为 1 的位上才是 1。举个例子:
>>> 3 & 1
1
用二进制字面量来看更容易理解(和上面的数字一样):
>>> 0b0011 & 0b0001
0b0001
按位操作的概念和布尔(真值)操作类似,但它们只作用于位。
那么,假设我有关于我车子的几个说法:
- 我的车是红色的
- 我的车有轮子
这两个说法的逻辑“与”是:
(我的车是红色的吗?) and (我的车有轮子吗?) => 逻辑上的真或假
这两个说法对我来说都是对的。所以整体的说法在逻辑上是“真”的。
这两个说法的按位“与”就有点复杂了:
(说法“我的车是红色的”的数值) & (说法“我的车有轮子”的数值) => 数字
如果 Python 能把这些说法转换成数值,它就会这样做,并计算这两个数值的按位与。这可能会让你觉得 &
和 and
是可以互换的,但实际上它们是不同的。此外,对于那些不能转换的对象,你会得到一个 TypeError
错误。
例子 3 和 4:
Numpy 实现了数组的算术操作:
ndarray 的算术和比较操作是按元素进行的,通常会返回 ndarray 对象作为结果。
但是不支持数组的逻辑操作,因为你不能在 Python 中重载逻辑运算符。这就是为什么例子三不工作,而例子四可以的原因。
所以回答你的 and
和 &
的问题:使用 and
。
按位操作用于检查一个数字的结构(哪些位被设置,哪些位没有被设置)。这种信息主要用于低级操作系统接口(例如unix 权限位)。大多数 Python 程序不需要知道这些。
然而,逻辑操作(and
、or
、not
)是经常使用的。
短路布尔运算符(and
和 or
)不能被重写,因为没有合适的方法来做到这一点,而不引入新的语言特性或牺牲短路特性。简单来说,这些运算符会先检查第一个操作数的真假值,然后根据这个值决定是否检查第二个操作数。如果第一个操作数为真,就会继续检查并返回第二个操作数;如果第一个操作数为假,就直接返回第一个操作数,而不去检查第二个操作数。
something_true and x -> x
something_false and x -> something_false
something_true or x -> something_true
something_false or x -> x
注意,这里返回的是实际操作数的值,而不是它的真假值。
要自定义它们的行为,唯一的方法是重写 __nonzero__
(在 Python 3 中改名为 __bool__
),这样你可以影响返回哪个操作数,但不能返回其他东西。列表(和其他集合)在有任何元素时被认为是“真实的”,而在空的时候被认为是“假的”。
NumPy 数组对此有不同的看法:在它们所针对的使用场景中,有两种常见的真假概念:(1)是否有任何元素为真,和(2)是否所有元素都为真。由于这两者完全不兼容,而且没有哪个更正确或更常见,NumPy 不会猜测,而是要求你明确使用 .any()
或 .all()
。
&
和 |
(还有 not
)是可以完全重写的,因为它们不会短路。重写后,它们可以返回任何值,NumPy 就很好地利用了这一点来进行逐元素操作,就像它对待几乎所有其他标量操作一样。另一方面,列表并不会在它们的元素之间广播操作。就像 mylist1 - mylist2
没有任何意义,而 mylist1 + mylist2
意思完全不同一样,列表没有 &
运算符。
关于 list
首先,有一个非常重要的观点,这将是后面内容的基础(我希望如此)。
在普通的Python中,list
并没有什么特别之处(除了它的构造语法看起来很可爱,这主要是历史原因造成的)。一旦你创建了一个列表,比如 [3,2,6]
,它在很多方面就和普通的Python对象一样,像数字 3
、集合 {3,7}
或者函数 lambda x: x+5
。
(没错,它支持修改元素,也支持迭代,还有很多其他功能,但这只是类型的特性:它支持某些操作,而不支持其他操作。比如,int类型支持幂运算,但这并不让它显得特别——这就是int的本质。lambda支持调用,但这也并不让它显得特别——毕竟这就是lambda的用途。)
关于 and
and
并不是一个操作符(你可以称它为“操作符”,但你也可以把“for”称为操作符:)。在Python中,操作符是通过在某种类型的对象上调用的方法实现的,通常是作为该类型的一部分来书写。没有办法让一个方法持有某些操作数的评估结果,但 and
可以(并且必须)这样做。
这意味着 and
不能被重载,就像 for
也不能被重载一样。它是完全通用的,并通过一个特定的协议进行交流。你可以自定义协议的一部分,但这并不意味着你可以完全改变 and
的行为。这个协议是:
想象一下Python在解释“a and b”(这并不是字面上发生的,但有助于理解)。当它遇到“and”时,它会查看刚刚评估的对象(a),并问它:你是真的还是假的?(不是:你是 True
吗?)如果你是a的类的作者,你可以自定义这个回答。如果 a
回答“不是”,那么 and
就完全跳过 b
,不进行评估,并且说:a
是我的结果(不是:False是我的结果)。
如果 a
没有回答,and
会问它:你的长度是多少?(同样,作为 a
的类的作者,你可以自定义这个)。如果 a
回答0,and
就和上面一样——认为它是假的(不是 False),跳过 b
,并给出 a
作为结果。
如果 a
对第二个问题(“你的长度是多少”)回答了其他值,或者根本没有回答,或者对第一个问题(“你是真的还是假的”)回答“是”,那么 and
就会评估 b
,并说:b
是我的结果。注意,它并不会问 b
任何问题。
换句话说,a and b
几乎等同于 b if a else a
,只是 a
只会被评估一次。
现在请花几分钟时间,拿起笔和纸,确保自己明白当 {a,b} 是 {True,False} 的子集时,它的工作方式正如你对布尔操作符的预期。但我希望我已经让你相信,它的通用性更强,正如你将看到的,这样更有用。
将这两者结合起来
现在我希望你理解你的例子1。and
不在乎 mylist1 是数字、列表、lambda 还是 Argmhbl 类的对象。它只关心 mylist1 对协议问题的回答。当然,mylist1 对长度的问题回答了5,所以 and
返回 mylist2。就这样。它与 mylist1 和 mylist2 的元素没有任何关系——这些元素在这里并不重要。
第二个例子: &
在 list
上
另一方面,&
是一个和其他操作符一样的操作符,比如 +
。它可以通过在类上定义一个特殊的方法来为某种类型定义。int
将其定义为按位“与”,而 bool 将其定义为逻辑“与”,但这只是一个选项:例如,集合和一些其他对象(如字典的键视图)将其定义为集合交集。list
并没有定义它,可能是因为Guido没有想到任何明显的定义方式。
numpy
另一方面,numpy数组 是 特殊的,或者至少它们在努力变得特殊。当然,numpy.array 只是一个类,它无法以任何方式重写 and
,所以它做了下一个最好的事情:当被问到“你是真的还是假的”时,numpy.array 会抛出一个ValueError,实际上是在说“请重新表述这个问题,我的真理观不符合你的模型”。(注意,ValueError的消息并没有提到 and
——因为numpy.array 并不知道 谁 在问它问题;它只是谈论真理。)
对于 &
,情况完全不同。numpy.array 可以按照自己的意愿定义它,并且它将 &
定义为与其他操作符一致:逐点运算。所以你最终得到了你想要的结果。
希望这些信息对你有帮助,
and
用来检查两个表达式是否都为逻辑上的 True
,而 &
(当用在 True
/False
值时)则是检查两个值是否都是 True
。
在 Python 中,空的内置对象通常被视为逻辑上的 False
,而非空的内置对象则被视为逻辑上的 True
。这使得我们在处理列表时很方便,比如想要在列表为空时做一件事,而在列表不为空时做另一件事。需要注意的是,这意味着列表 [False] 在逻辑上是 True
:
>>> if [False]:
... print('True')
...
True
在示例 1 中,第一个列表是非空的,因此逻辑上是 True
,所以 and
的结果和第二个列表的结果是一样的。(在这个例子中,第二个列表也是非空的,所以逻辑上也是 True
,但要判断这一点需要额外的计算步骤。)
在示例 2 中,列表不能以位运算的方式有意义地结合,因为它们可以包含不同类型的元素。可以进行位运算的东西包括:True
和 False
,整数。
相比之下,NumPy 对象支持向量化计算。也就是说,它们允许你对多组数据执行相同的操作。
示例 3 失败是因为 NumPy 数组(长度大于 1)没有逻辑值,这样可以避免基于向量的逻辑混淆。
示例 4 只是一个向量化的位 and
操作。
总结
如果你不处理数组,也不进行整数的数学运算,你可能想用
and
。如果你有一组逻辑值想要结合,使用
numpy
和&
。