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

70
当运行以下代码行时:
>>> [0xfor x in (1, 2, 3)]

我原本期望Python会返回一个错误。

然而,REPL却返回了:

[15]

可能的原因是什么?


34
请注意,Python 将此表达式视为 [0xf or x in (1, 2, 3)]。你实际上已经发现了 Stack Overflow 语法高亮器中的一个小错误,因为它没有对 0xfor 进行着色,而是跳过了 or ;) - 101
1
非常意外...显然这对于代码高尔夫很有用,但它与其余语法并不一致。在我看来,我更喜欢连续的字母数字字符始终被视为单个标记。 - GACy20
1
我觉得这是解析器中的一个bug。为了记录,它在3或4“hello”和5时给出了相同的结果。我怀疑这是为了适应二元运算符(如“3>4”)的情况,但在比较操作的情况下,它不是一个直接联系,因为你不能执行3and5。我已经在Python开发者社区发布了帖子,看看他们会说什么。 - Stefano Borini
还要注意的是,这不是由于新解析器引起的。这种行为在Python 2.7中也存在。 - Stefano Borini
3
斯托尔恰卡(Storchaka)字面意思是“它不违反规范,但看起来相当令人困惑,因此我们很可能会更改规范和实现以防止混淆”。自2018年以来这也是已知的。 - Stefano Borini
2
@StefanoBorini,“hello”和5以及3>5是不同的。 ">在标识符或其他形式的表达式中无效。令人意外的是,一串纯字母数字字符(即[a-z0-9])可以被解释为2个标记而不是一个“随机”的标记。 - GACy20
3个回答

104

TL;DR

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

  1. Python tokenizer
  2. Operator precedence

由于short-circuit evaluation,它永远不会引发NameError - 如果在or运算符左侧的表达式是真值,Python将永远不会尝试评估其右侧。

解析十六进制数字

首先,我们必须了解Python如何读取十六进制数字。

tokenizer.c的巨大的tok_get函数中,我们:

  1. Find第一个0x
  2. Keep reading the next characters只要它们在0-f的范围内。

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

我们仍然必须解析余下的代码or x in (1, 2, 3)],这将留下以下代码:

[15 or x in (1, 2, 3)]

运算符优先级

因为in运算符优先级高于or,我们可能会期望x in (1, 2, 3)首先被计算。

这是一个麻烦的情况,因为x不存在,将引发NameError

or是惰性的

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

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

parsed = ast.parse('0xfor x in (1, 2, 3)', mode='eval')
ast.dump(parsed)

输出:


    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())
                    ]
                )
            ]
        )
    )


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

附录A:PEG解析器

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

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

PyOS_strtoulPython/mystrtoul.c中。

在mystrtoul.c文件内,解析器会查看0x后面的一个字符。如果它是十六进制字符,则Python将数字的基数设置为16:

            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;
            } ...

然后它会解析剩余的数字,只要这些字符在0-f的范围内:

    while ((c = _PyLong_DigitValue[Py_CHARMASK(*str)]) < base) {
        if (ovlimit > 0) /* no overflow check required */
            result = result * base + c;
        ...
        ++str;
        --ovlimit;
    }

最终, 它将指针设置为指向已扫描的最后一个字符 - 即超出最后一个十六进制字符一个字符:

    if (ptr)
        *ptr = (char *)str;

感谢


2
有时我认为Python从来没有被设计成一个真正的产品。在单个方法中进行670行的标记化处理?谁想要维护那样的代码呢? - Thomas Weller
@defalt 你在说哪个空格?被询问的那一行中 0xf 之间没有空格。 - Douglas
2
@ThomasWeller A)对于一个标记生成器来说,这不错。B)Python不是一个“产品”,无论是真实的还是其他,事实上并不是以此为目的。它最初是作为一种教学语言而开始的。 - hobbs

28

其他答案已经解释清楚了具体发生了什么。但对我来说,有趣的是即使数字和运算符之间没有空格,Python 也能识别操作符。实际上,我的第一反应是“哇,Python 有一个奇怪的解析器”。

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

Perl:

$ perl -le 'print(0xfor 3)'
15

Lua:

$ lua5.3 -e 'print(0xfor 4)'
15

Awk 没有 or,但它有 in:

$ awk 'BEGIN { a[15]=1; print(0x0fin a); }'
1

Ruby?(我不是很了解它,但让我们猜一下):

$ ruby -e 'puts 0x0for 5'
15

就算是紧贴在数字常量后面,Python并不是唯一一个能够识别字母运算符的脚本语言。


如果您使用bash或zsh,也可以尝试这个命令: echo $(( 34#0xfor -15 ))。然而,请注意,这与其他情况不同,因为这里没有隐藏的“or”运算符。 - inof

6

就像其他人所解释的那样,它只是十六进制数字0xf后面跟着运算符or。运算符通常不需要周围有空格,除非必须避免歧义。在本例中,字母o不能成为十六进制数的一部分,因此不存在歧义。请参阅Python语言参考文献中关于标记之间空格的章节

由于短路计算,行的其余部分不会被计算,尽管当然会被解析和编译。

使用同样的“技巧”,您可以编写类似难以理解但不会抛出异常的Python代码,例如:

>>> 0xbin b'in'
False
>>> 0xbis 1000
False
>>> 0b1and 0b1is 0b00
False
>>> 0o1if 0b1else Oy1then
1

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接