为什么 Python return [15] for [0xfor x in (1, 2, 3)]?
Why does Python return [15] for [0xfor x in (1, 2, 3)]?
当运行下面一行:
>>> [0xfor x in (1, 2, 3)]
我预计 Python 到 return 一个错误。
相反,REPL returns:
[15]
可能是什么原因?
TL;DR
Python 将表达式读作 [0xf or (x in (1, 2, 3))]
,因为:
由于 short-circuit evaluation,它永远不会引发 NameError
- 如果 or
运算符左边的表达式是真值,Python 将永远不会尝试计算右边的值它的侧面。
解析十六进制数
首先,我们要了解Python如何读取十六进制数。
关于 tokenizer.c 的巨大 tok_get
功能,我们:
- Find第一个
0x
.
- 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
的 operator precedence 比 or
高,我们可能希望 x in (1, 2, 3)
先评估。
这是个麻烦的情况,因为 x
不存在并且会引发 NameError
。
or
懒惰
幸运的是,Python 支持 Short-circuit evaluation,因为 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.c的parsenumber_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_strtoul
在 Python/mystrtoul.c
.
在 mystrtoul.c 内,解析器查看 one character after the 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;
} ...
然后就是parses剩下的数字只要字符在0-f范围内即可:
while ((c = _PyLong_DigitValue[Py_CHARMASK(*str)]) < base) {
if (ovlimit > 0) /* no overflow check required */
result = result * base + c;
...
++str;
--ovlimit;
}
Eventually,它将指针设置为指向扫描的最后一个字符 - 这是最后一个十六进制字符后一个字符:
if (ptr)
*ptr = (char *)str;
谢谢
来自 reddit 的 - CSI_Tech_Dept 推荐我到 tokenizer.c 文件中的正确部分。
- The original Tweet.
正如其他人所解释的,它只是十六进制数 0xf
后跟运算符 or
。运算符通常不需要周围的空间,除非有必要避免歧义。在这种情况下,字母 o
不能是十六进制数的一部分,因此不会产生歧义。请参阅 Python 语言参考中的 section on whitespace。
由于短路评估,该行的其余部分没有被评估,当然,尽管它被解析和编译了。
使用相同的“技巧”,您可以编写类似的混淆代码 Python 不会抛出异常的代码,例如:
>>> 0xbin b'in'
False
>>> 0xbis 1000
False
>>> 0b1and 0b1is 0b00
False
>>> 0o1if 0b1else Oy1then
1
其他答案已经说明了到底发生了什么。但对我来说,有趣的是即使数字和它之间没有空格,运算符也能被识别。实际上,我的第一个想法是“哇,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
是的,FWIW,Python 并不孤单,所有其他脚本类型语言也能识别字母运算符,即使它们紧跟在数字常量的后面。
当运行下面一行:
>>> [0xfor x in (1, 2, 3)]
我预计 Python 到 return 一个错误。
相反,REPL returns:
[15]
可能是什么原因?
TL;DR
Python 将表达式读作 [0xf or (x in (1, 2, 3))]
,因为:
由于 short-circuit evaluation,它永远不会引发 NameError
- 如果 or
运算符左边的表达式是真值,Python 将永远不会尝试计算右边的值它的侧面。
解析十六进制数
首先,我们要了解Python如何读取十六进制数。
关于 tokenizer.c 的巨大 tok_get
功能,我们:
- Find第一个
0x
. - 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
的 operator precedence 比 or
高,我们可能希望 x in (1, 2, 3)
先评估。
这是个麻烦的情况,因为 x
不存在并且会引发 NameError
。
or
懒惰
幸运的是,Python 支持 Short-circuit evaluation,因为 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.c的parsenumber_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_strtoul
在 Python/mystrtoul.c
.
在 mystrtoul.c 内,解析器查看 one character after the 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;
} ...
然后就是parses剩下的数字只要字符在0-f范围内即可:
while ((c = _PyLong_DigitValue[Py_CHARMASK(*str)]) < base) {
if (ovlimit > 0) /* no overflow check required */
result = result * base + c;
...
++str;
--ovlimit;
}
Eventually,它将指针设置为指向扫描的最后一个字符 - 这是最后一个十六进制字符后一个字符:
if (ptr)
*ptr = (char *)str;
谢谢
-
来自 reddit 的
- CSI_Tech_Dept 推荐我到 tokenizer.c 文件中的正确部分。
- The original Tweet.
正如其他人所解释的,它只是十六进制数 0xf
后跟运算符 or
。运算符通常不需要周围的空间,除非有必要避免歧义。在这种情况下,字母 o
不能是十六进制数的一部分,因此不会产生歧义。请参阅 Python 语言参考中的 section on whitespace。
由于短路评估,该行的其余部分没有被评估,当然,尽管它被解析和编译了。
使用相同的“技巧”,您可以编写类似的混淆代码 Python 不会抛出异常的代码,例如:
>>> 0xbin b'in'
False
>>> 0xbis 1000
False
>>> 0b1and 0b1is 0b00
False
>>> 0o1if 0b1else Oy1then
1
其他答案已经说明了到底发生了什么。但对我来说,有趣的是即使数字和它之间没有空格,运算符也能被识别。实际上,我的第一个想法是“哇,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
是的,FWIW,Python 并不孤单,所有其他脚本类型语言也能识别字母运算符,即使它们紧跟在数字常量的后面。