sympy是如何工作的?它如何与交互式Python shell互动?交互式Python shell又是如何工作的?

18 投票
4 回答
2983 浏览
提问于 2025-04-16 00:54

当我按下 Enter 键时,内部发生了什么?

我之所以问这个问题,除了出于好奇,还有一个原因是想弄清楚当你

from sympy import *

并输入一个表达式时,背后到底发生了什么。它是如何从按下 Enter 键转变为调用

__sympifyit_wrapper(a,b)

在 sympy.core.decorators 中的内容的?(这是我在尝试检查一个计算时,winpdb 带我去的第一个地方。)我猜想可能有一个内置的 eval 函数会被正常调用,而在你导入 sympy 时,这个函数会被覆盖?

4 个回答

5

我刚刚查看了sympy的代码(在http://github.com/sympy/sympy上),发现__sympifyit_wrapper是一个装饰器。它会被调用的原因是因为某处有段代码看起来像这样:

class Foo(object):
    @_sympifyit
    def func(self):
        pass

__sympifyit_wrapper是由@_sympifyit返回的一个包装器。如果你继续调试,可能会找到这个函数(在我的例子中叫func)。

我了解到在sympy/__init__.py中导入的许多模块和包里,有些内置的代码被sympy的版本替代了。这些sympy版本可能使用了那个装饰器。

使用exec时,像>>>这样的操作不会被替换,但操作的对象会被替换。

6

这段内容其实和secondbanana的真正问题关系不大,只是想回应一下Omnifarious的悬赏而已;)

解释器本身其实很简单。事实上,你自己也可以写一个简单的(虽然远不完美,也不处理异常等等)解释器:

print "Wayne's Python Prompt"

def getline(prompt):
    return raw_input(prompt).rstrip()

myinput = ''

while myinput.lower() not in ('exit()', 'q', 'quit'):
    myinput = getline('>>> ')
    if myinput:
        while myinput[-1] in (':', '\\', ','):
            myinput += '\n' + getline('... ')
        exec(myinput)

在普通的命令行中,你可以做大部分你习惯的事情:

Waynes Python Prompt
>>> print 'hi'
hi
>>> def foo():
...     print 3
>>> foo()
3
>>> from dis import dis
>>> dis(foo)
  2           0 LOAD_CONST               1 (3)
              3 PRINT_ITEM
              4 PRINT_NEWLINE
              5 LOAD_CONST               0 (None)
              8 RETURN_VALUE
>>> quit
Hit any key to close this window...

真正的“魔法”发生在词法分析器和解析器中。

词法分析,或者说lexing,就是把输入的内容分解成一个个小的“标记”。这些标记是关键字或者是“不可分割”的元素。例如,=iftry:forpassimport都是Python的标记。想要看看Python是如何对程序进行标记的,可以使用tokenize模块。

把一些代码放到一个名为'test.py'的文件里,然后在这个目录下运行以下代码:

from tokenize import tokenize
f = open('test.py')
tokenize(f.readline)

对于print "Hello World!"这行代码,你会得到以下结果:

1,0-1,5: NAME 'print'
1,6-1,19: STRING '"hello world"'
1,19-1,20: NEWLINE '\n'
2,0-2,0: ENDMARKER ''

一旦代码被标记后,就会被解析成一个抽象语法树。最终结果是你程序的Python字节码表示。对于print "Hello World!",你可以看到这个过程的结果:

from dis import dis
def heyworld():
    print "Hello World!"
dis(heyworld)

当然,所有编程语言都会进行词法分析、解析、编译,然后执行它们的程序。Python会进行词法分析、解析,并编译成字节码。然后,这个字节码会被“编译”(翻译可能更准确)成机器代码,最后执行。这就是解释型语言和编译型语言之间的主要区别——编译型语言是直接从原始源代码编译成机器代码,这意味着你只需要在编译前进行词法分析和解析,然后就可以直接执行程序。这意味着执行速度更快(没有词法分析和解析的阶段),但也意味着为了达到初始的执行速度,你需要花费更多的时间,因为整个程序必须被编译。

13

好吧,经过一番尝试,我觉得我明白了。当我最初提问时,我并不知道有运算符重载这个概念。

那么,在这个 Python 会话中发生了什么呢?

>>> from sympy import *
>>> x = Symbol(x)
>>> x + x
2*x

其实,解释器评估这个表达式的方式并没有什么特别的;重要的是 Python 会把

x + x

转换成

x.__add__(x)

而且 Symbol 类是从 Basic 类继承而来的,Basic 类定义了 __add__(self, other) 方法,返回 Add(self, other)。如果你想看看,这些类可以在 sympy.core.symbol、sympy.core.basic 和 sympy.core.add 中找到。

正如 Jerub 所说,Symbol.__add__() 有一个叫做 _sympifyit装饰器,它基本上是在评估函数之前把函数的第二个参数转换成一个 sympy 表达式,过程中返回一个叫 __sympifyit_wrapper 的函数,这就是我之前看到的。

用对象来定义操作是个很酷的概念;通过定义自己的运算符和字符串表示,你可以很轻松地实现一个简单的符号代数系统:

symbolic.py --

class Symbol(object):
    def __init__(self, name):
        self.name = name
    def __add__(self, other):
        return Add(self, other)
    def __repr__(self):
        return self.name

class Add(object):
    def __init__(self, left, right):
        self.left = left
        self.right = right
    def __repr__(self):
        return self.left + '+' + self.right

现在我们可以做:

>>> from symbolic import *
>>> x = Symbol('x')
>>> x+x
x+x

稍微重构一下,就可以很容易地扩展到处理所有基本算术运算

class Basic(object):
    def __add__(self, other):
        return Add(self, other)
    def __radd__(self, other): # if other hasn't implemented __add__() for Symbols
        return Add(other, self)
    def __mul__(self, other):
        return Mul(self, other)
    def __rmul__(self, other):
        return Mul(other, self)
    # ...

class Symbol(Basic):
    def __init__(self, name):
        self.name = name
    def __repr__(self):
        return self.name

class Operator(Basic):
    def __init__(self, symbol, left, right):
        self.symbol = symbol
        self.left = left
        self.right = right
    def __repr__(self):
        return '{0}{1}{2}'.format(self.left, self.symbol, self.right)

class Add(Operator):
    def __init__(self, left, right):
        self.left = left
        self.right = right
        Operator.__init__(self, '+', left, right)

class Mul(Operator):
    def __init__(self, left, right):
        self.left = left
        self.right = right
        Operator.__init__(self, '*', left, right)

# ...

再稍微调整一下,我们就能得到和一开始的 sympy 会话相同的行为……我们会修改 Add,让它在参数相等时返回一个 Mul 实例。这有点棘手,因为我们必须在实例创建之前处理这个问题;我们需要使用__new__() 而不是 __init__()

class Add(Operator):
    def __new__(cls, left, right):
        if left == right:
            return Mul(2, left)
        return Operator.__new__(cls)
    ...

别忘了为 Symbols 实现等于运算符:

class Symbol(Basic):
    ...
    def __eq__(self, other):
        if type(self) == type(other):
            return repr(self) == repr(other)
        else:
            return False
    ...

好了。总之,你可以想出各种其他的实现,比如运算符优先级、带替换的评估、高级简化、微分等等,但我觉得基础部分这么简单真是太酷了。

撰写回答