Python调试: 显示最后N个执行的行

15

我希望能够看到在这个异常发生之前,Python解释器执行的最后10行代码:

test_has_perm_in_foobar.py F
Traceback (most recent call last):
  File "/.../test_has_perm_in_foobar.py", line 50, in test_has_perm
    self.assertFalse(check_perm(request, some_object))
  File "/usr/lib/python2.7/unittest/case.py", line 416, in assertFalse
    raise self.failureException(msg)
AssertionError: True is not false

我想看到 check_perm() 返回 True 的地方。

我知道我可以使用交互式调试来找到匹配的行,但是我很懒,希望找到一种更简单的方法来找到 check_perm() 返回值的所在行。

我使用pyCharm,但是一个基于文本的工具也可以解决我的需求。

顺便说一句:请不要告诉我如何使用step-over和step-into调试器。我已经知道了。

这里有一些示例代码。

def check_perm(request, some_object):
    if condition_1:
        return True
    if condition_2:
        return sub_check(some_object)
    if condition_3:
        return sub_check2(some_object)
    ...

有几种情况下check_perm()会返回True。 如果因为condition_1而返回True,那么我希望看到类似于这样的东西

+         if condition_1:
+            return True

我心目中想要的输出类似于在shell上使用set -x

更新

cgitb、pytest和其他工具可以显示在断言失败之前执行的行。但是,它们只显示当前Python文件的行。这个问题涉及到覆盖所有文件,包括断言之前执行的所有行。在我的情况下,我想知道check_perm()的返回值是在哪里创建的。工具pytest、cgitb等都没有显示这个。

我正在寻找的就像在shell上使用set -x一样:

帮助 set

-x 执行命令时,打印命令及其参数。

5个回答

4

出于这个原因,我已经将测试转向了pytest

它可以显示不同详细级别的本地变量和回溯信息。调用所在的行用>标记。

例如,在我的Django项目中:

$ py.test --showlocals --tb=long
=============================== test session starts ===============================
platform darwin -- Python 3.5.1, pytest-3.0.3, py-1.4.31, pluggy-0.4.0
Django settings: dj_tg_bot.settings (from ini file)
rootdir: /Users/el/Projects/dj-tg-alpha-bot, inifile: tox.ini
plugins: django-3.0.0, cov-2.4.0
collected 8 items

tests/test_commands.py ....F
tests/test_logger.py .
tests/test_simple.py ..

==================================== FAILURES =====================================
__________________________ TestSimpleCommands.test_start __________________________

self = <tests.test_commands.TestSimpleCommands testMethod=test_start>

    def test_start(self,):
        """
            Test bot accept normally command /start and replies as it should.
            """
>       self._test_message_ok(self.start)

self       = <tests.test_commands.TestSimpleCommands testMethod=test_start>

tests/test_commands.py:56:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <tests.test_commands.TestSimpleCommands testMethod=test_start>
action = {'in': ' /start', 'out': {'parse_mode': 'Markdown', 'reply_markup': '', 'text': 'Welcome'}}
update = <telegram.update.Update object at 0x113e16cf8>, number = 1

    def _test_message_ok(self, action, update=None, number=1):
        if not update:
            update = self.update
        with mock.patch("telegram.bot.Bot.sendMessage", callable=mock.MagicMock()) as mock_send:
            if 'in' in action:
                update.message.text = action['in']
            response = self.client.post(self.webhook_url, update.to_json(), **self.kwargs)
            #  Check response 200 OK
            self.assertEqual(response.status_code, status.HTTP_200_OK)
            #  Check
>           self.assertBotResponse(mock_send, action)

action     = {'in': ' /start', 'out': {'parse_mode': 'Markdown', 'reply_markup': '', 'text': 'Welcome'}}
mock_send  = <MagicMock name='sendMessage' id='4619939344'>
number     = 1
response   = <Response status_code=200, "application/json">
self       = <tests.test_commands.TestSimpleCommands testMethod=test_start>
update     = <telegram.update.Update object at 0x113e16cf8>

../../.pyenv/versions/3.5.1/lib/python3.5/site-packages/telegrambot/test/testcases.py:83:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

