輪郭分析

Python には プロファイラ がありますが、ここでは簡単な行実行回数プロファイラを作成してみたいと思います。

このプロファイラは 2 つのプログラムからなります。1 つはプロファイルされる Python プログラムを読み込んで、新しい Python プログラムを作成します。新しいプログラムは、コードブロックの先頭でカウンタを 1 インクリメント増やし、プログラム終了時に実行回数をファイルに書き出すようになっています。2 つ目のプログラムは、実行回数が記録されたファイルと元の Python プログラムを読み込んで、コードと共に行実行回数を表示します。

makeprof.py は、プロファイルされる Python プログラムからプロファイル版の Python プログラムを生成します。

# makeprof.py - python プログラムのプロファイル版を作成する
#     > python makeprof.py program.py >program.p.py とすると
#     program.py のプロファイル版 program.p.py を生成する

import sys

init = """
class Prof:
  def __init__(self, size):
    self.lst = [0] * size
    
  def count(self, n):
    self.lst[n] += 1
    
  def output(self):
    file = open("prof.cnts", "w")
    for i in self.lst:
      file.write("%d\\n" % i)
    file.close()
"""

def parse(filename):
  def nspc(line):
    n = 0
    for c in line:
      if c == ' ':
        n += 1
      else:
        break
    return n + 2
 
  file = open(filename, "r")
  counter = 0
  lines = []
  for line in file.readlines():
    lines.append(line)
    if len(line) > 1 and line[-2] == ":":
      lines.append("%sprof.count(%d)\n" % (' ' * nspc(line), counter))
      counter += 1
  file.close()
  return lines, counter
    
def makeprof(filename):
  lines, cnt = parse(filename)

  print init
  print "prof = Prof(%d)\n" % cnt
  for line in lines:
    print line,
  print "\nprof.output()"

makeprof(sys.argv[1])

関数 makeprof では読み込んだ Python プログラムの各行に対し、行末がセミコロン(:)で終わっていれば、その次の行に以下のコードを埋め込みます:

prof.count(n)

n は行によってにユニークな値になるような値にします。

テストプログラムとしてフィボナッチ数を計算するプログラム:

# fib.py

def fib(n):
  if n == 0 or n == 1:
    return 1
  else:
    return fib(n-1) + fib(n-2)

def fib2(n, a=1, b=0):
  if n < 1:
    return a
  else:
    return fib2(n-1, a+b, a)

for i in range(1, 10):
  print fib(i)

for i in range(1, 10):
  print fib2(i)

fib.py を makeprof.py にかけると、つぎのようなコードを出力します。

# fib.py を入力として makeprof.py から出力されるプログラム

class Prof:
  def __init__(self, size):
    self.lst = [0] * size
    
  def count(self, n):
    self.lst[n] += 1
    
  def output(self):
    file = open("prof.cnts", "w")
    for i in self.lst:
      file.write("%d\n" % i)
    file.close()

prof = Prof(8)

def fib(n):
  prof.count(0)
  if n == 0 or n == 1:
    prof.count(1)
    return 1
  else:
    prof.count(2)
    return fib(n-1) + fib(n-2)

def fib2(n, a=1, b=0):
  prof.count(3)
  if n < 1:
    prof.count(4)
    return a
  else:
    prof.count(5)
    return fib2(n-1, a+b, a)

for i in range(1, 10):
  prof.count(6)
  print fib(i)

for i in range(1, 10):
  prof.count(7)
  print fib2(i)

prof.output()

出力されたプログラムを実行すると、各行の実行回数が記録されたファイル(prof.cnts)が作成されます。

2 つ目のプログラム printprof.py は、prof.cnts と元のプログラム fib.py を元にプロファイル結果を表示します。

# printprof.py - プロファイルで得られた行実行回数を表示
#    > python printprof.py program.py とすると
#    prof.cnts に記録された行実行回数と program.py を表示する

import sys

def read_profcnts():
  lst = []
  file = open("prof.cnts", "r")
  for line in file.readlines():
    lst.append(line[:-1])
  return lst

def printprof(filename):
  cntslst = read_profcnts()
  file = open(filename, "r")
  cnt = 0
  for line in file.readlines():
    if len(line) > 1 and line[-2] == ":":
      print "%7s  " % cntslst[cnt], line,
      cnt += 1
    else:
      print " " * 9, line,

printprof(sys.argv[1])

fib.py に対するプロファイル実行の様子を以下に示します。

> python makeprof.py fib.py >fib.p.py # プロファイル版の fib.p.py を生成
> python fib.p.py                     # fib.p.py の実行
…実行結果省略…
> python printprof.py fib.py          # プロファイルの結果を表示
   275   def fib(n):
   142     if n == 0 or n == 1:
             return 1
   133     else:
             return fib(n-1) + fib(n-2)
    54   def fib2(n, a=1, b=0):
     9     if n < 1:
             return a
    45     else:
             return fib2(n-1, a+b, a)
     9   for i in range(1, 10):
           print fib(i)
     9   for i in range(1, 10):
           print fib2(i)

プログラムの左側に実行回数が表示されます。たとえば、fib 関数は 275 回呼び出されており、そのうち if 文の真ブロックが 142 回、else ブロックが 133 回実行されているのが分かります(142 + 133 = 275)。

たとえば チェス盤にクイーンをおく問題 で作成した queen.py に対してこのプロファイラを実行してみると次のようになります。

    92   def print_board(size, board):
   184     def border():
             return '+' + '-' * (size*2+1) + '+'
   736     def line(x):
             return '| ' + '. ' * x + 'Q ' + '. ' * (size-x-1) + '|'

           print border()
   736     for x,y in board:
             print line(x)
           print border()

  5508   def conflict(x, y, board):
 19368     def threat(a, b):
             return abs(x-a) == abs(y-b)

 19368     for a,b in board:
  3452       if threat(a, b):
               return True
           return False

     1   def queen(size):
  1965     def rec(y, board, rst):
  5508       for x in rst:
  2056         if not conflict(x, y, board):
                 bd = board + ((x,y),)
    92           if y+1 == size:
                   print_board(size, bd)
  1964           else:
                   rec(y+1, bd, filter(lambda a: a!=x, rst))
           rec(0, (), range(size))

         queen(8)

このプロファイラは「行末にセミコロンがある行の次の行に(プロファイル用の)コードを埋め込む」というシンプルな作りなため、fib.py のような単純なコードでは便利に利用できますが、一行で書かれた if 文に対応できていないなど多くの不都合があります。あくまで参考程度にということでご理解ください。

このプロファイラ作成には『プログラミング言語 AWK』を参考にしました。


Python ページ