性能分析与提升

图形化工具进行效能分析


此篇博客主要谈谈用图形化工具分析与优化python代码,虽然我们的工程不是很大,但符合比较大吧,功能有字母频率统计、词频统计、支持stopword、动词时态归一化、动介短语频率统计。我以 step0-输出某个英文文本文件中 26 字母出现的频率,由高到低排列,并显示字母出现的百分比,精确到小数点后面两位为例来说明吧。 还是全说吧

  • step0-输出某个英文文本文件中 26 字母出现的频率,由高到低排列,并显示字母出现的百分比,精确到小数点后面两位
  • step1:输出单个文件中的前 N 个最常出现的英语单词,支持stopword,包含动词归一化。
  • step2: 2个或两个以上短语的频率,支持stopword,包含动词归一化。
  • step3: 统计动介短语出现的频率 , 支持stopword,包含动词归一化
    有人会问,效能分析不是对整个工程进行优化,你为什么割裂开了呢。因为一方面为了好分析,更重的是,写代码时功能是独立实现的,每个功能我都封装成了一个模块,所以一步一步来分析和效能分析的初衷并不想违背。

cprofile分析工具

在使用图形化工具之前之前先插一句话,那就是为什么要使用可视化工具呢?原因很简单直观,你可以去使用cProfile分析Python程序性能看一下使用cprofile的基本用法,不想看的话,可以接着往下看我的分析。因为我会简要说一下,当然你要是特别熟悉产cprofile你可以直接跳过这部分。直接去搭建图形化工具所需的环境。
举个例子,大致看下,不用那么认真。

#!/usr/bin/env python
#-*- coding:utf-8 -*-
#author: albert time:2018/10/22 0022

import time
import re
import operator
from string import punctuation           #所有标点

start = time.clock()

# 对文本的每一行计算字母频率的函数
def ProcessLine(line, counts):
    # 用去掉除了字母以外的其他数字
    line=ReplacePunctuations(line)
    for ch in line:
        counts[ch] = counts.get(ch, 0) + 1
    return counts

# 用去掉除了字母以外的其他数字
def ReplacePunctuations(line):
    tags = [',', '.', '?', '"', '“', '”', '—']
    for ch in line :
        #这里直接用了string的标点符号库。将标点符号替换成空格
        if ch in tags:
            line=line.replace(ch,"")
    return line

def main():
    file = open('gone_with_the_wind.txt')
    wordsCount = 0
    # 建立用于计算26个字母的空字典
    alphabetCounts = {}
    for line in  file:
        alphabetCounts = ProcessLine(line.lower(), alphabetCounts)  # 这里line.lower()的作用是将大写替换成小写,方便统计词频

    file.close()

if __name__ == "__main__":
    import cProfile

    end = time.clock()
    print(end - start)

    # 直接把分析结果打印到控制台
    cProfile.run("main()")
    # 把分析结果保存到文件中,不过内容可读性差...需要调用pstats模块分析结果
    cProfile.run("main()", "result")
    # 还可以直接使用命令行进行操作
    # >python -m cProfile myscript.py -o result

此程序执行的功能是统计英文文本文件中 26 字母出现的频率,程序有main,Processline,ReplacePunctuations三个模块。我想用cprofile分析性能进而提升性能, 于是,先运行一下,结果为图1
顺序很乱,如果程序在庞大一点,就很难分析啦,我们可以根据消耗的时间tottime排个顺序。(ps tottime:表示指定函数的总的运行时间,除掉函数中调用子函数的运行时间)

#接着上面的代码写
  import pstats
    #创建Stats对象
    p = pstats.Stats("result")
    # 先按time排序,再按cumulative时间排序,然后打倒出前50%中含有函数信息
    p.sort_stats('time', 'cumulative').print_stats()

在这里插入图片描述
我们大致知道main,Processline,ReplacePunctuations三个模块耗时,最多是ProcessLine,我们就需要看preocessLine()模块里调用了哪些函数,花费了多长时间,我们还是默认时间按时间排序。

    #接上面的程序,查看ProcessLine函数中调用了哪些函数
    p.print_callees("ProcessLine")

图2
可以看到花费时间比较多的一个是调用ReplacePunctuations时花费所花费的时间。
就这样一步步的分析,当然,在程序较小可以理清函数之间的调用关系,对程序很熟悉的时候,用cprofile分析工具是很明智的选择,但是程序很大,而且并不是所有代码都是自己写的时候,图形化工具就很有用。
比如:

#Count.py
def CountLetters(file_name,n,stopName,verbName):
	统计字母频率,很多代码
def CountWords(file_name,n,stopName,verbName):
	统计单个单词频率,很多代码
def CountPhrases(file_name,n,stopName,verbName,k):
    统计指定长度的词组频率,很多代码
def CountVerbPre(file_name,n,stopName,verbName,preName):
	统计动介短语的频率,很多代码

用cProfile分析的结果
在这里插入图片描述
在这里插入图片描述

