工作需要,要实现几十种事件的参数编辑界面,事件的数据结构如下:

// TsatEvent
struct TSATDATASHARED_EXPORT TsatContingencyEvent
{
    virtual ~TsatContingencyEvent() {}

    virtual TsatContingencyEvent* clone() const = 0;

    virtual QString desc() const = 0;
};

// 母线三相短路
struct TSATDATASHARED_EXPORT TsatEventThreePhaseFaultAtBus : public TsatContingencyEvent
{
    QString bus;

    virtual TsatContingencyEvent* clone() const;

    virtual QString desc() const;
};

// 线路三相短路
struct TSATDATASHARED_EXPORT TsatEventThreePhaseFaultOnLine : public TsatContingencyEvent
{
    TsatEventThreePhaseFaultOnLine()
        :distance(0)
    {
    }

    QString fromBus;
    QString toBus;
    double distance;

    virtual TsatContingencyEvent* clone() const;

    virtual QString desc() const;
};

......
如何相关的编辑界面,那是另一个话题,光是这几十个类的成员函数和xml文件的读写就够写吐血的了。难度不是问题,问题是太烦了,作为一个有着多年偷懒经验的老程序员,自然很快想到为了代码自动生成。那就用可爱的python来搞吧,原因很简单,一是这玩意不需要编译;二是这玩意有文本处理的利器正则表达式。

友情提示,这里是基于Python3.4版本的,还在用Python2的同学就不要直接拷代码了。

作为一个职业程序员的基本素养,动手编码之前当前要先设计一下,主要功能如下:

  1. 文件的读写是最基本的,可以直接复用已有函数
  2. 从C++的头文件读取类的信息,包括类的名称和基类的名称,所有的类信息应该组织为列表,以保证顺序
  3. 从C++的头文件读取类中成员变量的信息,包括成员变量的类型和名称,成员变量的信息应该组织在相应的类信息下面,这实际上就是几十个类元数据信息
  4. 根据类的元数据信息结合代码模板自动生成相应的代码

好了,可以开工了,打开PyCharm,建立相关的py文件。当然,如果你愿意用文本编辑工具,也随便你。


先把python的文件读写函数拿过来吧,这没啥好说的:

def loadArray(path, records, encoding):
    """加载文件到列表"""

    try:
        file = open(path, "r", encoding=encoding)     # open file in read mode
    except IOError as message:                        # file open failed
        print("read file error({0}:{1})".format(message, path))
        sys.exit(1)

    lines = file.readlines()
    for line in lines:
        records.append(line)

    file.close()

def saveArray(path, records, encoding):
    """保存文件从列表"""

    try:
        file = open(path, "w", encoding=encoding)     # open file in write mode
    except IOError as message:                        # file open failed
        print("write file error({0}:{1})".format(message, path))
        sys.exit(1)

    file.writelines(records)

    file.close()
这里没有把头文件中的文字作为一个整理进行处理,而是分解为一行行的处理,效率当然会低一些,但是后面的处理可以更自由一些

然后定义两个类,分别代表类的元数据和类成员函数的元数据,也非常简单:

class Var:
    def __init__(self, type, name):
        self.type = type
        self.name = name

class Class:
    def __init__(self, name, desc, base):
        self.name = name
        self.desc = desc
        self.base = base
        self.vars = []

    def appendVar(self, type, name):
        var = Var(type, name)
        self.vars.append(var)

下面就是重点了,定义cpp文件解析的类:

class ClassLib:
    def __init__(self):
        self.classes = []
        self.classMaps = {}

其中classes存储类的元数据列表,classMap是为了搞一个从类名快速检索类元数据的结构,只是为了检索方便

既然要分析C++的头文件文件,自然要用到正则表达式了,因为这几个正则表达式只需要定义一次,就在类的构造函数中定义吧,当然更好一点的做法是做成属于类的静态变量,而不是属于对象的成员变量,不过也无所谓了,这个ClassLib基本上和单件也没啥区别的

        reg = """
        ^ 			    #开头
        (struct|class) 	            #捕获类或者结构的关键字
        \s+ 			    #空格
        [\w_]+                      #捕获导出标记
        \s+ 			    #空格
        (\w+) 			    #类名
        (.*) 			    #或许有基类,注释什么的
        $ 			    #结束
        """
        self.classReg = re.compile(reg, re.VERBOSE)

        reg = """
        \s*:\s*                     #空格:空格
        (public|private|protected)  #捕获派生的关键字
        \s+                         #空格
        (\w+)                       #基类
        """
        self.baseClassReg = re.compile(reg, re.VERBOSE)

        reg = """
        ^ 			    #开头
        \s+ 			    #空格
        (\w+) 			    #变量类型
        \s+ 			    #空格
        (\w+); 			    #变量名称
        .*                          #或许有注释什么的
        $ 			    #结束
        """
        self.dataReg = re.compile(reg, re.VERBOSE)

        reg = """
        ^ 			    #开头
        //                          #//
        \s* 			    #空格
        (.+) 			    #注释
        $ 			    #结束
        """
        self.commentReg = re.compile(reg, re.VERBOSE)

这些正则表达式都没啥技术含量,基本上都是简单粗暴的直接翻译,注释也写得很清楚。唯一需要说明的是这里把捕获类名和捕获基类名分成了两个部分,因为

TsatContingencyEvent是没有基类的

