关于python:奇怪地使用“和”/“或”运算符

Strange use of “and” / “or” operator

我在学习python,遇到了一些很好很短但不完全有意义的代码。

背景是:

1
2
def fn(*args):
    return len(args) and max(args)-min(args)

我知道它在做什么,但是为什么python要这样做——即返回值而不是真/假?

1
10 and 7-2

返回5。类似地,更改和到或将导致功能更改。所以

1
10 or 7 - 2

将返回10。

这是合法/可靠的风格,还是有什么问题?


DR

我们首先总结两个逻辑运算符andor的两种行为。这些习语将构成我们下面讨论的基础。

and

Return the first Falsy value if there are any, else return the last
value in the expression.

or

Return the first Truthy value if there are any, else return the last
value in the expression.

这些行为也在文档中进行了总结,特别是在下表中:

enter image description here

返回布尔值而不考虑其操作数的唯一运算符是not运算符。

"真实性"和"真实性"评价

声明

1
len(args) and max(args) - min(args)

是一种非常简洁(并且可以说可读性较低)的说法,"如果args不是空的,返回max(args) - min(args)的结果",否则返回0。一般来说,它是if-else表达式的更简洁的表示。例如,

1
exp1 and exp2

应该(粗略地)翻译成:

1
2
3
r1 = exp1
if not r1:
    r1 = exp2

或者,等价地,

1
r1 = exp1 if exp1 else exp2

其中exp1exp2是任意的python对象,或者返回某些对象的表达式。理解逻辑andor运算符的用法的关键是理解它们不限于操作或返回布尔值。任何具有真实值的对象都可以在此处进行测试。这包括intstrlistdicttuplesetNoneType和用户定义的对象。短路规则同样适用。

但什么是真实?它指的是在条件表达式中使用时如何计算对象。@帕特里克豪格在这篇文章中很好地总结了真实性。

All values are considered"truthy" except for the following, which are
"falsy":

  • None
  • False
  • 0
  • 0.0
  • 0j
  • Decimal(0)
  • Fraction(0, 1)
  • [] - an empty list
  • {} - an empty dict
  • () - an empty tuple
  • '' - an empty str
  • b'' - an empty bytes
  • set() - an empty set
  • an empty range, like range(0)
  • objects for which

    • obj.__bool__() returns False
    • obj.__len__() returns 0

A"truthy" value will satisfy the check performed by if or while
statements. We use"truthy" and"falsy" to differentiate from the
bool values True and False.

and的工作原理

我们以OP的问题为基础,进一步讨论了这些操作符在这些情况下是如何工作的。

Given a function with the definition

1
2
def foo(*args):
    ...

How do I return the difference between the minimum and maximum value
in a list of zero or more arguments?

查找最小值和最大值很容易(使用内置函数!).这里唯一的障碍是适当地处理角情况,其中参数列表可能为空(例如,调用foo())。多亏了and操作符,我们可以在一条线上同时执行这两项操作:

1
2
def foo(*args):
     return len(args) and max(args) - min(args)
1
2
3
4
5
foo(1, 2, 3, 4, 5)
# 4

foo()
# 0

由于使用了and,如果第一个表达式是True,则必须计算第二个表达式。请注意,如果第一个表达式的计算结果为Truthy,则返回值始终是第二个表达式的结果。如果第一个表达式的计算结果是错误的,则返回的结果是第一个表达式的结果。

在上述函数中,如果foo收到一个或多个参数,len(args)大于0(正数),则返回的结果为max(args) - min(args)。Otoh,如果没有通过任何论据,那么len(args)0,这是不可靠的,并且返回0

请注意,编写此函数的另一种方法是:

1
2
3
4
5
def foo(*args):
    if not len(args):
        return 0

    return max(args) - min(args)

或者更简而言之,

1
2
def foo(*args):
    return 0 if not args else max(args) - min(args)

当然,这些函数都不执行任何类型检查,因此除非您完全信任所提供的输入,否则不要依赖于这些构造的简单性。

or的工作原理