太多了我就不粘图片了,所以这时候如果用图形化工具
可视化图片
好像有点看不清,可以放大开,也可以点进链接里面看:link
简单的分析一下这幅图,由于函数名是count.py,所以第二层占用时间最长(100%)的便是count(count.py)这个模块,我有四个主要模块CounterLetters、CounterWords、CountPhrases、CountVerbPre但是之间是否有调用关系我不知道,所以第三层显示了四个函数模块各自的运行(不包含调用其他模块的)的时间以及之间的调用关系,再往下就是这个函数模块里的某个函数,或者某个调用的函数所占时间的多少,我们可以找到占用时间最长的模块进行优化。但是,前提是保证绝对时间在减小,因为这张图体现的是相对时间,我们的目的还是让程序运行的绝对时间减少。所以下来说一下图形化工具

图像化工具的环境搭建

  • win10系统

  • 安装graphviz

    pip install graphviz

  • 下载转换 dot 的 python 代码gprof2dot 官方下载,下载完了,解压缩,将```gprof2dot.py``copy 到当前分析文件的路径,或者你系统 PATH 环境变量设置过的路径。

使用

python -m cProfile -o result.out -s cumulative step.py  //性能分析, 分析结果保存到 result.out 文件;
python gprof2dot.py -f pstats result.out | dot -Tpng -o result.png   //gprof2dot 将 result.out 转换为 dot 格式;再由 graphvix 转换为 png 图形格式。

结果

可视化图片
好像有点看不清,可以放大开,也可以点进链接里面看:link
简单的分析一下这幅图:
由于我的程序结构是:

#Count.py
def CountLetters(file_name,n,stopName,verbName):
	统计字母频率
def CountWords(file_name,n,stopName,verbName):
	统计单个单词频率
def CountPhrases(file_name,n,stopName,verbName,k):
    统计指定长度的词组频率
def CountVerbPre(file_name,n,stopName,verbName,preName):
	统计动介短语的频率

所以第二层占用时间最长(100%)的便是count(count.py)这个模块,第三层显示了CounterLetters、CounterWords、CountPhrases、CountVerbPre四个函数模块占用的时间,再往下就是这个函数模块里的某个函数,或者某个调用的函数所占时间的多少,我们可以找到占用时间最长的模块进行优化。但是,(敲黑板),前提是保证绝对时间在减小,因为这张图体现的是相对时间,我们的目的还是让程序运行的绝对时间减少。

STEP 0 输出某个英文文本文件中 26 字母出现的频率

step0-输出某个英文文本文件中 26 字母出现的频率,由高到低排列,并显示字母出现的百分比,精确到小数点后面两位为例来说明吧。

1.优化前

代码
#!/usr/bin/env python
#-*- coding:utf-8 -*-
#author: Enoch time:2018/10/22 0031

import time
import re
import operator
from string import punctuation           

start = time.clock()
'''function:Calculate the word frequency of each line
    input:  line : a list contains a string for a row
            counts: an empty  dictionary 
    ouput:  counts: a dictionary , keys are words and values are frequencies
    data:2018/10/22
'''
def ProcessLine(line,counts):
    #Replace the punctuation mark with a space

    line = re.sub('[^a-z]', '', line)
    for ch in line:
        counts[ch] = counts.get(ch, 0) + 1
    return counts

def main():
    file = open("../Gone With The Wind.txt", 'r')
    wordsCount = 0
    alphabetCounts = {}
    for line in file:
        alphabetCounts = ProcessLine(line.lower(), alphabetCounts)
    wordsCount = sum(alphabetCounts.values())
    alphabetCounts = sorted(alphabetCounts.items(), key=lambda k: k[0])
    alphabetCounts = sorted(alphabetCounts, key=lambda k: k[1], reverse=True)
    for letter, fre in alphabetCounts:
    	print("|\t{:15}|{:<11.2%}|".format(letter, fre / wordsCount))

    file.close()


if __name__ == '__main__':
    main()

end = time.clock()
print (end-start)

编程思路

我们打开文本,每次读取一行程序,用函数ProcessLine()来统计一行中出现的字母个数,在函数中ProcessLine()用正则表达式re.sub('[^a-z]', '', line)除去非数字的所有部分。有关正则表达式参考正则表达式
输出的结果应该是正确的,先看下最后一行,用时1s多,是该优化了。
输出结果

我们运行以下两行代码
python -m cProfile -o result.out -s cumulative step0.py
python gprof2dot.py -f pstats result.out | dot -Tpng -o result.png
得到的结果如下图, 觉得图别扭的可以点击更加舒服一点的图片查看

step0

可以看到文本有9千多行,low函数和re.sub被调用了9023次,每个字母每个字母的统计get也被调用了1765982次,反正就是一行一行处理太慢了,函数次数太多了。我们可以直接统计26个字母出现了几次,这样对于大的文本,我们是需要遍历26次。

2. 优化后

既然是处理一次处理整个文本,就可以直接封装成一个函数,用count来统计26个字母出现的频率,而不去处理那么非字母的字符

#!/usr/bin/env python
#-*- coding:utf-8 -*-
#author: Enoch time:2018/10/22 0031
import re
import time
import os
import string
import sys

letters = ['a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z']

def CountLetters(file_name):
    wordsCount = 0
    alphabetCounts = {}
    t0 = time.clock()
    with open(file_name) as f:
        txt = f.read().lower()
    for letter in string.ascii_lowercase:
        alphabetCounts[letter] = txt.count(letter) #here count is faster than re
        wordsCount += alphabetCounts[letter]
    t1 = time.clock()
    for letter in string.ascii_lowercase:
        alphabetCounts[letter] = alphabetCounts[letter]/wordsCount
    alphabetCounts = sorted(alphabetCounts.items(), key=lambda k: k[0])
    alphabetCounts = sorted(alphabetCounts, key = lambda k: k[1],reverse=True)
    t2 = time.clock()
   
    for letter,fre in alphabetCounts:
        print("|\t{:15}|{:<11.2%}|".format(letter, fre))
 	print(t2-t1)

if __name__ == '__main__':
    CountLetters('../gone_with_the_wind.txt')
运行结果

以后只关心运行的时间,功能正不正确单元测试,回归测试已经保证了,这里在不细讲啦
在这里插入图片描述

直接下降了好多级,当然这也是最终版本,中途改了几改。最重要的是,这些大部分也不是我想出来的,是队友太强,从最初我的最慢的replace替换掉非字符,到杨涛用正则表达式 re来处理字符问题,到后来张贺直接统计字母出现的频率,牺牲空间换时间,一路被带飞。此处只是说了优化前和优化后,中间的优化细节太细啦,就不细讲啦。忘了给优化后的图啦,我还是建议你们网页开,点这里看图片
在这里插入图片描述
看来这个工具统计的东西太多了,分析大工程很好用,小工程还是用cprofile吧!!!
#### 3. 待优化
如果对结果还不满意,上图用淡紫色已经圈出,你可以继续优化str.lowerstr.count两个函数,不对整个文本用str.lower,而是在统计后,在str.lower是不是可以优化,可以试试。
试了变慢了

STEP 1 输出单个文件中的前 N 个最常出现的英语单词。

step1的结尾我们在分析一下支持stopword这个功能

编程思路

起初我们使用正则表达式将多余的符号用空格代替,然后遍历所有的单词,用字典来存放单词及其出现的频率。但是由于遍历的次数太多,频繁的改动字典,耗时很大,于是我们改用collections中的Counter,可以统计相同单词出现的频率。源代码

结果

时间降到了0.4s左右
在这里插入图片描述

效能分析图

link
图4

待优化

待优化的部分便是re.findall,能不能用split()来划分呢?

支持stopword的编程思路

我们可以循环遍历字典的键值,将在```stopwords.txt``里的单词删除掉即可
源代码