self = <tests.test_commands.TestSimpleCommands testMethod=test_start>
mock_send = <MagicMock name='sendMessage' id='4619939344'>
command = {'in': ' /start', 'out': {'parse_mode': 'Markdown', 'reply_markup': '', 'text': 'Welcome'}}

    def assertBotResponse(self, mock_send, command):
>       args, kwargs = mock_send.call_args
E       TypeError: 'NoneType' object is not iterable

command    = {'in': ' /start', 'out': {'parse_mode': 'Markdown', 'reply_markup': '', 'text': 'Welcome'}}
mock_send  = <MagicMock name='sendMessage' id='4619939344'>
self       = <tests.test_commands.TestSimpleCommands testMethod=test_start>

../../.pyenv/versions/3.5.1/lib/python3.5/site-packages/telegrambot/test/testcases.py:61: TypeError
------------------------------ Captured stderr call -------------------------------
Handler not found for {'message': {'from': {'username': 'username_4', 'last_name': 'last_name_4', 'id': 5, 'first_name': 'first_name_4'}, 'chat': {'username': 'username_4', 'last_name': 'last_name_4', 'first_name': 'first_name_4', 'title': 'title_4', 'type': 'private', 'id': 5}, 'text': ' /start', 'message_id': 5, 'date': 1482500826}, 'update_id': 5}
======================= 1 failed, 7 passed in 2.29 seconds ========================
(.env) ✘-1 ~/Projects/dj-tg-alpha-bot [master|✚ 1812]
16:47 $

我也使用pytest。是的,它会显示断言失败之前的代码行。但这仅限于当前文件。如果有一个断言检查方法调用的返回值,那么你看不到返回发生在哪里。这就是我的问题所在。我知道这很难解释。如果您知道如何更好地表达我的问题,请告诉我。 - guettli
我理解你的想法是从之前的侧面调用中收集所有数据。但在我看来,这似乎更多地涉及调试而不是测试。如果用单元测试覆盖它们以确保它们正常工作呢? - Eugene Lisitsky

1

那cgitb呢?你只需要将这个模块导入到你的代码中。

import cgitb
cgitb.enable(format='text')

def f():
    a = 1
    b = 2
    c = 3
    x = 0
    d = a * b * c / x
    return d

if __name__ == "__main__":
    f()

给出:
ZeroDivisionError
Python 3.5.2: /usr/bin/python3
Mon Dec 19 17:42:34 2016

A problem occurred in a Python script.  Here is the sequence of
function calls leading up to the error, in the order they occurred.
 /home/user1/123.py in <module>()
   10     d = a * b * c / x
   11     return x
   12 
   13 if __name__ == "__main__":
   14     f()
f = <function f>
 /home/user1/123.py in f()
    8     c = 3
    9     x = 0
   10     d = a * b * c / x
   11     return x
   12 
d undefined
a = 1
b = 2
c = 3
x = 0
ZeroDivisionError: division by zero
...
The above is a description of an error in a Python program.  Here is
the original traceback:

Traceback (most recent call last):
  File "123.py", line 14, in <module>
    f()
  File "123.py", line 10, in f
    d = a * b * c / x
ZeroDivisionError: division by zero

是的,我喜欢cgitb的输出(以及django调试页面)。我猜这些并没有给我我要找的信息。在我的情况下,我没有得到异常。该方法返回一个值。但实际值来自哪里?我更新了问题以说明我想要什么。尽管如此,感谢您的反馈。 - guettli

1

因为我找不到解决方案,所以我自己写了这个:

with trace_function_calls():    
    self.assertFalse(check_perm(request, some_object))

