加载中…
个人资料
  • 博客等级:
  • 博客积分:
  • 博客访问:
  • 关注人气:
  • 获赠金笔:0支
  • 赠出金笔:0支
  • 荣誉徽章:
正文 字体大小:

Python的热更新

(2017-01-27 17:01:08)
标签:

python

热更新

分类: Python

脚本语言和编程语言最大的不同,就是脚本语言是解释运行的,而非编译运行的,在运行期间,可以动态调整运行的内容,而这些是通过编译-链接来运行的编程语言是很难达到的,这篇博客就是来探索Python脚本语言进行热更新的。

一、热更新原理

现在很多软件的开发都是模块化的开发,OOP的开发也是基于一个个耦合性较小的类模块的开发,这样以来,在主程序运行的过程中,需要逐步的将当前使用到的模块导入。在编程语言例如C++中,这样的导入模块的操作,由#include的预处理指令来完成这样地方导入,但是在编译链接完成后,引入模块的过程就不可逆了,也就是很难在运行的时候,重新的载入模块。

而对于Python而言,导入模块的语句有两种,一种是import module,一种是from module import name。其中import module,是查找sys.modules中是否已经存在名字为module的对象,如果没有,那么将指定的module模块,放到系统的sys.modules的字典中,进行导入模块的操作;如果有,那么将module模块放到当前命名空间中,接着,此时就可以访问这个模块中的多个属性了(导入的模块也是一个对象,其中的函数和变量都是模块对象的属性);对于from module import name的操作,也是执行import语句,但并不把module放到名字空间中,并在module中查找name属性,放到命名空间中。

既然模块是放在sys.modules字典中的对象,那么对象更新后,更新字典,就可以获得新的模块的使用,也就是更新了对应的模块本身,那么在执行过程中对sys.modules字典进行更改就可以获得热更新的效果。

二、热更新的实现

Python中最常见的两种热更新的方式,如果是热更新A模块,有如下两种方式:

reload(A)

 

if A in sys.modules:

         del sys.modules[A]

         import A

这两种方式都能实现对于模块A的重新载入。但是重新载入的模块,对于之后使用该模块功能的方法是保证最新的,但是对于更新之前的使用该模块的,比如使用了该模块中的类的,那么依旧保持的是原来的模块内容。

例如模块A代码如下:

class A(object):

         def __init__(self):

                   self.value = 1

         def print_data(self):

                   print “get A value in instance is {} at version 1.0”.format(self.value)

如果更新代码中讲初始化的self.value = 1改成self.value = 2,同时修改print中的内容,讲version 1.0改成version 2.0,那么更新后,新建立的A类的对象,输出的内容是get A value in instance is 2 at version 2.0;而对于更新之前使用的A类的对象,输出内容不发生变化。

因而,对于A 类的更新前的对象,需要如何进行更新呢?首先,得确保你知道这个对象的存在,比如这个对象命名为a_instance,那么需要将a_instance__class__属性指向新的类定义,即在reload之后,对于已知的需要更新的类对象如a_instance,调用如下代码,其中new_class就是新类定义:

a_instance.__class__ = new_class

三、在线热更新的方案

知道python的热更新的原理以及实现了,那么对于传统的CS架构下的应用热更新就只有下列两种情况了:第一种是对于服务端的热更新,第二种是对于客户端的热更新。对于服务器端的热更新,一般需要在命令行中给出指令,接受到指令的情况下,对于模块做重新的加载;对于第二种情况,一般也是指令,只不过这是接收到服务器的指令,然后将新的文件下载到临时目录,进行替换后重新载入。综合上面的两种的要求,那么对于在线热更新,需要完成的功能是,规定更新指令,接收到指令后,拷贝覆盖文件,更新完文件后,进行热更新模块的操作,最后可以验证结果。基于上述的要求,我采用了PyQt4.0进行界面的开发,来做了一个更新的演示,代码如下,其中更新的指令为up,更新指令后要接需要更新的模块名称,同时当前目录下有以“re_更新模块名称”来命名的文件:
# -*- coding: utf-8 -*-

from PyQt4 import QtCore, QtGui


try:
    _fromUtf8 = QtCore.QString.fromUtf8
except AttributeError:
    _fromUtf8 = lambda s: s