结果

在这里插入图片描述

效能分析的结果

原图link
在这里插入图片描述
由于stopword.txt太小,处理它根本占不了多少时间,而且实际情况下,stopword.txt也不是很大。

step2: 2个或两个以上短语的频率 ,包含动词归一化

编程思路

起初我们按行处理,统计一行中短语出现频率,效率较低,之后我们改变思路,一次性统计,将文本连成一句话,由于re.findall在滑动的时候没有overlap,因此可以没统计一次,删掉开头一次词,在统计一遍。k个词的短语我们只需要遍历k-1次。
源代码

结果

用时1.6s左右
在这里插入图片描述

效能分析图

原图
在这里插入图片描述

待优化

待优化的部分是
* re.findal()l,能不能用split()来划分呢?
* sotred()函数
* re.sub()
换成了其他并没有提升,等待某天恍然大悟。

step3: 统计动介短语出现的频率、支持动词归一化

这里我们将动词归一化与动介短语的统计放到一起分析,因为比起统计动词归一化的动介短语,统计没有归一化的单词就没有太大意义。

代码

源代码

编程思路

将动词表存到字典里,key为动词各种时态包括原形,value为动词原形。
找到两个词语的短语,存入Counter里(类似于字典),遍历如果短语中第一个单词为动词,第二个单词为介词,则此短语为动介短语,存入字典的同时将动词归一化。

结果

在这里插入图片描述

效能分析

原图link

在这里插入图片描述
可以看到在判断两个词语的短语是不是动介短语时,我们执行了13万次,但是其占用的时间很少,也就是说我们可以先不急于优化这个。

待优化

待优化的部分是待优化的部分依然是
* re.findal()l,能不能用split()来划分呢?
* sotred()函数
* re.sub()
换成了其他并没有提升,等待某天恍然大悟。哈哈。

总结

此次效能分析,打破我们自动化原先的程序为我所用的思想,同时也觉得,为用户服务并不是一件小事。速度要快,性能要好。

猜你喜欢

转载自blog.csdn.net/qq_36097393/article/details/83574269