今、話題の人工知能(AI)などで人気のPython。初心者に優しいとか言われていますが、全然優しくない! という事を、つらつら、愚痴っていきます

056.システム開発で何をなすべきか

»

初回:2019/12/18

1.久しぶりのプログラムネタです

 『Python なんて、大っ嫌い!』というブログ名を名乗っているのに、最近プログラムネタが少なくなっていたので、久しぶりのネタです。

 ひとえに、手塚規雄さんのブログ

 https://el.jibun.atmarkit.co.jp/noriwo_t/2019/12/post_147.html
 異常処理系の設計、プログラミング、テストの大切さ

 に、触発されたからです。

 一般的な顧客も初心者プログラマも異常処理系についての認識が『甘い』と言わざるを得ません。私の個人的な感覚で言うと、通常処理と異常系処理では、2対8、いや、1対9 の割合だと思っています。

 以前、私のブログで取り上げた

 https://el.jibun.atmarkit.co.jp/pythonlove/2019/09/048.html
 048.プログラミングで大切な事

 で、

 課題:1から10までの整数を加算した合計を表示する、最速のプログラムを作成しなさい。

 について、再度考えてみましょう。

P子「また、引っ掛け問題?」(※1

 要望:1から10まで加算した合計を表示する。

 まっとうなプログラマなら、print(55) なんてコーディングはしないでしょう。一番素直な書き方で言うと...

# 1からnまでの整数の和
def mysum( n ):
return (n+1)*n/2
print(mysum( 10 ))

 少しだけ、気を利かせるとして、等差数列で、初項 a、末項 b、項差 d の整数の和

# 初項 a、末項 b、項差 d の整数の和
def mysum( a,b,d=1 ):
    return ((b-a)/d+1)*(a+b)/2
print(mysum( 1,10 ))

 もちろん、a,b,dにマイナスや少数、まさかの英字や漢字なんかを入力するとエラーになります。そこで、整数かどうかのチェックを入れます。ここでは、文字列の数字かどうかのチェックを採用します。

# 数字の文字列が整数か判定
def is_integer(n):
try: return float(n).is_integer() except ValueError: return False
def mysum( a,b,d=1 ):
if a < 0 or not(is_integer(a)) :
raise ValueError( '初項 a は、0以上の整数でなければなりません。' )
if b < 1 or not(is_integer(b)) :
raise ValueError( '初項 b は、1以上の整数でなければなりません。' )
if d < 1 or not(is_integer(d)) :
raise ValueError( '初項 d は、1以上の整数でなければなりません。' )
    return ((b-a)/d+1)*(a+b)/2

 あえて、ひとつづつチェックしているのは、どの入力項目でエラーが発生しているのかわかりやすくするためです。

 実はこのプログラムはバグっていて、先ほど、is_integerチェックで許可した数字に変換可能な文字列が、ifの最初の a < 0 の比較演算でエラーになります。もちろん、計算式の箇所も、数字に変換可能な文字列が来ても大丈夫なようにfloat 変換を入れておきます。

def mysum( a,b,d=1 ):
    if not(is_integer(a)) or float(a) < 0.0 :
        raise ValueError( '初項 a は、0以上の整数でなければなりません。' )
    if not(is_integer(b)) or float(b) < 1.0 :
        raise ValueError( '末項 b は、1以上の整数でなければなりません。' )
    if not(is_integer(d)) or float(d) < 1.0 :
        raise ValueError( '項差 d は、1以上の整数でなければなりません。' )
    return ((float(b)-float(a))/float(d)+1.0)*(float(a)+float(b))/2.0

 この程度のプログラムなら問題はないのですが、mysum('A','B','C')と入れた場合、最初に'A'のエラー、それを修正すると'B'のエラーと、ひとつづつエラーが発生します。まとめてエラーチェックするのが『親切な』コーディングと言えます。だいぶ複雑になってきたので、エラーチェックは関数を分けます。

def mysumCheck( a,b,d=1 ):
    errLst = []
    if not(is_integer(a)) or float(a) < 0.0 :
errLst.append( '初項 a は、0以上の整数でなければなりません。' )
if not(is_integer(b)) or float(b) < 1.0 :
errLst.append( '末項 b は、1以上の整数でなければなりません。' )
if not(is_integer(d)) or float(d) < 1.0 :
errLst.append( '項差 d は、1以上の整数でなければなりません。' )
    if len(errLst) == 0 :
return True
else :
raise ValueError( errLst )
def mysum( a,b,d=1 ):
if mysumCheck( a,b,d ) :
return ((float(b)-float(a))/float(d)+1.0)*(float(a)+float(b))/2.0
else :
raise Exception( '原因不明なエラーが発生しました' )

今さらですが、初項 a と 末項 b の大小関係のチェックが必要です。同じ値の場合も OK とします。

def mysumCheck( a,b,d=1 ):
    errLst = []
    if not(is_integer(a)) or float(a) < 0.0 :
errLst.append( '初項 a は、0以上の整数でなければなりません。' )
if not(is_integer(b)) or float(b) < 1.0 :
errLst.append( '末項 b は、1以上の整数でなければなりません。' )
if not(is_integer(d)) or float(d) < 1.0 :
errLst.append( '項差 d は、1以上の整数でなければなりません。' )
    try:
if float(a) > float(b) :
errLst.append( '初項 a と末項 b の大小関係が逆転しています。' )
except ValueError:
# is_integer のチェックですでにエラーが出ているが...
errLst.append( '初項 a か末項 bの数値変換に失敗しました。' )
    if len(errLst) == 0 :
return True
else :
raise ValueError( errLst )

 国際化対応としてメッセージ関連を一か所にまとめます。

 https://maku77.github.io/python/syntax/const.html
 Python で定数を定義する

で、紹介されています、const.py を実装しても良いのですが、共通的に使われるはずなのでここでは行数に含めないでおきます。メッセージはサブシステム単位に採番したり翻訳しやすいようにリソースファイルにまとめるかデータベースで管理するのが一般的です。ここでは簡易的に雰囲気だけ味わっていただくためのコードを記述します。エラーメッセージにエラーコードをマージするのは、エラー発生時に迅速にエラー箇所が判るようにするためです。

# メッセージの言語
LANG='ja'
# メッセージの辞書
MSGDIC={
# エラーメッセージ
'ERR0001':{'ja':'初項 a は、0以上の整数でなければなりません。'
, 'en':'The first term a must be an integer greater than or equal to zero.'}
, 'ERR0002':{'ja':'末項 b は、1以上の整数でなければなりません。'
, 'en':'The last term b must be an integer greater than or equal to 1.'}
, 'ERR0003':{'ja':'項差 d は、1以上の整数でなければなりません。'
, 'en':'The term difference d must be an integer greater than or equal to 1.'}
, 'ERR0004':{'ja':'初項 a と末項 b の大小関係が逆転しています。'
, 'en':'The magnitude relationship between the first term a and the last term b is reversed.'}
, 'ERR0005':{'ja':'初項 a か末項 bの数値変換に失敗しました。'
, 'en':'The numeric conversion of the first term a or the last term b failed. '}
, 'ERR0006':{'ja':'原因不明なエラーが発生しました'
, 'en':'An unknown error has occurred '}
, 'ERR0007':{'ja':'エラーコードが存在しません[{}]'
, 'en':'Error code does not exist[{}] '}
    # 通常メッセージ
, 'MSG0001':{'ja':'初項 a、末項 b、項差 d の等差数列の和を求めます。'
, 'en':'Find the sum of an arithmetic sequence of the first term a, last term b, and term difference d.'}
, 'MSG0002':{'ja':'初項[{}]'
, 'en':'First term[{}] '}
, 'MSG0003':{'ja':'末項[{}]'
, 'en':'Last item[{}] '}
, 'MSG0004':{'ja':'項差[{}]'
, 'en':'Term difference[{}] '}
}
# メッセージ辞書オブジェクトを作成する。
def msgDic( msgCD,arg1='' ) :
# msgCDから辞書を取り出す、
mDic = MSGDIC.get(msgCD)
if mDic is None : # 辞書になければエラー
# ERR0007:エラーコードが存在しません[{}]
return err( 'ERR0007',msgCD )
    # msgCDから辞書を取り出した辞書から、言語に対応したメッセージを取り出す。
mDicLn = mDic.get(LANG)
if mDicLn is None : # 言語が存在しない場合
mDicLn = mDic.get('ja') # 日本語のメッセージを返す
    return mDicLn
# エラーメッセージの表示
def err( errCd,arg1='' ) :
return errCd + ':' + msgDic(errCd).format( arg1 )
# 通常メッセージの表示
def msg( msgCd,arg1='' ) :
return msgDic(msgCd).format( arg1 )

2.最終系のまとめ

 先のメッセージ関連のソースと合わせて、下記のソースですべてです。
 メイン処理も付けておきます。

# 数字の文字列が整数か判定
def is_integer(n):
    try:
        return float(n).is_integer()
    except ValueError:
        return False
# 等差数列の和を求める処理の事前チェック
# 名称が変だが、float化された数値を返します。
def mysumCheck( a,b,d=1 ):
errLst = []
    if not(is_integer(a)) or float(a) < 0.0 :
# ERR0001:初項 a は、0以上の整数でなければなりません。
errLst.append( err('ERR0001') )
if not(is_integer(b)) or float(b) < 1.0 :
# ERR0002:末項 b は、1以上の整数でなければなりません。
errLst.append( err('ERR0002') )
if not(is_integer(d)) or float(d) < 1.0 :
# ERR0003:項差 d は、1以上の整数でなければなりません。
errLst.append( err('ERR0003') )
    try:
if float(a) > float(b) :
# ERR0004:初項 a と末項 b の大小関係が逆転しています。
errLst.append( err('ERR0004') )
except ValueError:
# is_integer のチェックですでにエラーが出ているが...
# ERR0005:初項 a か末項 bの数値変換に失敗しました。
errLst.append( err('ERR0005') )
    if len(errLst) == 0 :
return (float(a),float(b),float(d))
else :
raise ValueError( errLst )
# 等差数列の和を求める処理
def mysum( a1,b1,d1=1 ):
# 引数の整合性チェックと、float化を行う。
a,b,d = mysumCheck( a1,b1,d1 )
    return ((b-a)/d+1.0)*(a+b)/2.0
# メイン処理
def main():
# MSG0001:初項 a、末項 b、項差 d の等差数列の和を求めます。
print( msg('MSG0001') )
    # 初期値を設定しておきます。
a0 = 1
b0 = 10
c0 = 1
    # 画面から入力する
a = input( msg('MSG0002',a0) ) or a0 # 初項[{}]
b = input( msg('MSG0003',b0) ) or b0 # 末項[{}]
d = input( msg('MSG0004',c0) ) or c0 # 項差[{}]
    # 初項 a、末項 b、項差 d で、等差数列の和を求めるメイン処理
print( mysum(a,b,d) )
# メイン処理(Python)
if __name__ == "__main__":
main()

P子「ちょっとやり過ぎじゃない?」

 この後テストデータを作成して、それを実行するプログラムも作ろうと思ってましたが、止めておきます。ただ、テストデータは、境界条件や不正な値、あり得ない大きな値や小さな値、コンピュータの苦手な値など、色々と知恵を絞って作る必要があり、ここを疎かにすると本番で信じられないデータに泣かされることになります。気が向いたら、いつかテーマに取り上げたいと思います。

3.まとめ

 エラー処理を除いて64行です。メイン処理も入っているとはいえ、最初 3行のコードから考えると、1対20 位に増幅しています。今回はコマンドラインからメソッドを直接起動するだけですが、GUIを使ったりWebアプリケーション化したり、クラス化するとか、データベースから取り出したデータを計算したうえでデータベースに書き込むとか、マルチスレッド化するとか、エラー発生時にログファイルやデータベースに書き出したりしないと、業務アプリケーションとしては使えません。

 今回はブログ用に作成したので、社内のコーディングルールに沿っていませんし、検証チームも参加していません。メソッド名もいまいちです。しかも、このプログラムの設計仕様書とか、操作マニュアルとか必要でしょう。プロジェクトの進捗会議や顧客説明会など、関連する作業も多数あります。もちろん、開発業務以外の業務もいっぱいあります。

P子「なんか、愚痴がまじってない?」

 さらに、通常の開発では顧客の要望が次から次と押し寄せてきます。

 例えば、等差数列の和ではなく、指定のデータの和を求める様に変更が入るなんて、日常茶碗蒸しです。

https://el.jibun.atmarkit.co.jp/chatrun/2013/12/post-c448.html
【日常茶碗蒸し】

P子「こっそり『SEの格言・迷言・ことわざ集』の宣伝をぶっこんだわね」

 てへ(※2

 エンジニアがプログラム開発だけしている訳ではないことを理解してもらえれば、満足です。

P子「上司に理解してもらえれば...ってことね」

ほな、さいなら

======= <<注釈>>=======

※1 P子「また、引っ掛け問題?」
 P子とは、私があこがれているツンデレPythonの仮想女性の心の声です。

 課題:1から10までの整数を加算した合計を表示する、最速のプログラムを作成しなさい。
 回答:print(55)

※2 てへ
 軽くグーを作って自分の頭をこつんとしながら、ペロッと舌を出します。遠くから見ると「シェー」に見えるのでご注意ください。

Comment(0)

コメント

コメントを投稿する