在Python中围绕着使用一个有趣的问题是与一个字面的关系

152 阅读4分钟

由于本条目范围之外的原因,我最近在我们的一个系统上安装了Ubuntu的Pythonnetaddr包(适用于Ubuntu 20.04)。当我这样做时,我得到了一个有趣的Python警告,我以前没有见过:

SyntaxWarning: "is not" with a literal. Did you mean "!="?

我很好奇,就去查了一下有关的代码,它归结为类似这样的东西:

def int_to_bits(int_val, word_size, num_words, word_sep=''):
    [...]
    if word_sep is not '':
        [...]

(目前的代码用'!='代替了这个,这也是该文件中其他类似代码的用法。Ubuntu就是Ubuntu,他们可能永远不会更新或修复20.04的'python3-netaddr'包)。

这段代码的意图很明显;它想检查你是否提供了自己的word_sep参数。一方面,在这里使用 'is not' 并不是正确的做法。当你以这种方式使用 'is not' 时,你需要有一个哨兵对象,而不是一个哨兵值,而这段代码使用的是值'' ,即空字符串。另一方面,这段代码实际上是有效的,至少有三个原因。其中一个可能稍微令人惊讶。

这段代码工作的第一个原因是机械性的,因为我省略了if 的主体和其他实际使用word_sep的代码。 这里是几乎完整的代码:

if word_sep is not '':
   if not _is_str(word_sep):
       raise ValueError(...)

return word_sep.join(bit_words)

因此,如果代码认为它有一个非默认的word_sep,它所做的唯一不同的事情就是检查它是否真的是一个字符串。 由于空字符串通过了该检查,所以一切正常。鉴于此,if并非完全必要;你也可以一直检查word_sep是一个字符串。然而这第一个原因是针对代码本身的。

第二和第三个原因是一般性的,无论代码如何使用word_sep,也无论它在if 。 我首先以图解的形式介绍第二个原因:

>>> def a(b=''):
...   return b is not ''
...
<stdin>:2: SyntaxWarning: "is not" with a literal. Did you mean "!="?
>>> a()
False
>>> a(b='')
False

在CPython中,一些特定的字符串和其他(不可改变的)值被称为内部的。无论它们在你的Python代码中的不同地方被使用多少次,这些值永远只有一个实例。例如,空元组只有一个实例,'()',许多小整数也只有一个实例。整数对于生动地说明这一点特别有用,因为你可以操作当前的整数来创建新的值:

>>> a = 10
>>> b = 5
>>> c = 4
>>> (b+c+1) is a
True

如果你把a 改为 300,把b 改为 295,这将是False(从 Python 3.8.7 开始)。

空字符串,'' ,是这些内含的 (字符串) 值之一。所有空字符串的拷贝都是相同的对象,不管它们来自哪里。因为它们是相同的对象,你可以使用'is not'(和'is')来比较它们的值,而且总是有效。这当然不是语言规范或CPython所能保证的,但这是一个基本的优化,如果它不再是这样的话,那就很不正常了。不过,你还是应该使用'!=',不要这么麻烦。

第三个原因最好再次以图解的形式呈现:

>>> def a(b=3000):
...    return b is 3000
[...]
>>> a()
True
>>> a(b=3000)
False

这是另一个CPython的优化,但它是一个单一函数中的优化。当CPython为一个函数生成字节码时,它很聪明地只保留每个常量值的一个副本,这种常量的合并包括默认参数。所以在a 函数中,b 默认值的整数'3000'和代码中的整数字面'3000'是同一个对象,'is' 会告诉你这一点。然而,来自外部的'3000'的整数是一个不同的对象 (因为 3000 是一个足够大的整数,Python 不会对它进行实习)。

这种优化可能会留在CPython中,但我强烈建议你不要在你的代码中利用它。就像警告中说的那样,不要在字面上使用 'is' 或 'is not' 。你可能从利用这一点得到的非常轻微的性能改善并不值得你去制造混乱。