为什么 Python 为 [0xfor x in (1, 2, 3)] 返回 [15]?

Inf*_*ity 64 python operator-precedence short-circuiting python-3.x

运行以下行时:

>>> [0xfor x in (1, 2, 3)]
Run Code Online (Sandbox Code Playgroud)

我希望 Python 返回一个错误。

相反,REPL 返回:

[15]

可能是什么原因?

Inf*_*ity 99

TL; 博士

Python 将表达式读作[0xf or (x in (1, 2, 3))],因为:

  1. Python的分词器
  2. 运算符优先级

NameError由于短路评估,它永远不会引发- 如果or运算符左侧的表达式是一个真值,Python 将永远不会尝试评估它的右侧。

解析十六进制数

首先,我们要了解Python是如何读取十六进制数的。

tokenizer.c的巨大tok_get功能上,我们:

  1. 找到第一个0x
  2. 只要接下来的字符在 0-f 的范围内,就继续阅读它们。

解析的标记0xf(因为“o”不在 0-f 的范围内),最终将传递给 PEG 解析器,该解析器会将其转换为十进制值15(参见附录 A)。

我们仍然需要解析其余的代码,or x in (1, 2, 3)],剩下的代码如下:

[15 or x in (1, 2, 3)]
Run Code Online (Sandbox Code Playgroud)

运算符优先级

因为in具有比更高的运算符优先级or,我们可能期望x in (1, 2, 3)首先评估。

这是一个麻烦的情况,因为x不存在并且会引发NameError.

or 很懒

幸运的是,Python 支持短路求值,就像or一个惰性运算符:如果左操作数等价于True,Python 不会费心求值右操作数。

我们可以使用ast模块看到它:

[15 or x in (1, 2, 3)]
Run Code Online (Sandbox Code Playgroud)

输出:

parsed = ast.parse('0xfor x in (1, 2, 3)', mode='eval')
ast.dump(parsed)
Run Code Online (Sandbox Code Playgroud)

所以最终的表达式等于[15]


附录 A:PEG 解析器

pegen.cparsenumber_raw函数中,我们可以找到 Python 如何处理前导零:


    Expression(
        body=BoolOp(
            op=Or(),
            values=[
                Constant(value=15),   # <-- Truthy value, so the next operand won't be evaluated.
                Compare(
                    left=Name(id='x', ctx=Load()),
                    ops=[In()],
                    comparators=[
                        Tuple(elts=[Constant(value=1), Constant(value=2), Constant(value=3)], ctx=Load())
                    ]
                )
            ]
        )
    )

Run Code Online (Sandbox Code Playgroud)

PyOS_strtoulPython/mystrtoul.c.

在 mystrtoul.c 中,解析器查看 .c之后的一个字符0x。如果是十六进制字符,Python 将数字的基数设置为 16:

    if (s[0] == '0') {
        x = (long)PyOS_strtoul(s, (char **)&end, 0);
        if (x < 0 && errno == 0) {
            return PyLong_FromString(s, (char **)0, 0);
        }
    }
Run Code Online (Sandbox Code Playgroud)

然后,只要字符在 0-f 范围内,它就会解析数字的其余部分:

            if (*str == 'x' || *str == 'X') {
                /* there must be at least one digit after 0x */
                if (_PyLong_DigitValue[Py_CHARMASK(str[1])] >= 16) {
                    if (ptr)
                        *ptr = (char *)str;
                    return 0;
                }
                ++str;
                base = 16;
            } ...
Run Code Online (Sandbox Code Playgroud)

最终,它将指针设置为指向扫描的最后一个字符——这是最后一个十六进制字符后的一个字符:

    if (ptr)
        *ptr = (char *)str;
Run Code Online (Sandbox Code Playgroud)

谢谢

  • 有时我认为 Python 从来就没有打算成为一个真正的产品。在一个方法中进行 670 行标记化?谁愿意维持这个状态? (2认同)
  • @ThomasWeller A)对于分词器来说,这还不错。B) Python 不是一种“产品”,无论是真实的还是其他的,而且事实上它也不是被设计为“产品”的。它最初是作为一种教学语言。 (2认同)

ilk*_*chu 26

其他答案已经说明究竟发生了什么。但对我来说,有趣的部分是即使数字和它之间没有空格,也能识别运算符。实际上,我的第一个想法是“哇,Python 有一个奇怪的解析器”。

但在过于苛刻之前,也许我应该问问其他朋友的看法:

珀尔:

$ perl -le 'print(0xfor 3)'
15
Run Code Online (Sandbox Code Playgroud)

路亚:

$ lua5.3 -e 'print(0xfor 4)'
15
Run Code Online (Sandbox Code Playgroud)

Awk 没有or,但它有in

$ awk 'BEGIN { a[15]=1; print(0x0fin a); }'
1
Run Code Online (Sandbox Code Playgroud)

红宝石?(我真的不知道,但让我们猜猜):

$ ruby -e 'puts 0x0for 5'
15
Run Code Online (Sandbox Code Playgroud)

是的,FWIW,Python 并不孤单,所有其他脚本类型的语言也能识别字母运算符,即使立即卡在数字常量的后面。


小智 5

正如其他人所解释的那样,它只是0xf后跟操作符的十六进制数or。操作员通常不需要周围的空间,除非有必要避免歧义。在这种情况下,字母o不能是十六进制数的一部分,因此没有歧义。请参阅Python 语言参考中有关空格部分

由于短路评估,该行的其余部分没有被评估,当然,尽管它被解析和编译。

使用相同的“技巧”,您可以编写类似的混淆 Python 代码,不会抛出异常,例如:

>>> 0xbin b'in'
False
>>> 0xbis 1000
False
>>> 0b1and 0b1is 0b00
False
>>> 0o1if 0b1else Oy1then
1
Run Code Online (Sandbox Code Playgroud)