我用一个人为的例子以类似的方式解释了or的工作。

Given a function with the definition

1
2
def foo(*args):
    ...

How would you complete foo to return all numbers over 9000?

我们用or来处理这个角箱。我们将foo定义为:

1
2
3
4
5
6
7
8
def foo(*args):
     return [x for x in args if x > 9000] or 'No number over 9000!'

foo(9004, 1, 2, 500)
# [9004]

foo(1, 2, 3, 4)
# 'No number over 9000!'

foo对列表进行过滤,以保留9000上的所有数字。如果存在任何这样的数字,那么列表理解的结果就是一个非空的列表,这是真实的,所以它会被返回(这里的操作是短路的)。如果没有这样的数字,那么列表比较的结果是[],这是错误的。因此,第二个表达式现在被计算(非空字符串)并返回。

使用条件,我们可以将此函数重新编写为,

1
2
3
4
5
6
def foo(*args):
    r = [x for x in args if x > 9000]
    if not r:
        return 'No number over 9000!'

    return r

和以前一样,这种结构在错误处理方面更加灵活。


从python文档引用

Note that neither and nor or restrict the value and type they return
to False and True, but rather return the last evaluated argument. This
is sometimes useful, e.g., if s is a string that should be replaced by
a default value if it is empty, the expression s or 'foo' yields the
desired value.

所以,这就是设计Python来评估布尔表达式的方法,上面的文档让我们了解了为什么要这样做。

要得到一个布尔值,只需输入它。

1
return bool(len(args) and max(args)-min(args))

为什么?

短路。

例如:

1
2
2 and 3 # Returns 3 because 2 is Truthy so it has to check 3 too
0 and 3 # Returns 0 because 0 is Falsey and there's no need to check 3 at all

对于or也一样,也就是说,它一找到表达式就返回正确的表达式,因为对表达式的其余部分进行评估是多余的。

python不返回硬核TrueFalse,而是返回truthy或false,这无论如何都将对TrueFalse进行评估。您可以按原样使用表达式,它仍然有效。

要知道什么是真是假,请检查帕特里克·豪的回答。


和或执行布尔逻辑,但在比较时返回实际值之一。使用和时,将在从左到右的布尔上下文中计算值。在布尔上下文中,0、""、[]、()、和none为false;其他都为true。

如果布尔上下文中的所有值都为真,则返回最后一个值。

1
2
3
4
>>> 2 and 5
5
>>> 2 and 5 and 10
10

如果布尔上下文中的任何值为假,则返回第一个假值。

1
2
3
4
>>> '' and 5
''
>>> 2 and 0 and 5
0

所以代码

1
return len(args) and max(args)-min(args)

返回EDOCX1的值(0),如果有其他参数,则返回EDOCX1的值(1),即0。


Is this legit/reliable style, or are there any gotchas on this?

这是合法的,它是最后一个返回值的短路评估。

你提供了一个很好的例子。如果没有传递参数,函数将返回0,并且代码不必检查是否存在未传递参数的特殊情况。

另一种使用方法是将none参数默认为可变原语,如空列表:

1
2
3
def fn(alist=None):
    alist = alist or []
    ....

如果将某个非真实值传递给alist,则默认为空列表,这是避免if语句和可变默认参数陷阱的简便方法。


高查斯

是的,有一些问题。

fn() == fn(3) == fn(4, 4)

首先,如果fn返回0时,不知道调用它时是否没有任何参数、有一个参数或有多个相同参数:

1
2
3
4
5
6
>>> fn()
0
>>> fn(3)
0
>>> fn(3, 3, 3)
0

fn是什么意思?

然后,python是一种动态语言。它在任何地方都没有指定fn做什么,它的输入应该是什么,它的输出应该是什么样子。因此,正确命名函数非常重要。同样,论点也不必称为argsdelta(*numbers)calculate_range(*numbers)可以更好地描述函数应该做什么。

自变量误差