实现 trace_function_calls() 函数:
class trace_function_calls(object):
    depth_symbol = '+'

    def __init__(self, write_method=None, log_lines=True):
        '''
        write_method: A method which gets called for every executed line. Defauls to logger.info

        # Simple example:

        with debugutils.trace_function_calls():
            method_you_want_to_trace()
        '''
        if write_method is None:
            write_method=logger.info
        self.write_method = write_method
        self.log_lines = log_lines

    def __enter__(self):
        self.old = sys.gettrace()
        self.depth = 0
        sys.settrace(self.trace_callback)

    def __exit__(self, type, value, traceback):
        sys.settrace(self.old)

    def trace_callback(self, frame, event, arg):
        # from http://pymotw.com/2/sys/tracing.html#tracing-function-calls
        if event == 'return':
            self.depth -= 1
            return self.trace_callback

        if event == 'line':
            if not self.log_lines:
                return self.trace_callback
        elif event == 'call':
            self.depth += 1
        else:
            # self.write_method('unknown: %s' % event)
            return self.trace_callback

        msg = []
        msg.append(self.depth_symbol * self.depth)

        co = frame.f_code
        func_name = co.co_name
        func_line_no = frame.f_lineno

        func_filename = co.co_filename
        if not is_python_file_from_my_codebase(func_filename):
            return self.trace_callback
        code_line = linecache.getline(func_filename, func_line_no).rstrip()
        msg.append('%s: %s %r on line %s of %s' % (
            event, func_name, code_line, func_line_no, func_filename))
        self.write_method(' '.join(msg))
        return self.trace_callback

PS:这是开源软件。如果您想创建Python包,请直接操作并告诉我,我会感到很高兴。


1

trace 模块具有类似于 bourne 兼容 shell 的 set -x 特性。 trace.Trace 类的 trace 参数启用行执行跟踪。该类还接受一个 ignoredirs 参数,用于忽略位于指定目录下方的跟踪模块或包。我在这里使用它来防止跟踪 unittest 模块。

test_has_perm_in_foobar.py

import sys
import trace
import unittest

from app import check_perm

tracer = trace.Trace(trace=1, ignoredirs=(sys.prefix, sys.exec_prefix))

class Test(unittest.TestCase):
    def test_one(self):
        tracer.runctx('self.assertFalse(check_perm("dummy", 3))', globals(), locals())

if __name__ == '__main__':
    unittest.main()

app.py

def sub_check1(some_object):
    if some_object * 10 == 20:
        return True

def sub_check2(some_object):
    if some_object * 10 == 30:
        return True

def check_perm(request, some_object):
    if some_object == 1:
        return True
    if some_object == 2:
        return sub_check1(some_object)
    if some_object == 3:
        return sub_check2(some_object)

测试;

$ python test_has_perm_in_foobar.py 
 --- modulename: test_has_perm_in_foobar, funcname: <module>
<string>(1):   --- modulename: app, funcname: check_perm
app.py(10):     if some_object == 1:
app.py(12):     if some_object == 2:
app.py(14):     if some_object == 3:
app.py(15):         return sub_check2(some_object)
 --- modulename: app, funcname: sub_check2
app.py(6):     if some_object * 10 == 30:
app.py(7):         return True
F
======================================================================
FAIL: test_one (__main__.Test)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test_has_perm_in_foobar.py", line 23, in test_one
    tracer.runctx('self.assertFalse(check_perm("dummy", 3))', globals(), locals())
  File "/usr/lib/python2.7/trace.py", line 513, in runctx
    exec cmd in globals, locals
  File "<string>", line 1, in <module>
AssertionError: True is not false

----------------------------------------------------------------------
Ran 1 test in 0.006s

FAILED (failures=1)

为了让代码和输出更加简短,只需跟踪所需的函数。
import trace
import unittest

from app import check_perm

tracer = trace.Trace(trace=1)

class Test(unittest.TestCase):
    def test_one(self):
        self.assertFalse(tracer.runfunc(check_perm, 'dummy', 3))

if __name__ == '__main__':
    unittest.main()

测试;

$ python test_has_perm_in_foobar.py
 --- modulename: app, funcname: check_perm
app.py(10):     if some_object == 1:
app.py(12):     if some_object == 2:
app.py(14):     if some_object == 3:
app.py(15):         return sub_check2(some_object)
 --- modulename: app, funcname: sub_check2
app.py(6):     if some_object * 10 == 30:
app.py(7):         return True
F
======================================================================
FAIL: test_one (__main__.Test)
----------------------------------------------------------------------
Traceback (most recent call last):
  File "test_has_perm_in_foobar.py", line 19, in test_one
    self.assertFalse(tracer.runfunc(check_perm, 'dummy', 3))
