Snowdream

I'm awake but my world is half asleep
posts - 403, comments - 310, trackbacks - 0, articles - 7
  BlogJava :: 首页 :: 新随笔 :: 联系 :: 聚合  :: 管理

[zz]True closure in Python

Posted on 2008-02-11 16:18 ZelluX 阅读(689) 评论(0)  编辑  收藏 所属分类: Scripting
发现自己对Python的语法的兴趣远比对使用Python本身的兴趣浓厚得多
为什么水木上的帖子每行末尾都是用空格填充的,每次转载还要先放到vim里面处理一下。。。

by ilovecpp

让Python支持true closure有多难?
只需修改11行代码。

如果你不知道什么是true closure,这里简单解释一下。Python支持lexicalscope:

>>> def add_n(n):
...     def f(m):
...             return n+m
...     return f
>>> add_2 = add_n(2)
>>> add_2(0)
2
>>> add_2(2)
4

f引用了外层函数add_n的局部变量n。有趣的是,f引用n的时候,add_n已经结束,n似乎不存在了。f所以能正常工作,是因为创建它的时候就把n作为f的上下文(closure)保存了下来,并不随add_n结束而消失。
但是,Python的lexical scope和Scheme/Smalltalk/Ruby还有一点区别:不能在内层函数中rebind外层函数的局部变量。
>>> def f():
...     def g():
...             n=1
...     n=0
...     g()
...     return n
...
>>> f()
0

这是因为Python没有变量声明, n=1 自动使n成为g的局部变量,也就无法rebind f中的n了。可以说Python的closure是只读的。如果你听到有人说"Python不支持true closure",就是指这个。其实,Python VM能够支持true closure。因为,Python支持内层函数看见外层函数的name rebinding:

>>> def f():
...     def g():
...             yield n
...             yield n
...     x = g()
...     n = 0
...     print x.next()
...     n = 1
...     print x.next()
...
>>> f()
0
1

对于Python的closure实现(flat closure),"外层函数rebind name"和"内层函数rebind name"其实没有区别。我们知道用global关键字可以rebind module scopename。如果增加一个类似的outer关键字,就可以支持rebind outer scope name。真正的限制是Guido不愿意为支持true closure增加关键字。

也可以不增加关键字,而是把global n的语义改为"如果outer scope定义了n,rebind outer scope n;否则rebind module scope n"。简单起见,我没有修改Python的built-in compiler,而是修改了compiler module(用Python实现的Python compiler)。你只需把下面这个patch打到compiler/symbols.py(Python 2.5.1)就可以体验true closure了:

C:\Python\Lib>diff -u compiler/symbols.py.orig compiler/symbols.py
--- compiler/symbols.py.orig    Thu Aug 17 10:28:56 2006
+++ compiler/symbols.py Mon Feb 11 12:03:01 2008
@@ -21,6 +21,7 @@
         self.params = {}
         self.frees = {}
         self.cells = {}
+        self.outers = {}
         self.children = []
         # nested is true if the class could contain free variables,
         # i.e. if it is nested within another function.
@@ -54,8 +55,10 @@
         if self.params.has_key(name):
             raise SyntaxError, "%s in %s is global and parameter" % \
                   (name, self.name)
-        self.globals[name] = 1
-        self.module.add_def(name)
+        if self.nested:
+            self.outers[name] = 1
+        else:
+            self.globals[name] = 1

     def add_param(self, name):
         name = self.mangle(name)
@@ -90,6 +93,8 @@
         """
         if self.globals.has_key(name):
             return SC_GLOBAL
+        if self.outers.has_key(name):
+            return SC_FREE
         if self.cells.has_key(name):
             return SC_CELL
         if self.defs.has_key(name):
@@ -107,6 +112,7 @@
             return ()
         free = {}
         free.update(self.frees)
+        free.update(self.outers)
         for name in self.uses.keys():
             if not (self.defs.has_key(name) or
                     self.globals.has_key(name)):
@@ -134,6 +140,9 @@
         free.
         """
         self.globals[name] = 1
+        if self.outers.has_key(name):
+            self.module.add_def(name)
+            del self.outers[name]
         if self.frees.has_key(name):
             del self.frees[name]
         for child in self.children:

因为我们没有修改built-in compiler,所以程序要写在字符串里,用compiler.compile编译,用exec执行:
>>> from compiler import compile
>>> s = '''
... def counter():
...     n = 0
...     def inc():
...             global n
...             n += 1
...     def dec():
...             global n
...             n -= 1
...     def get():
...             return n
...     return inc, dec, get
... '''
>>> exec compile(s, '', 'exec')
>>> inc, dec, get = counter()
>>> get()
0
>>> inc()
>>> get()
1
>>> dec()
>>> get()
0

后记

1 搞这个东西的缘起是Selfless Python(http://www.voidspace.org.uk/python/weblog/arch_d7_2006_12_16.shtml#e583)。很有趣的bytecode hack,给一个类中的所有函数补上self参数。既然PythonVM支持true closure,能不能用类似的手法让Python支持true closure呢?不过很快就明白这个在bytecode层面不好弄,还是得修改编译器。不过改起来还真是出乎意料地简单。

2 Guido早已明确表示不能改变global的语义(因为会影响现有代码),所以这个只是玩玩而已,不用指望成为现实。当然你可以只发布bytecode,大概还能把反编译器搞挂掉。:-)
3 我可以理解Guido的决定。除非你之前一直在用Scheme,否则我觉得像上面counter例子那种一组共享状态的函数还是写成class为好,至少共享状态是什么一目了然。Lexical scope太implicit,用在开头add_n那种地方挺方便,再复杂就不好了。

又:很抱歉"幕后的故事"拖了这么久。写起来才发现自己还是不懂descriptor。
不过我肯定不会让它烂尾的。

 


只有注册用户登录后才能发表评论。


网站导航: