Ruby,与 Python 不同,它使许多事情变得隐式,并且有一种特殊的 if 表达式很好地证明了这一点。它通常被称为“内联 if”或“条件修饰符”,这种特殊的语法能够在条件为真时返回一个值,但在条件为假时返回另一个值(具体来说是 nil
)。这是一个例子
$ irb
irb(main):> RUBY_VERSION
=> "2.7.1"
irb(main):> a = 42 if true
=> 42
irb(main):> b = 21 if false
=> nil
irb(main):> b
=> nil
irb(main):> a
=> 42
在 Python 中,如果不显式地向表达式添加 else
子句,就无法做到这一点。事实上,截至 此 PR,解释器会立即告诉您 else
是强制性的
$ python
Python 3.11.0a0
>>> a = 42 if True
File "<stdin>", line 1
;a = 42 if True
^^^^^^^^^^
SyntaxError: expected 'else' after 'if' expression
但是,我发现 Ruby 的 if
实际上非常方便。

Python 接受类似于 Ruby 的无 else if 语句。
当我不得不回到 Python 并编写这样的代码时,这种便利性变得更加明显
>>> my_var = 42 if some_cond else None
所以我心想,如果 Python 也有类似的功能会怎么样?我自己能做到吗?这有多难?
查看 Python 的源代码
深入研究 CPython 的代码并更改语言的语法对我来说听起来并非易事。幸运的是,在同一周,我在 Twitter 上发现 Anthony Shaw 刚刚写了一本关于 CPython 内部机制的书,并且可以预售。我没有多想就买了这本书。老实说,我是那种买东西但不立即使用的人。由于我还有其他计划,我让它在我的主文件夹中“积灰”,直到我不得不再次使用 Ruby 服务。它让我想起了 CPython 内部机制的书,以及破解 Python 内部机制将是多么具有挑战性。
第一件事是从头到尾通读这本书,并尝试遵循每个步骤。这本书的重点是 Python 3.9,所以为了遵循它,需要检出 3.9 标签,我也是这样做的。我了解了代码的结构以及如何编译它。接下来的章节展示了如何扩展语法并添加新内容,例如新的运算符。
随着我熟悉代码库以及如何调整语法,我决定试一试并对其进行自己的更改。
第一次(失败的)尝试
当我开始从最新的主分支中找到在 CPython 代码中的方向时,我注意到自 Python 3.9 以来,很多东西都发生了变化,但一些基本概念并没有改变。
我的第一次尝试是深入研究语法定义并找到 if 表达式规则。该文件目前名为 Grammar/python.gram
。找到它并不困难。普通的 CTRL+F 搜索 else
关键字就足够了。
file: Grammar/python.gram
...
expression[expr_ty] (memo):
| invalid_expression
| a=disjunction 'if' b=disjunction 'else' c=expression { _PyAST_IfExp(b, a, c, EXTRA) }
| disjunction
| lambdef
....
现在有了规则,我的想法是在当前的 if
表达式中添加一个选项,使其匹配 a=disjunction 'if' b=disjunction
,而 c
表达式将为 NULL
。
这个新规则应该紧跟在完整规则之后,否则,解析器将始终匹配 a=disjunction 'if' b=disjunction
,并返回 SyntaxError
。
...
expression[expr_ty] (memo):
| invalid_expression
| a=disjunction 'if' b=disjunction 'else' c=expression { _PyAST_IfExp(b, a, c, EXTRA) }
| a=disjunction 'if' b=disjunction { _PyAST_IfExp(b, a, NULL, EXTRA) }
| disjunction
| lambdef
....
重新生成解析器并从源代码编译 Python
CPython 附带一个 Makefile
,其中包含许多有用的命令。其中之一是 regen-pegen
命令,它将 Grammar/python.gram
转换为 Parser/parser.c
。
除了更改语法之外,我还必须修改 if 表达式的 AST。AST 代表抽象语法树,它是将语法的句法结构表示为树的一种方式。有关 AST 的更多信息,我强烈推荐 《Crafting Interpreters》 这本书,作者是 Robert Nystrom。
继续,如果您观察 if 表达式的规则,它看起来像这样
| a=disjunction 'if' b=disjunction 'else' c=expression { _PyAST_IfExp(b, a, c, EXTRA) }
这意味着当解析器找到此规则时,它会调用 _PyAST_IfExp
,后者返回一个 expr_ty
数据结构。因此,这给了我一个线索,要实现新规则的行为,我需要更改 _PyAST_IfExp
。
为了找到它的位置,我使用了我的 rip-grep
技能并在源代码根目录中搜索了它
$ rg _PyAST_IfExp -C2 .
[OMITTED]
Python/Python-ast.c
2686-
2687-expr_ty
2688:_PyAST_IfExp(expr_ty test, expr_ty body, expr_ty orelse, int lineno, int
2689- col_offset, int end_lineno, int end_col_offset, PyArena *arena)
2690-{
[OMITTED]
实现如下所示
expr_ty
_PyAST_IfExp(expr_ty test, expr_ty body, expr_ty orelse, int lineno, int
col_offset, int end_lineno, int end_col_offset, PyArena *arena)
{
expr_ty p;
if (!test) {
PyErr_SetString(PyExc_ValueError,
"field 'test' is required for IfExp");
return NULL;
}
if (!body) {
PyErr_SetString(PyExc_ValueError,
"field 'body' is required for IfExp");
return NULL;
}
if (!orelse) {
PyErr_SetString(PyExc_ValueError,
"field 'orelse' is required for IfExp");
return NULL;
}
p = (expr_ty)_PyArena_Malloc(arena, sizeof(*p));
if (!p)
return NULL;
p->kind = IfExp_kind;
p->v.IfExp.test = test;
p->v.IfExp.body = body;
p->v.IfExp.orelse = orelse;
p->lineno = lineno;
p->col_offset = col_offset;
p->end_lineno = end_lineno;
p->end_col_offset = end_col_offset;
return p;
}
由于我传递了 orelseNULL,我认为这只是将 if (!orelse)
None 的主体更改为 orelse
的问题。它看起来像这样
if (!orelse) {
- PyErr_SetString(PyExc_ValueError,
- "field 'orelse' is required for IfExp");
- return NULL;
+ orelse = Py_None;
}
现在是测试的时候了。我用 make -j8 -s
编译了代码并启动了解释器
$ make -j8 -s
Python/Python-ast.c: In function ‘_PyAST_IfExp’:
Python/Python-ast.c:2703:16: warning: assignment from incompatible pointer type [-Wincompatible-pointer-types]
orelse = Py_None;
尽管有明显的警告,但我还是决定忽略它,只是想看看会发生什么。
$ ./python
Python 3.11.0a0 (heads/ruby-if-new-dirty:f92b9133ef, Aug 2 2021, 09:13:02) [GCC 7.5.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> a = 42 if True
>>> a
42
>>> b = 21 if False
[1] 16805 segmentation fault (core dumped) ./python
哎哟!它适用于 if True
的情况,但是将 Py_None
分配给 expr_ty orelse
导致了 segfault
。
是时候回去看看哪里出错了。
第二次尝试
找出我哪里搞砸了并不难。orelse
是一个 expr_ty
,我将 Py_None
分配给它,而 Py_None
是一个 PyObject *
。再次感谢 rip-grep
,我找到了它的定义
$ rg constant -tc -C2
Include/internal/pycore_asdl.h
14-typedef PyObject * string;
15-typedef PyObject * object;
16:typedef PyObject * constant;
现在,我是如何发现 Py_None
是一个常量的呢?
在查看 Grammar/python.gram
文件时,我发现新的模式匹配语法的规则之一是这样定义的
# Literal patterns are used for equality and identity constraints
literal_pattern[pattern_ty]:
| value=signed_number !('+' | '-') { _PyAST_MatchValue(value, EXTRA) }
| value=complex_number { _PyAST_MatchValue(value, EXTRA) }
| value=strings { _PyAST_MatchValue(value, EXTRA) }
| 'None' { _PyAST_MatchSingleton(Py_None, EXTRA) }
但是,此规则是 pattern_ty
,而不是 expr_ty
。但这没关系。真正重要的是理解 _PyAST_MatchSingleton
实际上是什么。然后,我在 Python/Python-ast.c:
中搜索了它
file: Python/Python-ast.c
...
pattern_ty
_PyAST_MatchSingleton(constant value, int lineno, int col_offset, int
end_lineno, int end_col_offset, PyArena *arena)
...
我查找了语法中 None
节点的定义。令我欣慰的是,我找到了它!
atom[expr_ty]:
| NAME
| 'True' { _PyAST_Constant(Py_True, NULL, EXTRA) }
| 'False' { _PyAST_Constant(Py_False, NULL, EXTRA) }
| 'None' { _PyAST_Constant(Py_None, NULL, EXTRA) }
....
至此,我已经掌握了所有需要的信息。要返回表示 None
的 expr_ty
,我需要在 AST 中创建一个节点,该节点使用 _PyAST_Constant
函数是常量。
| a=disjunction 'if' b=disjunction 'else' c=expression { _PyAST_IfExp(b, a, c, EXTRA) }
- | a=disjunction 'if' b=disjunction { _PyAST_IfExp(b, a, NULL, EXTRA) }
+ | a=disjunction 'if' b=disjunction { _PyAST_IfExp(b, a, _PyAST_Constant(Py_None, NULL, EXTRA), EXTRA) }
| disjunction
接下来,我还必须恢复 Python/Python-ast.c
。由于我向它提供了一个有效的 expr_ty
,因此它永远不会是 NULL
。
file: Python/Python-ast.c
...
if (!orelse) {
- orelse = Py_None;
+ PyErr_SetString(PyExc_ValueError,
+ "field 'orelse' is required for IfExp");
+ return NULL;
}
...
我再次编译了它
$ make -j8 -s && ./python
Python 3.11.0a0 (heads/ruby-if-new-dirty:25c439ebef, Aug 2 2021, 09:25:18) [GCC 7.5.0] on linux
Type "help", "copyright", "credits" or "license" for more information.
>>> c = 42 if True
>>> c
42
>>> b = 21 if False
>>> type(b)
<class 'NoneType'>
>>>
它工作了!
现在,我需要再做一次测试。Ruby 函数允许在条件匹配时返回值,如果条件不匹配,则执行函数体的其余部分。像这样
> irb
irb(main):> def f(test)
irb(main):> return 42 if test
irb(main):> puts 'missed return'
irb(main):> return 21
irb(main):> end
=> :f
irb(main):> f(false)
missed return
=> 21
irb(main):> f(true)
=> 42
此时,我想知道这是否适用于我修改后的 Python。我再次冲到解释器并编写了相同的函数
>>> def f(test):
... return 42 if test
... print('missed return')
... return 21
...
>>> f(False)
>>> f(True)
42
>>>
如果 test 为 False
,则该函数返回 None
... 为了帮助我调试,我调用了 ast 模块。官方文档对其定义如下
ast 模块帮助 Python 应用程序处理 Python 抽象语法语法的树。抽象语法本身可能会随着每个 Python 版本的发布而改变;此模块有助于以编程方式找出当前语法的样子。
我打印了这个函数的 AST
>>> fc = '''
... def f(test):
... return 42 if test
... print('missed return')
... return 21
... '''
>>> print(ast.dump(ast.parse(fc), indent=4))
Module(
body=[
FunctionDef(
name='f',
args=arguments(
posonlyargs=[],
args=[
arg(arg='test')],
kwonlyargs=[],
kw_defaults=[],
defaults=[]),
body=[
Return(
value=IfExp(
test=Name(id='test', ctx=Load()),
;body=Constant(value=42),
orelse=Constant(value=None))),
Expr(
value=Call(
func=Name(id='print', ctx=Load()),
args=[
Constant(value='missed return')],
keywords=[])),
Return(
value=Constant(value=21))],
decorator_list=[])],
type_ignores=[])
现在事情变得更有意义了。我对语法的更改只是“语法糖”。它将这样的表达式:a if b
变成这样:a if b else None
。这里的问题是 Python 无论如何都会返回,因此函数体的其余部分将被忽略。
您可以查看生成的 字节码 以了解解释器实际执行的内容。为此,您可以使用 dis
模块。根据文档
dis 模块支持通过反汇编来分析 CPython 字节码。
>>> import dis
>>> dis.dis(f)
2 0 LOAD_FAST 0 (test)
2 POP_JUMP_IF_FALSE 4 (to 8)
4 LOAD_CONST 1 (42)
6 RETURN_VALUE
>> 8 LOAD_CONST 0 (None)
10 RETURN_VALUE
这基本上意味着,如果 test 为假,则执行跳转到 8,这将 None
加载到堆栈顶部并返回它。
支持“return-if”
为了支持相同的 Ruby 功能,我需要将表达式 return 42 if test
转换为常规 if
语句,该语句在 test
为真时返回。
为此,我需要添加一个规则。这一次,它将是一个匹配 return <value> if <test>
代码段的规则。不仅如此,我还需要一个 _PyAST_
函数来为我创建节点。然后我将其称为 _PyAST_ReturnIfExpr:
file: Grammar/python.gram
return_stmt[stmt_ty]:
+ | 'return' a=star_expressions 'if' b=disjunction { _PyAST_ReturnIfExpr(a, b, EXTRA) }
| 'return' a=[star_expressions] { _PyAST_Return(a, EXTRA) }
如前所述,所有这些函数的实现都位于 Python/Python-ast.c
中,它们的定义在 Include/internal/pycore_ast.h
中,所以我将 _PyAST_ReturnIfExpr
放在那里
file: Include/internal/pycore_ast.h
stmt_ty _PyAST_Return(expr_ty value, int lineno, int col_offset, int
end_lineno, int end_col_offset, PyArena *arena);
+stmt_ty _PyAST_ReturnIfExpr(expr_ty value, expr_ty test, int lineno, int col_of
fset, int
+ end_lineno, int end_col_offset, PyArena *arena);
stmt_ty _PyAST_Delete(asdl_expr_seq * targets, int lineno, int col_offset, int
end_lineno, int end_col_offset, PyArena *arena);
file: Python/Python-ast.c
+stmt_ty
+_PyAST_ReturnIfExpr(expr_ty value, expr_ty test, int lineno, int col_offset, int end_lineno, int
+ end_col_offset, PyArena *arena)
+{
+ stmt_ty ret, p;
+ ret = _PyAST_Return(value, lineno, col_offset, end_lineno, end_col_offset, arena);
+
+ asdl_stmt_seq *body;
+ body = _Py_asdl_stmt_seq_new(1, arena);
+ asdl_seq_SET(body, 0, ret);
+
+ p = _PyAST_If(test, body, NULL, lineno, col_offset, end_lineno, end_col_offset, arena);
+
+ return p;
+}
+
stmt_ty
我检查了 _PyAST_ReturnIfExpr
的实现。我想将 return <value> if <test>
转换为 if <test>: return <value>
。
return
和常规 if
都是语句,因此在 CPython 中,它们表示为 stmt_ty
。_PyAST_If
期望一个 expr_ty test
和一个主体,主体是一系列语句。在这种情况下,body
是 asdl_stmt_seq *body
。
因此,我真正想要的是一个 if
语句,其主体中唯一的语句是 return <value>
语句。
CPython 处置了一些方便的函数来构建 asdl_stmt_seq *
,其中之一是 _Py_asdl_stmt_seq_new
。所以我用它来创建主体,并将我之前用 _PyAST_Return
创建的 return 语句添加进去。
完成之后,最后一步是将 test
以及 body
传递给 _PyAST_If
。
在我忘记之前,您可能想知道 PyArena *arena
到底是什么。Arena 是 CPython 用于内存分配的抽象。它通过使用内存映射 mmap() 并将其放置在连续的 内存块 中来实现高效的内存使用。
是时候重新生成解析器并再次测试它了
>>> def f(test):
... return 42 if test
... print('missed return')
... return 21
...
>>> import dis
>>> f(False)
>>> f(True)
42
它不起作用。检查字节码
>>> dis.dis(f)
2 0 LOAD_FAST 0 (test)
2 POP_JUMP_IF_FALSE 4 (to 8)
4 LOAD_CONST 1 (42)
6 RETURN_VALUE
>> 8 LOAD_CONST 0 (None)
10 RETURN_VALUE
>>>
又是相同的字节码指令!
回到编译器课程
那时,我毫无头绪。我不知道发生了什么,直到我决定深入研究扩展语法规则的兔子洞。
我添加的新规则如下:'return' a=star_expressions 'if' b=disjunction { _PyAST_ReturnIfExpr(a, b, EXTRA) }
。
我唯一的假设是 a=star_expressions 'if' b=disjunction
正在被解析为我一开始添加的无 else 规则。
通过再次查看语法,我发现我的理论成立。star_expressions
将匹配 a=disjunction 'if' b=disjunction { _PyAST_IfExp(b, a, NULL, EXTRA) }
。
唯一的解决方法是摆脱 star_expressions
。所以我将规则更改为
return_stmt[stmt_ty]:
- | 'return' a=star_expressions 'if' b=disjunction { _PyAST_ReturnIfExpr(a, b, EXTRA) }
+ | 'return' a=disjunction guard=guard !'else' { _PyAST_ReturnIfExpr(a, guard, EXTRA) }
| 'return' a=[star_expressions] { _PyAST_Return(a, EXTRA) }
您可能想知道,guard,
!else
和 star_expressions
是什么?
这个 guard
是模式匹配规则的一部分。Python 3.10 中添加的新模式匹配功能允许这样的操作
match point:
case Point(x, y) if x == y:
print(f"Y=X at {x}")
case Point(x, y):
print(f"Not on the diagonal")
规则是这样的
guard[expr_ty]: 'if' guard=named_expression { guard }
有了这个,我添加了一个检查。为了避免它出现 SyntaxError
错误,我需要确保该规则仅匹配如下代码:return value if cond
。因此,为了防止诸如 return an if cond else b
之类的代码被过早匹配,我在规则中添加了 !' else
。
最后但并非最不重要的一点是,star_expressions
允许我返回解构的可迭代对象。例如
>>> def f():
...: a = [1, 2]
...: return 0, *a
...:&
>>> f()
(0, 1, 2)
在这种情况下,0, * a
是一个元组,它属于 star_expressions
的类别。据我所知,常规的 if 表达式不允许将 star_expressions
与之一起使用,因此更改新的 return
规则不会有问题。
它现在工作了吗?
在修复 return 规则后,我再次重新生成了语法并编译了它
>>> def f(test):
... return 42 if test
... print('missed return')
... return 21
...
>>> f(False)
missed return
21
>>> f(True)
42
它工作了!
查看字节码
>>> import dis
>>> dis.dis(f)
2 0 LOAD_FAST 0 (test)
2 POP_JUMP_IF_FALSE 4 (to 8)
4 LOAD_CONST 1 (42)
6 RETURN_VALUE
3 >> 8 LOAD_GLOBAL 0 (print)
10 LOAD_CONST 2 ('missed return')
12 CALL_FUNCTION 1
14 POP_TOP
4 16 LOAD_CONST 3 (21)
18 RETURN_VALUE
>>>
这正是我想要的。AST 与常规 if
的 AST 相同吗?
>>> import ast
>>> print(ast.dump(ast.parse(fc), indent=4))
Module(
body=[
FunctionDef(
name='f',
args=arguments(
posonlyargs=[],
args=[
arg(arg='test')],
kwonlyargs=[],
kw_defaults=[],
defaults=[]),
body=[
If(
test=Name(id='test', ctx=Load()),
body=[
Return(
value=Constant(value=42))],
orelse=[]),
Expr(
value=Call(
func=Name(id='print', ctx=Load()),
args=[
Constant(value='missed return')],
keywords=[])),
Return(
value=Constant(value=21))],
decorator_list=[])],
type_ignores=[])
>>>
确实是!
If(
test=Name(id='test', ctx=Load()),
body=[
Return(
value=Constant(value=42))],
orelse=[]),
此节点与以下代码生成的节点相同
if test: return 42
如果未经测试,它会损坏吗?
为了结束这段旅程,我认为添加一些单元测试也是一个好主意。在编写任何新代码之前,我想了解一下我破坏了什么。
通过手动测试代码,我使用 test
模块 python -m test -j8
运行了所有测试。-j8
表示它使用八个进程并行运行测试
$ ./python -m test -j8
令我惊讶的是,只有一个测试失败了!
== Tests result: FAILURE ==
406 tests OK.
1 test failed:
test_grammar
由于我运行了所有测试,因此很难浏览输出,因此我可以再次单独运行这一个
======================================================================
FAIL: test_listcomps (test.test_grammar.GrammarTests)
----------------------------------------------------------------------
Traceback (most recent call last):
File "/home/miguel/projects/cpython/Lib/test/test_grammar.py", line 1732, in test_listcomps
check_syntax_error(self, "[x if y]")
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
File "/home/miguel/projects/cpython/Lib/test/support/__init__.py", line 497, in check_syntax_error
with testcase.assertRaisesRegex(SyntaxError, errtext) as cm:
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
AssertionError: SyntaxError not raised
----------------------------------------------------------------------
Ran 76 tests in 0.038s
FAILED (failures=1)
test test_grammar failed
test_grammar failed (1 failure)
== Tests result: FAILURE ==
1 test failed:
test_grammar
1 re-run test:
test_grammar
Total duration: 82 ms
Tests result: FAILURE
就在这里!当运行 [x if y]
表达式时,它期望出现语法错误。我可以安全地删除它并再次重新运行测试
== Tests result: SUCCESS ==
1 test OK.
Total duration: 112 ms
Tests result: SUCCESS
现在一切正常,是时候添加更多测试了。重要的是不仅要测试新的“无 else if”,还要测试新的 return
语句。
通过浏览 test_grammar.py
文件,我可以找到几乎每个语法规则的测试。我首先查找的是 test_if_else_expr
。此测试没有失败,因此它仅测试了快乐的情况。为了使其更健壮,我需要添加两个新测试来检查 if True
和 if False
的情况
self.assertEqual((6 < 4 if 0), None)
self.assertEqual((6 < 4 if 1), False)
我再次运行了所有内容,这次所有测试都通过了。
注意:Python 中的 bool
是 整数的子类,因此您可以使用 1
表示 True
,使用 0
表示 False
。
Ran 76 tests in 0.087s
OK
== Tests result: SUCCESS ==
1 test OK.
Total duration: 174 ms
Tests result: SUCCESS
最后,我需要 return
规则的测试。它们在 test_return
测试中定义。就像 if
表达式一样,此测试在没有修改的情况下通过了。
为了测试这个新的用例,我创建了一个函数,该函数接收一个 bool
参数,并在参数为真时返回。当它为假时,它会跳过 return,就像我到目前为止一直在进行的手动测试一样
def g4(test):
a = 1
return a if test
a += 1
return a
self.assertEqual(g4(False), 2)
self.assertEqual(g4(True), 1)
我保存了文件并再次重新运行了 test_grammar
----------------------------------------------------------------------
Ran 76 tests in 0.087s
OK
== Tests result: SUCCESS ==
1 test OK.
Total duration: 174 ms
Tests result: SUCCESS
看起来不错!test_grammar
测试通过了。以防万一,我重新运行了完整的测试套件
$ ./python -m test -j8
过了一会儿,所有测试都通过了,我对结果感到非常满意。
局限性
如果您精通 Ruby,到目前为止,您可能已经注意到,我在这里所做的与条件修饰符并非 100% 相同。例如,在 Ruby 中,您可以在这些修饰符中运行实际表达式
irb(main):002:0> a = 42
irb(main):003:0> a += 1 if false
=> nil
irb(main):004:0> a
=> 42
irb(main):005:0> a += 1 if true
=> 43
我无法使用我的实现做到这一点
>>> a = 42
>>> a += 1 if False
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: unsupported operand type(s) for +=: 'int' and 'NoneType'
>>> a += 1 if True
>>> a
43
这表明我创建的 return
规则只是一个变通方法。如果我想使其尽可能接近 Ruby 的条件修饰符,我需要使其也与其他语句一起使用,而不仅仅是 return
。
不过,这很好。我进行此实验的目标只是更多地了解 Python 内部机制,并了解我将如何浏览用 C 编写的鲜为人知的代码库并对其进行适当的更改。我不得不承认,我对结果非常满意!
结论
添加受 Ruby 启发的新语法是学习更多 Python 内部机制的一个非常好的练习。当然,如果我要将其转换为 PR,核心开发人员可能会发现一些缺点,正如我在上一节中已经描述的那样。但是,由于我这样做只是为了好玩,所以我对结果非常满意。
包含我所有更改的源代码在我的 CPython 分支 ruby-if-new 分支 下。
本文最初发表在 作者的个人博客 上,并已获得许可进行改编。
评论已关闭。