下面就开始解析C++的头文件文件吧:

    def parse(self, lines):
        object = None
        comment = ""
        for line in lines:
            m = re.match(self.commentReg, line)
            if m:
                comment = m.group(1)
                continue

            m = re.match(self.classReg, line)
            if m:
                name = m.group(2)
                base = None
                if m.lastindex >= 3:
                    mm = re.match(self.baseClassReg, m.group(3))
                    if mm:
                        base = self.classMaps[mm.group(2)]
                object = Class(name, comment, base)
                self.classes.append(object)
                self.classMaps[name] = object
                continue

            m = re.match(self.dataReg, line)
            if m:
                type = m.group(1)
                name = m.group(2)
                object.appendVar(type, name)
代码很简单,注释都没必要写,就是发现是注释行则保存注释;发现是类定义行则捕获类名和基类名,然后生成类的元数据;发现时类的成员变量行则捕获成员变量的类型和名称,然后加入到类的元数据中

解析出来的类的元数据基本上就是一个简单的两层树形结构,再写一个print函数吧,方便测试和调试:

    def print(self):
        for object in self.classes:
            base = "None"
            if object.base != None:
                base = object.base.name
            print("{0} : {1} // {2}".format(object.name, base, object.desc))

            for var in object.vars:
                print("\t{0} {1};".format(var.type, var.name))
好了,可以看主函数了:

srcPath = 'src.cpp'

srcs = []
loadArray(srcPath, srcs, 'utf-8')

classLib = ClassLib()
classLib.parse(srcs)
classLib.print()
没啥可说的,打印出来的结果是:

TsatContingencyEvent : None // TsatEvent
TsatEventThreePhaseFaultAtBus : TsatContingencyEvent // 母线三相短路
	QString bus;
TsatEventThreePhaseFaultOnLine : TsatContingencyEvent // 线路三相短路
	QString fromBus;
	QString toBus;
	double distance;
......
好了,可以开始第4步了,根据类的元数据信息结合代码模板自动生成相应的代码:

def writeCpp(classLib, path):
    dsts = []

    cpp = """
    TsatContingencyEvent* $name$::clone() const
    {
        $name$ *ret = new $name$;
        *ret = *this;

        return ret;
    }

    QString $name$::desc() const
    {
        return QString();
    }
    """

    for object in classLib.classes:
        if object.base == None:
            continue

        base = object.base.name
        dsts.append(cpp.replace("$name$", object.name))

    saveArray(path, dsts, 'utf-8')
基本上就是一个关键字替换,当然具体的模板可以随便写,所以也不要纠结于为什么我这里的desc函数是直接返回QString()了,因为自动生成代码也不是万能的,不要为了全自动生成代码而增加一堆额外的工作,除非是一个系统的基础数据结构,后面需要不断的被自动化,那还可以考虑,至于这里的小工具嘛,就算了吧。

调用一下吧:

writeCpp(classLib, 'dst.cpp')

自动生成的代码是这个样子的:

    TsatContingencyEvent* TsatEventThreePhaseFaultAtBus::clone() const
    {
        TsatEventThreePhaseFaultAtBus *ret = new TsatEventThreePhaseFaultAtBus;
        *ret = *this;

        return ret;
    }

    QString TsatEventThreePhaseFaultAtBus::desc() const
    {
        return QString();
    }
    
    TsatContingencyEvent* TsatEventThreePhaseFaultOnLine::clone() const
    {
        TsatEventThreePhaseFaultOnLine *ret = new TsatEventThreePhaseFaultOnLine;
        *ret = *this;

        return ret;
    }

    QString TsatEventThreePhaseFaultOnLine::desc() const
    {
        return QString();
    }
...... 
当然了,有的同学看得比较仔细,会一针见血的指出这个例子只用到类名嘛,那基类和类的成员函数的元数据根本没用到嘛。好吧,那就再举一个例子:

def writeFiler(classLib, path):
    dsts = []

    cpp = """
template<typename Filer>
inline void filerElem(Filer& filer, $name$* data, QDomElement& elem)
{
    filerElem(filer, dynamic_cast<$base$*>(data), elem);

    $vars$
}
"""

    for object in classLib.classes:
        if object.base == None:
            continue

        vars = []
        for var in object.vars:
            type = var.type.title()
            if var.type == 'QString':
                type = 'String'
            vars.append('\tfiler.attribute{0}(elem, data->{1}, "{1}");'.format(type, var.name))

        filer = cpp
        filer = filer.replace('$name$', object.name)
        filer = filer.replace('$base$', object.base.name)
        filer = filer.replace('$vars$', "\n".join(vars)[1:])
        dsts.append(filer)

    saveArray(path, dsts, 'utf-8')
其实就是多了一层循环,多替换一些关键字而已,也没啥难度。

再调用一下吧:

writeFiler(classLib, 'filer.cpp')

这次自动生成的代码是这个样子的:

template<typename Filer>
inline void filerElem(Filer& filer, TsatEventRemoveLine* data, QDomElement& elem)
{
    filerElem(filer, dynamic_cast<TsatContingencyEvent*>(data), elem);

    filer.attributeString(elem, data->fromBus, "fromBus");
    filer.attributeString(elem, data->toBus, "toBus");
}

template<typename Filer>
inline void filerElem(Filer& filer, TsatEventDisconnectGenerator* data, QDomElement& elem)
{
    filerElem(filer, dynamic_cast<TsatContingencyEvent*>(data), elem);

    filer.attributeString(elem, data->bus, "bus");
}
......
好了,怎么用python自动生成代码差多不也就这样了,没啥难度,但确实节约时间。我记得有人说过:珍爱生命,我用python。还有人说过,人生苦短,我用正则表达式。这两个东西这里都用到了,算是够怕死了吧,呵呵。

Logo

权威|前沿|技术|干货|国内首个API全生命周期开发者社区

更多推荐