HOME Python download 書き込む

関数電卓 (Python 版)


1. 初めに

電卓スクリプトは適度に複雑なプログラムなので、新しいプログラミング言語を習得するときの 練習問題として適しています。言語の特徴を生かした電卓スクリプトが書ければ、 その言語をほぼマスターしたと考えていいと思います。
一方、電卓スクリプトがうまくかけない場合は、その言語そのものに難点があるか、 あなたとの相性は悪いということのなので、その言語はあきらめて他の言語を当たることをお勧めします。

Python は関数型、オブジェクト指向などのいろいろなパラダイムを使うことができます。そのため、 言語からの制約がほとんどなく、自由にプログラムをデザインすることができます。 そこで、Scheme 版と同様に、演算子をデータとして扱い、 個々の演算子と、計算手順を完全に分離するデザインを採用します。 このデザインでは、演算子を追加してもロジックに変更を加える必要が無いので、電卓プログラムの内部設計として 優れていると思います。
また、Python の優れた点として、

  1. 例外処理が充実している。
  2. 関数名や、リストの記法が短い。
  3. 要素数固定のタプルと要素数可変のリストがわかれている。
  4. 正規表現が充実している。
などがあります。

2. Python で実装した関数電卓スクリプト

早速コードを示します。

[code 1] (calc.py)