# 界面逻辑
class Ui_Dialog(object):

    def setupUi(self, Dialog):
        Dialog.setObjectName(_fromUtf8("Dialog"))
        Dialog.resize(625, 300)
        self.plainTextEdit_2 = QtGui.QPlainTextEdit(Dialog)
        self.plainTextEdit_2.setGeometry(QtCore.QRect(0, 0, 631, 221))
        self.plainTextEdit_2.setReadOnly(True)
        self.plainTextEdit_2.setObjectName(_fromUtf8("plainTextEdit_2"))
        self.lineEdit = QtGui.QLineEdit(Dialog)
        self.lineEdit.setGeometry(QtCore.QRect(0, 220, 631, 81))
        self.lineEdit.setObjectName(_fromUtf8("lineEdit"))

        self.retranslateUi(Dialog)
        QtCore.QObject.connect(self.lineEdit, QtCore.SIGNAL(_fromUtf8("returnPressed()")), self.pressTest)
        QtCore.QMetaObject.connectSlotsByName(Dialog)

        # 定时器每次5秒调用一次loop
        self.timer = QtCore.QTimer()
        QtCore.QObject.connect(self.timer, QtCore.SIGNAL("timeout()"), self.loop)
        self.timer.start(5000)

    def retranslateUi(self, Dialog):
        Dialog.setWindowTitle(QtGui.QApplication.translate("Dialog", "Dialog", None, QtGui.QApplication.UnicodeUTF8))

    def pressTest(self):
        self.pressHandle(self.lineEdit.text())
        self.lineEdit.clear()

    def printData(self, textString):
        self.plainTextEdit_2.appendPlainText(textString)

    def loop(self):
        pass


class UpdateTest(Ui_Dialog):
    def __init__(self, commadDict, testInstance, className):
        super(UpdateTest, self).__init__()
        self.commadDict = commadDict
        self.testInstance = testInstance

    def pressHandle(self, commad):
        args = None
        commad = str(commad).split(" ")
        if len(commad) > 1:
            args = commad[1:]
            commad = commad[0]
        else:
            self.printData("commad " + commad[0] + " has no args with it!\n")
            return
        if commad in self.commadDict:
            self.printData("before the reload...")
            self.commadDict[commad](self.testInstance, args)
            self.printData("after reload finished\n")
        else:
            self.printData("commad " + commad + " is not handled\n")

    def loop(self):
        self.printData("A print information " + self.testInstance.print_data())
        # 创建新变量
        d = {}
        className = getattr(test, "A", None)
        d["b"] = className()

        self.printData("B print information " + d["b"].print_data())
        del d["b"]

        # 分隔符
        self.printData("\n")


def reload_method(relaod_instance, args):
    import os
    # args 为模块名
    args = args[0]
    module = str(args)
    new_file = "re_" + module + ".py"
    old_file = module + ".py"

    # 存在覆盖文件,那么拷贝覆盖文件
    if os.path.exists(new_file):
        copy = open(new_file, 'r')
        file = open(old_file, 'w')
        file.write(copy.read())
        file.close()
        copy.close()
        module = __import__(module)
        # 更新信息
        reload(module)
        new = getattr(test, relaod_instance.__class__.__name__, None)
        if new is not None:
            relaod_instance.__class__ = new
    else:
        print "can't find file"


def reload_commad(instance, args):
    reload_method(instance, args)

if __name__ == "__main__":
    import sys
    import test
    # 创建需要热更新对象
    className = getattr(test, "A", None)
    a_instance = className()
    # 界面启动
    app = QtGui.QApplication(sys.argv)
    Form = QtGui.QWidget()
    handleDict = {}
    handleDict["up"] = reload_commad
    ui = UpdateTest(handleDict, a_instance, className)
    ui.setupUi(Form)
    Form.show()

    sys.exit(app.exec_())

    最后,启动运行更新指令up test,执行结果如下

上述代码只是支持更新py文件的热更新,但实际上reload,还有import语句支持的导入的模块为py,pyc,pyo等文件格式,这样的话给客户端的更新提供了保障,需要加密等过程都可以在文件上做文章,然后到本地解密完成后导入即可


0

阅读 收藏 喜欢 打印举报/Report
  

新浪BLOG意见反馈留言板 欢迎批评指正

新浪简介 | About Sina | 广告服务 | 联系我们 | 招聘信息 | 网站律师 | SINA English | 产品答疑

新浪公司 版权所有