and'(布尔)与'&'(按位)- 用于列表与numpy数组时行为有何不同?

185 投票
8 回答
123647 浏览
提问于 2025-04-17 23:53

为什么布尔运算和按位运算在列表和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 个回答

20
  1. 在Python中,表达式 X and Y 的结果是 Y,前提是 X 的布尔值为真(即 bool(X) == True),或者 XY 中有任何一个是假的。例如:

    True and 20 
    >>> 20
    
    False and 20
    >>> False
    
    20 and []
    >>> []
    
  2. 位运算符对于列表来说是没有定义的。但它是可以用于整数的,作用于数字的二进制表示上。比如数字16(01000)和31(11111):

    16 & 31
    >>> 16
    
  3. NumPy并不是一个有超能力的工具,它不知道你是否想让比如 [False, False] 在逻辑表达式中等于 True。在这方面,它改变了Python的标准行为,标准行为是:“任何长度为0的空集合(即 len(collection) == 0)都被视为 False”。

  4. 这可能是NumPy数组的&运算符的预期行为。

20

例子 1:

这就是 and 运算符的工作原理。

xy => 如果 x 是假(false),那么结果是 x,否则结果是 y

换句话说,因为 mylist1 不是 False,所以这个表达式的结果是 mylist2。(只有空列表会被认为是 False。)

例子 2:

& 运算符是用来做按位与的,正如你提到的。按位操作只适用于数字。a & b 的结果是一个数字,这个数字的二进制位中,只有在 ab 都为 1 的位上才是 1。举个例子:

>>> 3 & 1
1

二进制字面量来看更容易理解(和上面的数字一样):

>>> 0b0011 & 0b0001
0b0001

按位操作的概念和布尔(真值)操作类似,但它们只作用于位。

那么,假设我有关于我车子的几个说法:

  1. 我的车是红色的
  2. 我的车有轮子

这两个说法的逻辑“与”是:

(我的车是红色的吗?) and (我的车有轮子吗?) => 逻辑上的真或假

这两个说法对我来说都是对的。所以整体的说法在逻辑上是“真”的。

这两个说法的按位“与”就有点复杂了:

(说法“我的车是红色的”的数值) & (说法“我的车有轮子”的数值) => 数字

如果 Python 能把这些说法转换成数值,它就会这样做,并计算这两个数值的按位与。这可能会让你觉得 &and 是可以互换的,但实际上它们是不同的。此外,对于那些不能转换的对象,你会得到一个 TypeError 错误。

例子 3 和 4:

Numpy 实现了数组的算术操作

ndarray 的算术和比较操作是按元素进行的,通常会返回 ndarray 对象作为结果。

但是不支持数组的逻辑操作,因为你不能在 Python 中重载逻辑运算符。这就是为什么例子三不工作,而例子四可以的原因。

所以回答你的 and& 的问题:使用 and

按位操作用于检查一个数字的结构(哪些位被设置,哪些位没有被设置)。这种信息主要用于低级操作系统接口(例如unix 权限位)。大多数 Python 程序不需要知道这些。

然而,逻辑操作(andornot)是经常使用的。

23

短路布尔运算符(andor)不能被重写,因为没有合适的方法来做到这一点,而不引入新的语言特性或牺牲短路特性。简单来说,这些运算符会先检查第一个操作数的真假值,然后根据这个值决定是否检查第二个操作数。如果第一个操作数为真,就会继续检查并返回第二个操作数;如果第一个操作数为假,就直接返回第一个操作数,而不去检查第二个操作数。

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 意思完全不同一样,列表没有 & 运算符。

34

关于 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 可以按照自己的意愿定义它,并且它将 & 定义为与其他操作符一致:逐点运算。所以你最终得到了你想要的结果。

希望这些信息对你有帮助,

159

and 用来检查两个表达式是否都为逻辑上的 True,而 &(当用在 True/False 值时)则是检查两个值是否都是 True

在 Python 中,空的内置对象通常被视为逻辑上的 False,而非空的内置对象则被视为逻辑上的 True。这使得我们在处理列表时很方便,比如想要在列表为空时做一件事,而在列表不为空时做另一件事。需要注意的是,这意味着列表 [False] 在逻辑上是 True

>>> if [False]:
...    print('True')
...
True

在示例 1 中,第一个列表是非空的,因此逻辑上是 True,所以 and 的结果和第二个列表的结果是一样的。(在这个例子中,第二个列表也是非空的,所以逻辑上也是 True,但要判断这一点需要额外的计算步骤。)

在示例 2 中,列表不能以位运算的方式有意义地结合,因为它们可以包含不同类型的元素。可以进行位运算的东西包括:TrueFalse,整数。

相比之下,NumPy 对象支持向量化计算。也就是说,它们允许你对多组数据执行相同的操作。

示例 3 失败是因为 NumPy 数组(长度大于 1)没有逻辑值,这样可以避免基于向量的逻辑混淆。

示例 4 只是一个向量化的位 and 操作。

总结

  • 如果你不处理数组,也不进行整数的数学运算,你可能想用 and

  • 如果你有一组逻辑值想要结合,使用 numpy&

撰写回答