001:   #!/usr/bin/env python
002:   # -*- coding:euc-jp -*-
003:   
004:   u'''
005:   Python で書いた関数電卓プログラムです。
006:   動的型付けと関数オブジェクトを使うと簡潔に書けます。
007:   '''
008:   
009:   
010:   import math, re, sys, operator
011:   
012:   
013:   
014:   
015:   
016:   class Operator:
017:       u'''演算子と定数のクラスです'''
018:   
019:       # 演算子の優先度。大きい方から優先的に計算される
020:       P_CST=6                                 # 定数の優先度
021:       P_LHB=5                                 # ! の優先度
022:       P_FUN=4                                 # 関数の優先度
023:       P_POW=3                                 # 累乗の優先度
024:       P_UPM=2                                 # 単項の + と - の優先度
025:       P_MD=1                                  # *, /, % の優先度
026:       P_BPM=0                                 # 2項の +, - の優先度
027:   
028:       
029:       def __init__(self, name, fun, priority):
030:           self.name=name
031:           self.fun=fun
032:           self.priority=priority
033:   
034:       def __call__(self, x=None, y=None):
035:           return self.fun        if self.is_const() else                    \
036:                  self.fun(x)     if self.is_unary() or self.is_lhb() else   \
037:                  self.fun(x, y)
038:   
039:       def __gt__(self, other):
040:           u'''self が other より優先度が高いとき True を返す'''
041:           assert other is None or isinstance(other, Operator)
042:           return other is None or \
043:             self.priority > other.priority or \
044:             ( self.priority in (Operator.P_FUN, Operator.P_POW, Operator.P_UPM) and self.priority == other.priority)
045:   
046:       def __str__(self):
047:           return self.name
048:   
049:       def __repr__(self):
050:           return repr(self.name)
051:   
052:       def is_const(self):
053:           return self.priority==Operator.P_CST
054:   
055:       def is_lhb(self):
056:           return self.priority==Operator.P_LHB
057:   
058:       def is_upm(self):
059:           return self.priority==Operator.P_UPM
060:   
061:       def is_func(self):
062:           return self.priority==Operator.P_FUN
063:   
064:       def is_unary(self):
065:           return self.priority in (Operator.P_FUN, Operator.P_UPM)
066:   
067:       def is_binary(self):
068:           return self.priority in (Operator.P_POW, Operator.P_MD, Operator.P_BPM)
069:   
070:   
071:   
072:   # 関数
073:   
074:   def is_natural(n):
075:       u'''自然数?'''
076:       return type(n) is int and n>=0
077:   
078:   
079:   
080:   def fact(n):
081:       u'''階乗'''
082:       assert is_natural(n)
083:       return reduce(operator.__mul__, range(1, n+1)) if n>0 else 1
084:   
085:   
086:       
087:   def permutation(m,n):
088:       u'''順列'''
089:       assert is_natural(m) and is_natural(n) and m>=n
090:       return reduce(operator.__mul__, range(m-n+1, m+1), 1)
091:   
092:   
093:       
094:   def combination(m,n):
095:       u'''組み合わせ'''
096:       assert is_natural(m) and is_natural(n) and m>=n
097:       return permutation(m,n)/fact(n)
098:   
099:   
100:   
101:   
102:   
103:   # 演算子と定数の辞書。
104:   # 文字列をキーとし、対応する関数と優先度のタプル または 対応する定数を値にとる。
105:   L_OP= [ \
106:     Operator('@+', operator.__pos__, Operator.P_UPM), \
107:     Operator('@-', operator.__neg__, Operator.P_UPM), \
108:     Operator('+', operator.__add__, Operator.P_BPM),\
109:     Operator('-', operator.__sub__, Operator.P_BPM), \
110:     Operator('*', operator.__mul__, Operator.P_MD),\
111:     Operator('/', operator.__truediv__, Operator.P_MD), \
112:     Operator('%', operator.__mod__, Operator.P_MD), \
113:     Operator('<<', operator.__lshift__, Operator.P_POW), \
114:     Operator('>>', operator.__rshift__, Operator.P_POW), \
115:     Operator('**', math.pow, Operator.P_POW), \
116:     Operator('^', math.pow, Operator.P_POW), \
117:     Operator('exp', math.exp, Operator.P_FUN), \
118:     Operator('log', math.log, Operator.P_FUN), \
119:     Operator('log10', math.log10, Operator.P_FUN), \
120:     Operator('sqrt', math.sqrt, Operator.P_FUN),  \
121:     Operator('abs', operator.__abs__, Operator.P_FUN), \
122:     Operator('sin', math.sin, Operator.P_FUN), \
123:     Operator('cos', math.cos, Operator.P_FUN), \
124:     Operator('tan', math.tan, Operator.P_FUN), \
125:     Operator('asin', math.asin, Operator.P_FUN), \
126:     Operator('acos', math.acos, Operator.P_FUN), \
127:     Operator('atan', math.atan, Operator.P_FUN), \
128:     Operator('!', fact, Operator.P_LHB), \
129:     Operator('P', permutation, Operator.P_POW), \
130:     Operator('C', combination, Operator.P_POW), \
131:     Operator('pi', math.pi, Operator.P_CST), \
132:     Operator('e', math.e, Operator.P_CST), \
133:     ]
134:   
135:   # 演算子の名前をキーにしてハッシュ表を作る
136:   H_OP=dict([(str(op), op)  for op in L_OP])
137:   
138:   
139:   def convert_op_name(op):
140:       u'''演算子を正規表現文字列に変換して返す'''
141:   
142:       return ''.join([(c if c.isalnum() else '\\'+c) for c in str(op)]) + \
143:                (r'(?=\W|$)' if op.is_const() else r'(?=[\s\(])' if op.is_func() else '')
144:       
145:   
146:   
147:   # 式の要素を表す正規表現
148:   RE_FORM = re.compile( \
149:   r'''(?P<nest>\()                                            |   # 入れ子
150:       (?P<num>\d+(?P<after_dot>\.\d+)?(?:[eE][+-]?\d+)?)      |   # 数値
151:       (?P<op_name>%s)                                             # 演算子
152:   ''' % ('|'.join([ convert_op_name(op)  for op in sorted([op for op in L_OP if not op.is_upm()], key=lambda x:len(str(x)), reverse=True)]),), \
153:    re.VERBOSE)
154:   
155:   
156:   
157:   
158:   # ツール
159:   def cons(obj, ls):
160:       u'''Lisp 風にするため'''
161:       ls.append(obj)
162:       return ls
163:   
164:   
165:   
166:   def operator_position (ls):
167:       u'''ls の中で最初に計算する演算子の位置を返す'''
168:   
169:       tprev, term0, pos = None, None, -1
170:       
171:       for i, term in enumerate(ls):
172:           if isinstance(term, Operator) and (term > term0 or (isinstance(tprev, Operator) and term.is_upm())):
173:               term0, pos = term, i
174:           tprev=term
175:               
176:       return term0, pos
177:   
178:   
179:   
180:   def eval_ls(ls):
181:       u'''数値と演算子からなるリストを計算して結果を返す'''
182:   
183:       if operator.isNumberType(ls): return ls   # 数値ならそれを返す
184:       elif len(ls)==1:
185:           i=ls[0]
186:           return eval_ls(i() if isinstance(i, Operator) else i)    # 要素が1つのリストならそれを取り出す
187:       else:
188:           op, pos =operator_position(ls)  # 最初の使う演算子を探す
189:   
190:           if op.is_const(): # 定数
191:               return eval_ls(cons(op(), ls[:pos]) + ls[pos+1:])
192:           
193:           elif op.is_unary() and pos < len(ls)-1: # 単項演算子
194:               return eval_ls(cons(op(eval_ls(ls[pos+1])), ls[0:pos]) + ls[pos+2:])
195:               
196:           elif op.is_lhb() and pos>0: # !
197:               return eval_ls(cons(op(eval_ls(ls[pos-1])), ls[0:pos-1]) + ls[pos+1:])
198:               
199:           elif op.is_binary() and 0 < pos < len(ls)-1: # 二項演算子
200:               return eval_ls(cons(op( eval_ls(ls[pos-1]), eval_ls(ls[pos+1]) ), ls[0:pos-1]) + ls[pos+2:])
201:           
202:           else:
203:               raise RuntimeError, "invalid formmula: (%r)" % (ls,)
204:   
205:   
206:   
207:   def find_pair(s0):
208:       u'''対応する閉じ括弧を探す'''
209:       n=0
210:       for i, c in enumerate(s0):
211:           if c in '()': n+= (1 if c=='(' else -1)
212:           if n==0: return i
213:       else:
214:           raise RuntimeError, "Cannot find the close parenthesis!"
215:           
216:   
217:               
218:   def read_str(str):
219:       u'''文字列を読み込んで、数値と演算子のリストを返す'''
220:       def _iter(s, ls):
221:           s=s.strip()
222:           if(s):
223:               obj=RE_FORM.match(s)
224:               if obj and obj.group('nest'):
225:                   idx=find_pair(s)
226:                   return _iter(s[idx+1:], cons(_iter(s[1:idx], []), ls))
227:               elif obj:
228:                   s1=s[obj.end():]
229:                   if obj.group('num'):
230:                       return _iter(s1, cons((float if obj.group('after_dot') else int)(obj.group('num')), ls))
231:                   else:
232:                       op_name = obj.group('op_name')
233:                       if op_name in '+-' and  (not ls or (isinstance(ls[-1], Operator) and not ls[-1].is_lhb())):   # 単項の +/- を識別する
234:                           op_name = '@' + op_name
235:                       return _iter(s1, cons(H_OP[op_name], ls))
236:               else:
237:                   raise RuntimeError, "Cannot parse input!"
238:           else:
239:               return ls
240:   
241:       return _iter(str, [])
242:   
243:   
244:   
245:   if __name__=='__main__':
246:   
247:       interactive= len(sys.argv)>1 and sys.argv[1]=='-i'
248:       if interactive:
249:           sys.stderr.write("Available operators and constants:\n" + \
250:                            ', '.join([str(op) for op in L_OP if not op.is_upm()]) + \
251:                            "\nq:quit\n\n")
252:           
253:       while(1):
254:           if interactive: sys.stderr.write('> ')
255:           str=sys.stdin.readline()
256:           if str:
257:               str=str.rstrip()
258:               if interactive and str=='q': break
259:               try:
260:                   print eval_ls(read_str(str))
261:               except Exception,  strerror:
262:                   print strerror
263:           else:
264:               break
265:   
266:   