AssertionError: True is not false

----------------------------------------------------------------------
Ran 1 test in 0.005s

FAILED (failures=1)

0
你考虑过以下的工作流吗?我看了你的BTW,但是有时候严格的规则会阻止我们解决问题(特别是当你陷入XY困境时),所以我建议你仍然使用调试器。我经常遇到测试失败的情况。当完整的堆栈跟踪对于解决问题至关重要时,我使用pdbpy.test的组合来获取整个信息。考虑以下程序...
import pytest

@pytest.mark.A
def test_add():

  a = 1
  b = 2

  add(a,b)

def add(a, b):
  assert a>b

  return a+b

def main():
  add(1,2)
  add(2,1)

if __name__ == "__main__":
  # execute only if run as a script
  main()

运行命令py.test -v -tb=short -m A code.py会产生以下输出...
art@macky ~/src/python/so-answer-stacktrace: py.test -v --tb=short -m A code.py
============================= test session starts ==============================
platform darwin -- Python 2.7.5 -- pytest-2.5.0 -- /Users/art/.pyenv/versions/2.7.5/bin/python
collected 1 items

code.py:3: test_add FAILED

=================================== FAILURES ===================================
___________________________________ test_add ___________________________________
code.py:9: in test_add
>     add(a,b)
code.py:12: in add
>     assert a>b
E     assert 1 > 2
=========================== 1 failed in 0.01 seconds ===========================

一种简单的调查堆栈跟踪的方法是在测试中放置一个 pdb 调试点,使用 pytest 标记标记单个测试,调用该测试,并在调试器中检查堆栈。就像这样...
def add(a, b):
  from pdb import set_trace;set_trace()
  assert a>b

  return a+b

现在当我再次运行相同的测试命令时,我会得到一个挂起的pdb调试器。就像这样...
art@macky ~/src/python/so-answer-stacktrace: py.test -v --tb=short -m A code.py
=========================================================================================== test session starts ============================================================================================
platform darwin -- Python 2.7.5 -- pytest-2.5.0 -- /Users/art/.pyenv/versions/2.7.5/bin/python
collected 1 items

code.py:3: test_add
>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> PDB set_trace (IO-capturing turned off) >>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>
> /Users/art/src/python/so-answer-stacktrace/code.py(13)add()
-> assert a>b
(Pdb)

如果此时我输入神奇的w,然后按下enter键,我就可以看到完整的堆栈跟踪信息...

(Pdb) w
  /Users/art/.pyenv/versions/2.7.5/bin/py.test(9)<module>()
-> load_entry_point('pytest==2.5.0', 'console_scripts', 'py.test')()
  /Users/art/.pyenv/versions/2.7.5/lib/python2.7/site-packages/pytest-2.5.0-py2.7.egg/_pytest/config.py(19)main()
-> return config.hook.pytest_cmdline_main(config=config)
  /Users/art/.pyenv/versions/2.7.5/lib/python2.7/site-packages/pytest-2.5.0-py2.7.egg/_pytest/core.py(376)__call__()
-> return self._docall(methods, kwargs)
  /Users/art/.pyenv/versions/2.7.5/lib/python2.7/site-packages/pytest-2.5.0-py2.7.egg/_pytest/core.py(387)_docall()
-> res = mc.execute()
  /Users/art/.pyenv/versions/2.7.5/lib/python2.7/site-packages/pytest-2.5.0-py2.7.egg/_pytest/core.py(288)execute()
-> res = method(**kwargs)
  /Users/art/.pyenv/versions/2.7.5/lib/python2.7/site-packages/pytest-2.5.0-py2.7.egg/_pytest/main.py(111)pytest_cmdline_main()
-> return wrap_session(config, _main)
  /Users/art/.pyenv/versions/2.7.5/lib/python2.7/site-packages/pytest-2.5.0-py2.7.egg/_pytest/main.py(81)wrap_session()
-> doit(config, session)
  /Users/art/.pyenv/versions/2.7.5/lib/python2.7/site-packages/pytest-2.5.0-py2.7.egg/_pytest/main.py(117)_main()
