之前一直用的都是python2.4,这是一个很老的版本了,连“with”这样强大的语法都没有,早就考虑到升级到python2.7,现在项目基本做完了,自然也开始捣鼓了起来。
升级过程中不巧就遇到了py2exe和wx.lib.pubsub的问题,花了些时间把它解决后,考虑到这个问题或许有些代表性,于是在这里记录了下来。
简单说一下,wx.lib.pubsub是wxpython用以实现observer模式的利器,在python2.7版本对应的wx中,它提供了一个选择加载为了兼容早期pubsub api的老版本模块和新api版本模块的机制,于是,和其他一些比较‘正常’的包有一些不同,而py2exe采取的默认策略没法cover它,于是产生了不一致的错误。
问题描述
如下的一个程序:
from wx.lib.pubsub import setuparg1
from wx.lib.pubsub import pub
#from wx.lib.pubsub import Publisher as pub # 这是python2.4的wx.lib.pubsub引用的写法,为什么改用上面的语句不在这里讨论
pub = pub.Publisher()
print pub
直接在python2.7的环境下执行ok
如果用py2exe打包的话,可以产生最终的可执行文件,但执行时会出错,这里的setup.py是一个最简单的安装脚本:
from distutils.core import setup
import py2exe
setup(console=['test.py'])
执行时报错:
注意在前面打包时,py2exe就已经报告警告了:
py2exe的原理
很明显,我们如果在python的环境下执行诸如from package import module,import module这样的语句的时候,如果这个module或者package来自于第三方,python会到文件系统里去找对应的包,一般来说,第三方包提供有setup.py来安装的话,那么,这些module都在install_of_python/site_packages/下面。
当程序执行from...import...或import ...的时候,如果是package,则以package.__init__.py会被执行,如果是module的话,对应的module.py文件会被执行,它们定义的对象(函数、变量等)会被引入当前的名字空间以外,他们调用的子函数也会被执行,这个过程是递归的,直到所有import指令都处理完。
在python环境下这些module都在文件系统里,那么通过py2exe打包的话这些module该在哪里找呢?我们可以把setup.py的bundle选项设成3,即打开bundle,来看py2exe到底产生了些什么东西:
from distutils.core import setup
import py2exe
setup(console=['test.py'],
options={'py2exe': {'bundle_files': 3}}
)
来到dist目录,可以发现有一个library.zip文件。答案就在这里,在生成的test.exe文件中,所有import和from指令都被redirect到这个library.zip文件中去寻找它们所reference的module和package(这里只讨论第三方的包和模块,像os这种内置的不一样),比如,我们在里面发现了wx.lib.pubsub包目录:
对比一下library.zip下面的wx.lib.pubsub包和文件系统下面的wx.lib.pubsub包会发现不同:
上图左边是文件系统下的,右图是library.zip下的,可以发现library.zip下的module,package比文件系统下的少
在上面错误提示中抛出的listenerimpl.py也可以在文件系统下找到,而在library.zip中就没有
那这是怎么造成的呢?
我自己总结了一个规律,没有理论依据支撑,如果知道的朋友能告诉我最好了!
在默认option下,py2exe会依次扫描程序的.py文件(注意:不是动态执行),会检查其中的import指令,然后递归将被Import的模块,包括包下面的__init__.py。比如,有下面的包
我们写一个简单的测试客户程序,只有一条语句:
from test_package import test_import1
再来看__init__.py:
def my_test(dumb_flag):
if dumb_flag:
import test_import3
注意这里只定义了一个函数,然后在一个条件判断语句下写了import语句
当我们执行py2exe产生dist后,看看library.zip下包含了些什么
简单说明一下我的观点,__init__.pyc被包含是因为客户程序的from test_package import test_import1语句被py2exe parse的结果,test_import1.pyc同上。test_import2.pyc呢?没有出现,因为不管在客户测试程序中还是__init__.py中,都没有显式的import test_import2,所以,它没有被包含到library.zip中;那么test_import3.pyc又是什么原因呢?因为当py2exe parse __Init__.py时,发现import test_import3的语句,所以就包含了它,但显然这里只是定义了一个函数而已,根本就没执行它,更不会在runtime时真正去import它了,而py2exe是不会管的,它只是静态的parse而已。
这就是我总结的py2exe的默认选项下的策略,好在我们有options可以控制这个behavior,packages就是用来干这个事的,packages参数可以让py2exe在打包时把整个package都打包进去,而不是仅仅parse它认为需要import的modules。如在这个例子中,我们在例子的setup.py中如下修改:
from distutils.core import setup
import py2exe
setup(console=['test2.py'],
options={'py2exe': {'bundle_files': 3,
'packages': ['test_package'],
}
}
)
这时可以检查,整个test_package目录都被包含进去了,和文件系统的行为一致了。
回到刚开始pubsub的问题:
pubsub包因为提供两种互斥的api,通过用户选择来加载。所以,它的__init__.py和模块中有运行期代码,做一些诸如根据用户输入来动态设置sub package的名字,而非显式地通过指定名字的方法来from...import...,熟悉linux的朋友,打个比方就好比一个程序链接动态库时不是通过build时的显示链接(-l选项)而是通过运行期dlopen的方式来链接。自然,py2exe在模块源文件中找不到它们的名字,继而不会把它们放到library.zip中
只要在刚开始那个setup.py加入option:
'packages': ['wx.lib.pubsub']
即可解决。