3. 使い方

コマンドラインから
python calc.py -i
と入力してください (対話的に計算するには -i オプションをつけてください)。計算式の入力を促すプロンプトが表示されます。 (q で終了。)
$ python calc.py -i
Available operators and constants:
+, -, *, /, %, <<, >>, P, C, **, ^, exp, log, log10, sqrt, abs, sin, cos, tan,
sin, acos, atan, !, pi, e
q:quit

> 4!*7+3
171
> 64>>2
16
> 3<<4
48
> sqrt sin (pi/3)
0.930604859102
> 1233333333333333333333333333333333 * 2222222222222222244444444444444
2740740740740740768148148148147599259259259259259251851851851852
> log exp 40
40.0
> q

$
次のようにパイプ処理することもできます。
python calc.py < input.txt > output.txt

4. 簡単な説明

4.1. Operator クラス

まず、演算子のクラスを定義します。 定数も定数関数としてこのクラスに含めます。 __call____gt__ を使って各種演算子をオーバーロードするによってプログラムを直感的にわかりやすいものにします。

定数

Operator クラスには次の定数が定義されています。
定数名 説明
P_CST 6 定数の優先度
P_LHB 5 ! の優先度
P_FUN 4 算術関数の優先度
P_POW 3 累乗の優先度
P_UPM 2 単項の + と - の優先度
P_MD 1 *, /, % の優先度
P_BPM 0 2項の +, - の優先度