-> config.hook.pytest_runtestloop(session=session)
  /Users/art/.pyenv/versions/2.7.5/lib/python2.7/site-packages/pytest-2.5.0-py2.7.egg/_pytest/core.py(376)__call__()
-> return self._docall(methods, kwargs)
  /Users/art/.pyenv/versions/2.7.5/lib/python2.7/site-packages/pytest-2.5.0-py2.7.egg/_pytest/core.py(387)_docall()
-> res = mc.execute()
  /Users/art/.pyenv/versions/2.7.5/lib/python2.7/site-packages/pytest-2.5.0-py2.7.egg/_pytest/core.py(288)execute()
-> res = method(**kwargs)
  /Users/art/.pyenv/versions/2.7.5/lib/python2.7/site-packages/pytest-2.5.0-py2.7.egg/_pytest/main.py(137)pytest_runtestloop()
-> item.config.hook.pytest_runtest_protocol(item=item, nextitem=nextitem)
  /Users/art/.pyenv/versions/2.7.5/lib/python2.7/site-packages/pytest-2.5.0-py2.7.egg/_pytest/core.py(376)__call__()
-> return self._docall(methods, kwargs)
  /Users/art/.pyenv/versions/2.7.5/lib/python2.7/site-packages/pytest-2.5.0-py2.7.egg/_pytest/core.py(387)_docall()
-> res = mc.execute()
  /Users/art/.pyenv/versions/2.7.5/lib/python2.7/site-packages/pytest-2.5.0-py2.7.egg/_pytest/core.py(288)execute()
-> res = method(**kwargs)
  /Users/art/.pyenv/versions/2.7.5/lib/python2.7/site-packages/pytest-2.5.0-py2.7.egg/_pytest/runner.py(62)pytest_runtest_protocol()
-> runtestprotocol(item, nextitem=nextitem)
  /Users/art/.pyenv/versions/2.7.5/lib/python2.7/site-packages/pytest-2.5.0-py2.7.egg/_pytest/runner.py(72)runtestprotocol()
-> reports.append(call_and_report(item, "call", log))
  /Users/art/.pyenv/versions/2.7.5/lib/python2.7/site-packages/pytest-2.5.0-py2.7.egg/_pytest/runner.py(106)call_and_report()
-> call = call_runtest_hook(item, when, **kwds)
  /Users/art/.pyenv/versions/2.7.5/lib/python2.7/site-packages/pytest-2.5.0-py2.7.egg/_pytest/runner.py(124)call_runtest_hook()
-> return CallInfo(lambda: ihook(item=item, **kwds), when=when)
  /Users/art/.pyenv/versions/2.7.5/lib/python2.7/site-packages/pytest-2.5.0-py2.7.egg/_pytest/runner.py(137)__init__()
-> self.result = func()
  /Users/art/.pyenv/versions/2.7.5/lib/python2.7/site-packages/pytest-2.5.0-py2.7.egg/_pytest/runner.py(124)<lambda>()
-> return CallInfo(lambda: ihook(item=item, **kwds), when=when)
  /Users/art/.pyenv/versions/2.7.5/lib/python2.7/site-packages/pytest-2.5.0-py2.7.egg/_pytest/main.py(161)call_matching_hooks()
-> return hookmethod.pcall(plugins, **kwargs)
  /Users/art/.pyenv/versions/2.7.5/lib/python2.7/site-packages/pytest-2.5.0-py2.7.egg/_pytest/core.py(380)pcall()
-> return self._docall(methods, kwargs)
  /Users/art/.pyenv/versions/2.7.5/lib/python2.7/site-packages/pytest-2.5.0-py2.7.egg/_pytest/core.py(387)_docall()
-> res = mc.execute()
  /Users/art/.pyenv/versions/2.7.5/lib/python2.7/site-packages/pytest-2.5.0-py2.7.egg/_pytest/core.py(288)execute()
-> res = method(**kwargs)
  /Users/art/.pyenv/versions/2.7.5/lib/python2.7/site-packages/pytest-2.5.0-py2.7.egg/_pytest/runner.py(86)pytest_runtest_call()
-> item.runtest()
  /Users/art/.pyenv/versions/2.7.5/lib/python2.7/site-packages/pytest-2.5.0-py2.7.egg/_pytest/python.py(1076)runtest()
-> self.ihook.pytest_pyfunc_call(pyfuncitem=self)
  /Users/art/.pyenv/versions/2.7.5/lib/python2.7/site-packages/pytest-2.5.0-py2.7.egg/_pytest/main.py(161)call_matching_hooks()
-> return hookmethod.pcall(plugins, **kwargs)
  /Users/art/.pyenv/versions/2.7.5/lib/python2.7/site-packages/pytest-2.5.0-py2.7.egg/_pytest/core.py(380)pcall()
-> return self._docall(methods, kwargs)
  /Users/art/.pyenv/versions/2.7.5/lib/python2.7/site-packages/pytest-2.5.0-py2.7.egg/_pytest/core.py(387)_docall()
-> res = mc.execute()
  /Users/art/.pyenv/versions/2.7.5/lib/python2.7/site-packages/pytest-2.5.0-py2.7.egg/_pytest/core.py(288)execute()
-> res = method(**kwargs)
  /Users/art/.pyenv/versions/2.7.5/lib/python2.7/site-packages/pytest-2.5.0-py2.7.egg/_pytest/python.py(188)pytest_pyfunc_call()
-> testfunction(**testargs)
  /Users/art/src/python/so-answer-stacktrace/code.py(9)test_add()
-> add(a,b)
> /Users/art/src/python/so-answer-stacktrace/code.py(13)add()
-> assert a>b
(Pdb)

我在框架中做了很多工作。 pdb + where 可以让你看到程序的实际入口之前的所有内容。你可以在其中找到我的函数以及测试运行器的帧。如果这是 Django 或 Flask,我将看到这些框架内部所涉及的所有堆栈帧。它是我真正遇到问题时的最后一道关卡。

如果你有一个带有很多迭代或条件语句的测试,你可能会发现自己一次又一次地卡在同样的代码上。解决方案是要聪明地选择在哪里用 pdb 进行调试。将其嵌套在条件语句中,或者使用条件语句对迭代/递归进行调试(基本上是说当这个条件为True时,挂起以便我检查正在发生什么)。此外,pdb 还允许您查看所有运行时上下文(赋值、状态等)。

针对你的情况,看起来需要对 check_perm 进行创造性的调试。


是的,你的解决方案对你所解释的用例有效。在你的情况下,断言在方法内部。如果断言在方法调用之后,则堆栈跟踪不会显示错误值的来源。你需要回溯时间才能看到问题的根源。据我所知,在pdb中这是不可能的。 - guettli
抱歉,我刚刚读到这一行“我知道我可以使用交互式调试来找到匹配的行,但我很懒,想找到一个更容易找到check_perm()返回值的方法。”所以这是浪费时间。答案是什么?我不知道,你自己编写编辑器/测试运行器组合?提交请愿书,要求将物理定律改为更方便的东西?为什么要问这样愚蠢的限制性问题。 - nsfyn55
是的,你说得对。到目前为止,我的(和你的)解决方案(追踪)是我首选的答案。但是如果不问,我怎么知道呢?也许有更好的解决方案。据我所知,stackoverflow上的所有人都是志愿者。喜欢回答问题的人会这样做。不喜欢回答问题的人就不会。也许我眼瞎,但我看不出有什么问题。 - guettli
顺便聊一下,我真的点了赞这个问题,因为我学到了trace模块。但是一个问题怎么能在没有互动的情况下变得如此具体呢?我的意思是更新问题,因为浏览每个评论...你懂的! - Nizam Mohamed
@NizamMohamed:“但是没有交互,如何使问题变得具体?”这就是SO的“尽职调查”方面的定义。你应该自己至少做足够的工作来制定一个具体的问题。然后你作为“提问者”的任务就完成了;现在是时候坐下来等待答案了。礼仪上的违规出现在来回交流中。当一个问题被提出,一个合理的解决方案被提出,然后OP说“不,我的问题比那个更具体”,然后编辑问题以追踪他们真正想要的答案。这就像编辑历史一样。 - nsfyn55
显示剩余6条评论

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