Python的动态重新封装的教程
让我们描绘一下本文的情节:假设您要在本地机器上运行一个进程,而部分程序逻辑却在另一处。让我们特别假设这个程序逻辑会不时更新,而您运行进程时,希望使用最新的程序逻辑。有许多方法可以满足刚提到的要求;本文将向您说明其中几种方法。
随着“可爱的Python”专栏不断进行,已经讨论了我的公共域实用程序Txt2Html的正在进行的增强。该实用程序将“智能ASCII”文本文件转换成HTML。以前的文章讨论了实用程序的Web代理版本和实用程序的curses界面。同样,我偶尔注意到可以用更有效的方法转换某些ASCII标记,或者解决了一个在处理某个特殊标记结构中的错误。
事实上,本专栏的文章都是用ASCII编写的,然后在编辑过程中转换成您可以阅读的HTML格式。在发表文章草稿之前,我运行了类似以下处理的程序:
文章的命令行HTML化
txt2htmlcharming_python_7.txt>charming_python_7.html
如果愿意,我可以指定一些标志来修改操作;但不管怎样,事实上转换器的最新版本在我的本地驱动器和路径中。如果在另一台机器上工作,或者对于要使用该实用程序的读者,则过程比较麻烦:请访问我的网站,注意比较版本号和文件日期(有时更改太小,我不会更改版本号),下载当前版本、将当前版本复制到正确目录,然后运行命令行转换器。(请参阅本文后面的参考资料。)
以上的过程包括几个需要手工操作且比较费时的步骤。应该更简单,而且可以做到这点。
命令行Web访问
大多数人认为Web是在GUI环境中交互式浏览页面的一种方法。那样做当然很好,但命令行中也有许多功能。带文本模式Web浏览器lynx的系统完全可以将整个Web看作是命令行工具使用的另一个文件集。例如,我发现有些命令很有用:
使用lynx进行命令行Web浏览
lynx-dumphttp://gnosis.cx/publish/. lynx-dumphttp://ibm.com/developerworks/.>ibm_developer.txt lynx-dumphttp://gnosis.cx/publish|wc|sed"s/(*[0-9]**\)\([0-9]*\)\(.*\)/\2/g"
第一行说:“将DavidMertz的主页(以ASCII文本)显示到控制台。”第二行说:“将IBM的当前developerWorks主页的ASCII版本保存到文件。”第三行示例说:“显示David主页的字数。”(不必担心细节,它只显示与管道结合的命令行工具。)
关于lynx,有一点要注意它(使用-dump选项时)执行几乎与Txt2Html完全相反的操作:前一种工具将HTML转换成文本;而后一种工具则转换成其它格式。但没有理由不使用与lynx一样流行的Txt2Html。可以使用一个很短的Python脚本完成这个操作:
'fetch_txt2html.py'命令行转换器
importsys fromurllibimporturlopen,urlencode iflen(sys.argv)==2: cgi='http://gnosis.cx/cgi/txt2html.cgi' opts=urlencode({'source':sys.argv[1],'proxy':'NONE'}) printurlopen(cgi,opts).read() else: print"PleasespecifyURLforTxt2Htmlconversion"
要运行这个脚本,只要执行如下操作:
pythonfetch_txt2html.pyhttp://gnosis.cx/publish/programming/charming_python_7.txt
这并没有向您提供本地Txt2Html处理的全部开关,但如有必要,添加它们也很容易。可以像使用任何命令行工具一样来输送和重定向输出。但是,在上述版本中,只能处理URL可以到达的数据文件,而不能处理本地文件。
实际上,fetch_txt2html.py可以完成lynx不能完成的任务(Txt2Html本身也不能):它不仅从URL取得数据源,而且还远程获取程序逻辑。如果使用fetch_txt2html.py,就不必在本地机器上安装Txt2Html;将(使用最新版本)远程调用处理,并且将把结果发送回来,就像运行的是本地进程。很棒吧?Txt2Html的本地版本可以访问远程URL,就像访问本地文件一样,但它还不能保证它自身是最新的……!
动态初始化
使用fetch_txt2html.py确保了在转换中始终使用最新的程序逻辑。但是,这个方法可以完成的另一件事情是将处理器(和内存)的需求转移给gnosis.cxWeb服务器。此特殊进程的负载并不是特别高,但人们却很可能认为在客户机上处理的其它类型的进程会更有效且令人满意。
组织Txt2Html的方式--也就是组织大多数程序的方式--是用一些由各种实用函数提供的核心流量控制函数。尤其是这些实用函数是一些经常更新的函数;核心函数(main()和一些其它函数)只有在做重大改写时才会变动。总而言之,在每个程序运行时有效更新的就是实用函数。其实,大部分情况下,主Txt2Html模块dmTxt2Html中的大多数函数就够了。
'd2h_textfuncs.py'动态Txt2Html更新
"""Hot-pluggablereplacementfunctionsforTxt2Html""" #--Functionstomassageblocksbytype #def Titleify(block): #defAuthorify(block): #...[moreblockmassagingfunctions]... #--Utilityfunctionsfortexttransformation #defAdjustCaps(txt): #defcapwords(txt): #defURLify(txt): defTypographify (txt): #[module]names r=re.compile(r ""'([\(\s'/">]|^)\[(.*?)\]([<\s\.\),:;'"?!/-])""",re.M|re.S) txt=r.sub( '\\1<em><code>\\2</code></em>\\3',txt) #*stronglyemphasize*words r=re.compile(r ""'([\(\s'/"]|^)\*(.*?)\*([\s\.\),:;'"?!/-])""",re.M|re.S) txt=r.sub( '\\1<strong>\\2</strong>\\3',txt) #...[moretextmassaging]... return txt #...[moretexttransformationfunctions].....
要使用最新和最具体的支持模块,需要一些准备步骤。首先,将主Txt2Html模块下载到本地系统(这是一次性步骤)。其次,在本地系统上创建类似于以下示例的Python脚本:
'dyn_txt2html.py'命令行转换器
from dmTxt2Html import * #Importthebodyof'Txt2Html'code from urllib import urlopen import sys #Checkforupdatedfunctions(failgracefullyifnotfetchable) try : updates=urlopen( 'http://gnosis.cx/download/t2h_textfuncs.py').read() fh=open( 't2h_textfuncs.py', 'w') fh.write(updates) fh.close() except : sys.stderr.write( 'CannotcurrentlydownloadTxt2Htmlupdates') #Importtheupdatedfunctions(ifavailable) try : from t2h_textfuncs import * except : sys.stderr.write( 'CannotimporttheupdatedTxt2Htmlfunctions') #Setoptionsbasedonrunmode(shellvs.CGI) if len(sys.argv)>=2: cfg_dict=ParseArgs(sys.argv[1:]) main(cfg_dict) else : print"PleasespecifyURL(andoptions)forTxt2Htmlconversion"
在dyn_txt2html.py脚本中,请注意当执行fromt2h_textfuncsimport*语句时,所有以前在dmTxt2Html中定义的函数(如Typographify())都将由t2h_textfuncs版本的同名函数替换。当然,如果t2h_textfuncs的函数被注释掉了,则不会被替换。
有件小问题得注意,不同的系统以不同的方式处理写入STDERR。在类UNIX系统中,运行脚本时可以重定向STDERR;但是在当前OS/2外壳和Windows/DOS中,STDERR消息将附加到控制台输出。您也许要将以上的错误/警告写到日志文件中,或者只习惯于将STDOUT定向到文件(可能会更有用)。例如:
'dyn_txt2html'的命令行会话
G:\txt2html>pythondyn_txt2html.pytest.txt>test.html CannotcurrentlydownloadTxt2Htmlupdates
错误转至控制台;经转换的输出转至文件。
一件更有趣的事情是dyn_txt2html.py为什么不下载整个dmTxt2Html模块,而仅下载支持模块。当然这是有理由的。t2h_textfuncs支持模块远远小于主dmTxt2Html模块,特别是因为大多数函数已经过删节/被注释掉。在调制解调器连接上,它的速度明显快很多。但下载大小并不是主要原因。
对于Txt2Html,如果用户自动下载整个最新模块也没关系。但程序逻辑是分布式的系统(特别是维护责任也是分布式的)会发生什么情况呢?您也许会让Alice、Bob和Charlie分别负责模块Funcs_A、Funcs_B和Funcs_C。他们每个人都对他们负责的函数进行定期(且独立)更改,并将最新和最好的版本上传到他们自己的网站(如http://alice.com/Funcs_A.py)。在这种情况下,让三个程序员都更改同一个主模块不太可行。但可以直接扩展类似于dyn_txt2html.py的脚本以在启动时尝试导入Funcs_A、Funcs_B和Funcs_C(如果不能获取这些资源,则会退到MainProg版本)。
长期运行的动态进程
迄今为止,我们研究的工具已经通过在初始化时下载更新资源而获得了动态程序逻辑。这对于命令行处理或批处理很有意义,但对于长期运行的应用程序又会怎样。这种长期运行的应用程序最可能是一些不断响应客户机请求的服务器进程。但是在这个案例中,我们将使用为以前的文章开发的curses_txt2html.py来说明Python的reload()函数。程序curses_txt2html是dmTxt2Html本地副本的封装器。这里并不是第二次提到curses编程,谈一下curses_txt2html提供了一组交互式菜单以配置和运行多个连续的Txt2Html转换也足够了。
curses_txt2html可以一直在后台运行,当切换到它的会话并运行转换时,我们希望它能够使用最新的程序逻辑。对于这个特定的简单示例,关闭和重新启动应用程序并不难,并不会带来特别的损害。但这很容易令人联想到其它一直运行着的进程(可能是说明会话中所执行操作状态的进程)。
在本文中,添加了新的File/Update子菜单。它被激活时只调用新的函数update_txt2html()。除了与提供发生的确认相关的curses调用之外,我们已经在本文的其它示例中看到过这些步骤:
'curses_txt2html.py'动态更新函数
defupdate_txt2html (): #Checkforupdatedfunctions(failgracefullyifnotfetchable) s=curses.newwin(6,60,4,5) s.box() s.addstr(1,2, "*PRESSANYKEYTOCONTINUE*",curses.A_BOLD) s.addstr(3,2, "...downloading...") s.refresh() try : from urllib import urlopen updates=urlopen( 'http://gnosis.cx/download/dmTxt2Html.py').read() fh=open( 'dmTxt2Html.py', 'w') fh.write(updates) fh.close() s.addstr(3,2, "Module[dmTxt2Html]downloadedtocurrentdirectory") except : s.addstr(3,2, "Downloadofupdated[dmTxt2Html]modulefailed!") reload(dmTxt2Html) s.addstr(4,2, "Module[dmTxt2Html]reloadedfromcurrentdirectory") s.refresh() c=s.getch() s.erase()
dyn_txthtml.py和update_txt2html()函数之间有两个重要差异。其中一个差异是继续操作,并导入主dmTxt2Html模块而不只导入支持函数。这主要是简化了导入。这里的问题是我们使用importdmTxt2Html来访问模块,而不是fromdmTxt2Htmlimport*。从许多方面考虑,这是一个更安全的过程,但结果是使覆盖dmTxt2Html中的函数变得更困难(不论是无心地还是故意地)。如果我们要从d2h_textfuncs附加函数,则必须对导入的支持模块执行dir(),并将成员以属性形式附加到"dmTxt2Html"名称空间。执行这种样式的覆盖是留给读者的练习。
update_txt2html()函数带来的最主要差异是Python的内置reload()函数的用法。只执行全新的importdmTxt2Html将不会覆盖以前导入的函数。请密切注意这一点!许多初学者认为重新导入模块将更新内存中的版本。这是错的。实际上,更新模块中函数的内存映像的方法是reload()模块。
以上示例中还执行了另一个小技巧。更新dmTxt2Html模块的下载位置是本地工作目录,而这个目录可能是(也可能不是)原来装入dmTxt2Html的目录。事实上,如果它在Python库目录中,那么您也许不在该目录中使用(也许对它没有用户许可权)。但reload()调用尝试先从当前目录装入,然后再尝试Python路径的其余部分。所以,不论下载是否成功,reload()应该是一个安全的操作(虽然它可能装入新的模块,也可能不装入)。