メソッド

以下に Operator クラスのメソッドを挙げます
__init__(self, name, fun, priority)
初期化メソッドです。 name (名前), fun (演算の手続き), priority (優先度) を this.name, this.fun, this.priority として保持します。
__call__(self, x=None, y=None)
関数として呼ばれたときの挙動を規定します。
__gt__(self, other)
other より self を先に計算すべきとき True を返します。 リストの先頭から演算子を捜すことを前提にしています。 次の順でチェックし、該当する場合には True をしない場合には False を返します。
  1. other が None か?
  2. self.priority > other.priority か?
  3. self が右側の方が先に計算される演算子で、self.priority==other.priority か?
__str__(self)
self.name を返します
__repr__(self)
repr(self.name) を返します。
is_const(self)
self が定数なら True を返します。
is_lhb(self)
self が '!' なら True を返します。
is_upm(self)
self が 単項の +/- なら True を返します。
is_func(self)
self が算術関数なら True を返します。
is_unary(self)
self が単項演算子なら True を返します。
is_binary(self)
self が二項演算子なら True を返します。

4.2. Operator のリストと辞書

Operator のリスト LS_OP と名前をキーとする辞書 H_OP を定義します。 辞書を定義することによって Operator の検索が高速化されます。

単項の +/- と二項の +/- を区別するため、 単項の方の名前を "@+", "@-" にします。

4.3. 式の要素にマッチする正規表現

RE_FORM は式の要素にマッチする正規表現です。 式の要素は 入れ子、数値、演算子の3つがあります。 Python では名前付きグループが使えるので正規表現を明瞭に書くことができます。 演算子を表す部分は、個々の演算子にマッチするものを LS_OP から生成します。 この際、名前の長いほうから順番にマッチさせ、名前の短い演算子に先にマッチしないようにします。 さらに、先読みを用いて
  1. 算術演算子は 空白文字か開きカッコ
  2. 定数は 数字、アルファベット以外か、文字列の終わりと マッチするようにします。

4.4. 入力文字列の解析

read_str(str) 関数で文字列を読み込んで、式の要素のリストに変換します。 実際の処理は内部関数 _iter(s, ls) で行われます。 _iter の動作は以下のようになっています。
  1. 文字列の前後の空白文字を削除します。
  2. もし、その文字列が空なら得られたリストを返します。
  3. もし、最初の文字が開きカッコだったら、対応する閉じカッコをの位置 (idx) を探して カッコの中を解析して得られたリストをいままでの解析結果のリストにくわえ、 idx+1 番目の文字から解析を続けます。
  4. 数字にマッチすれば、マッチした部分を数に変換してリストに加えます。
  5. 演算子にマッチすれば、マッチした部分を Operator オブジェクトに変換してリストに加えます。 リストが空リストか、最後の要素が Operator の場合は、+, - は単項の +,- と解釈します。

4.5. 数と演算子のリストを処理して結果を算出する

eval_ls(ls) でリストを計算します。 eval_ls は再帰的に自分自身を呼び出します。 再帰的に処理することにより、入れ子になった式の計算を簡潔に記述することができます。
  1. 引数 (ls) が数値ならそれを返す
  2. 引数が要素数が1のリストなら、その要素を返す。 数値ならそのまま返し、定数演算子なら、__call__ して数値に変換して返す。
  3. それ以外の場合は最初に作用させる演算子を捜して、演算子の種類によって以下の処理を行う。
    1. 定数演算子の場合はそれを数値に変換して処理を続ける
    2. 単項演算子の場合はまず、演算子の次の項目に eval_ls を再帰的に適応してそれを数値に変換してから演算子を作用させる。 その結果をもとに処理を続ける。
    3. '!' の場合は演算子の前の項目に eval_ls を再帰的に適応してそれを数値に変換してから演算子を作用させる。 その結果をもとに処理を続ける。
    4. 二項演算子の場合は演算子の前後の項目に eval_ls を再帰的に適応してそれを数値に変換してから演算子を作用させる。 その結果をもとに処理を続ける。

5. 終わりに

電卓スクリプトを書いてつくづく思うのですが、Python はよく考えられたプログラミング言語です。

Python はいろいろなパラダイムがつかえ、正規表現などのライブラリやエラー処理が充実しています。 そのため簡潔にプログラムを書くことができます。 また、特殊メソッドを用いて演算子をオーバーロードすることによって 直感的にわかりやすいコードになります。