最后,逻辑And运算符应该可以防止在没有任何参数的情况下调用函数时失败。但是,如果某个参数不是数字,它仍然失败:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
>>> fn('1')
Traceback (most recent call last):
  File"<stdin>", line 1, in <module>
  File"<stdin>", line 2, in fn
TypeError: unsupported operand type(s) for -: 'str' and 'str'
>>> fn(1, '2')
Traceback (most recent call last):
  File"<stdin>", line 1, in <module>
  File"<stdin>", line 2, in fn
TypeError: '>' not supported between instances of 'str' and 'int'
>>> fn('a', 'b')
Traceback (most recent call last):
  File"<stdin>", line 1, in <module>
  File"<stdin>", line 2, in fn
TypeError: unsupported operand type(s) for -: 'str' and 'str'

可能的替代方案

以下是根据"请求原谅比请求允许更容易"原则编写函数的方法:

1
2
3
4
5
6
7
def delta(*numbers):
    try:
        return max(numbers) - min(numbers)
    except TypeError:
        raise ValueError("delta should only be called with numerical arguments") from None
    except ValueError:
        raise ValueError("delta should be called with at least one numerical argument") from None

举个例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
>>> delta()
Traceback (most recent call last):
  File"<stdin>", line 1, in <module>
  File"<stdin>", line 7, in delta
ValueError: delta should be called with at least one numerical argument
>>> delta(3)
0
>>> delta('a')
Traceback (most recent call last):
  File"<stdin>", line 1, in <module>
  File"<stdin>", line 5, in delta
ValueError: delta should only be called with numerical arguments
>>> delta('a', 'b')
Traceback (most recent call last):
  File"<stdin>", line 1, in <module>
  File"<stdin>", line 5, in delta
ValueError: delta should only be called with numerical arguments
>>> delta('a', 3)
Traceback (most recent call last):
  File"<stdin>", line 1, in <module>
  File"<stdin>", line 5, in delta
ValueError: delta should only be called with numerical arguments
>>> delta(3, 4.5)
1.5
>>> delta(3, 5, 7, 2)
5

如果您真的不想在没有任何参数的情况下调用delta时引发异常,您可以返回一些不可能的值(例如-1None)。

1
2
3
4
5
6
7
8
9
10
11
>>> def delta(*numbers):
...     try:
...         return max(numbers) - min(numbers)
...     except TypeError:
...         raise ValueError("delta should only be called with numerical arguments") from None
...     except ValueError:
...         return -1 # or None
...
>>>
>>> delta()
-1

Is this legit/reliable style, or are there any gotchas on this?

我想补充一点,这个问题不仅合法可靠,而且非常实用。下面是一个简单的例子:

1
2
3
>>>example_list = []
>>>print example_list or 'empty list'
empty list

因此,你真的可以利用它。为了不受割礼,我是这样看的:

Or运算符

python的Or运算符返回第一个真值y或最后一个值,然后停止

And运算符

python的And运算符返回第一个假y值或最后一个值,然后停止

幕后

在python中,所有数字都被解释为True,0除外。因此,说:

1
0 and 10

相同:

1
False and True

很明显,这是1号[7号]。因此,它返回0是合乎逻辑的


对。这是正确的行为和比较。

至少在python中,如果A本质上是True,那么A and B返回B,包括A不为空,而不是None不为空容器(如空的listdict等)。如果A实质上是FalseNone或空或空,则返回A

另一方面,如果A实质上是True的话,A or B返回A,包括A不为空、None不为空容器(如空listdict等),否则返回B

很容易不注意(或忽略)这种行为,因为在python中,任何non-null非空对象的计算结果为true都被视为布尔值。

例如,以下所有内容都将打印"真"

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if [102]:
    print"True"
else:
    print"False"

if"anything that is not empty or None":
    print"True"
else:
    print"False"

if {1, 2, 3}:
    print"True"
else:
    print"False"

另一方面,以下所有内容都将打印"假"

1
2
3
4
5
6
7
8
9
10
11
12
13
14
if []:
    print"True"
else:
    print"False"

if"":
    print"True"
else:
    print"False"

if set ([]):
    print"True"
else:
    print"False"