62940


ある関数の内部で、処理の本質には必要ないが、エラーログやデバッグログには表示したい情報があります。  
基本的にはそれらは関数の引数として渡すしかないと思っているのですが、どうにもスマートではない気がして抵抗があります。

プログラミングの一般論として、このやり方の是非についてのご意見、もしくはもっとスマートなやり方など、ご教示いただけないでしょうか。

* * *

<例>

カレントフォルダ直下の複数のcsvファイルを1行ずつ読み込んだうえで、  
それらを1カラムずつ、あるDBテーブルに登録する処理を以下のように書きました。

関数 `read_csv()` が関数`save_recodes()` を呼び出す構造は変更しない、という前提のもとで、  
エラーログに必要な情報 `csvname` `lno` を `save_recodes()` に渡す場合、どのような方法が  
スマートでしょうか。

※サンプルコードはpythonですが、他言語の場合のご意見もお待ちしています。

[code]

    
    def save_recodes(key, values, csvname, lno):
        for value in values:
            if is_valid(value):
                save_keyvalue(key, value)
            else:
                # エラーログ
                print(f'WARNING: invalid value "{value}", file"{csvname}", line {lno}'
                    , file=sys.stderr)
    
    def read_csv(csvname):
        for lno, line in enumerate(open(csvname), 1):
            key,*values = line.strip().split(',')
            save_recodes(key, values, csvname, lno)
    
    import glob
    for csvname in glob.glob('./*.csv'):
        read_csv(csvname)
    
[/code]


CC BY-SA 4.0
2020-02-10T10:14:08.223
62929
2020-02-11T06:51:25.477
2020-02-10T10:22:02.570
28998
28998
question
python デザインパターン
ログメッセージのためだけの情報を関数引数として渡すことの是非


私だったら、クラス等にまとめてメンバ変数として参照させます。

様々な関数のエラーメッセージに必要になるような変数なのであれば、その変数を使っている関数たちは何らかの責務を負っていそうで、となるとクラスに切り分けられそうです。その上で更に色々な関数で繰り返し同じ値を使うのであれば、メンバ変数として保持しておくと使いやすそうです。


CC BY-SA 4.0
2020-02-10T11:53:33.817
62932
2020-02-10T11:53:33.817
19110
62929
answer


エラーを伝達する方法として、以下のパターンが考えられます。

パターン1： 下位の階層から上位の階層へエラー情報を「例外」で通知する。  
パターン2： 下位の階層から上位の階層へ「引数」や「復帰値」で通知する。  
パターン3： 下位の階層から上位の階層へ「グローバルな領域」を介して通知する。

中間の階層でしか知りえない情報も合わせて上位に伝達したい場合、パターン3
も検討の候補に入れた方がよいと思います。なお、中間の階層ではエラー発生に備えて常にその情報を「グローバルな領域」に格納しておく必要があります。


CC BY-SA 4.0
2020-02-10T14:06:04.710
62935
2020-02-10T14:06:04.710
35558
62929
answer


参考までに、スマートな方法ではありませんが、Python には [PEP 232 -- Function
Attributes](https://www.python.org/dev/peps/pep-0232/)
がありますので、質問欄のサンプルコードは以下の様にも書くことができます。

[code]

    def save_recodes(key, values):
        for value in values:
            if is_valid(value):
                save_keyvalue(key, value)
            else:
                # エラーログ
                print(f'WARNING: invalid value "{value}", file"{read_csv.csvname}", line {read_csv.lno}',
                      file=sys.stderr)
    
    def read_csv(csvname):
        read_csv.csvname = csvname
        for read_csv.lno, line in enumerate(open(csvname), 1):
            key, *values = line.strip().split(',')
            save_recodes(key, values)
    
[/code]


CC BY-SA 4.0
2020-02-10T14:08:42.987
62936
2020-02-10T14:32:48.057
2020-02-10T14:32:48.057
62929
answer


`save_recodes()`の仕様が「DBにデータを登録する。無視可能なエラーが出たらログに記録する」というものであれば、ログメッセージに必要な情報を堂々と渡せばいいと思います。`save_recodes()`の仕様を満たすために必要不可欠なわけですから。

この場合はむしろ、ログに求められる要件は何かを整理してみることをお勧めします。

いつでも同じフォーマットで標準エラーにメッセージを書き出すのであれば、今のコードでも特に問題はないと思います。しかし例えば、`save_recodes()`が他の関数、`read_xml()`や`read_json()`から呼ばれ、それぞれメッセージが異なるとか、ログの出力先が変わることがあるとか、そういう要件が考えられるのなら、ログのメカニズムを、ログクラスを作るなり、グローバルなログ関数を作るなり、抽象化する必要があります。

要件を整理すれば、どこでメッセージを作るべきか、どこで実際に出力するかなど、はっきりしてくるのではないでしょうか。


CC BY-SA 4.0
2020-02-10T22:15:20.053
62940
2020-02-10T22:15:20.053
3605
62929
answer


自己レスです。  
ご意見ありがとうございます。  
自分自身、疑問を整理できてなかった部分があるのですが、頂いたご意見から以下のように納得しました。

  * 表題の疑問は悩む価値のあることなのか。全くの検討違いなことで時間を使っていたのではないのか。  
=> 回答もプラス投票もいただけたので、悩むに値する疑問だったのだろう、と思っています。

  * 表題の通り。「是か非か」。  
=> 100%是というわけではない。が、そのためにログメッセージの内容含め、別の妥協をするほどではない。

  * ログメッセージのための情報を渡すための専用のライブラリや定形コードはないか  
=> なさそう。元々はpythonのloggingモジュールに期待したのですが、調査した結果なさそうに思います。

* * *

<例>のケースでは、さしあたっては、ログを出力するすべての関数に、ログ情報専用の引数 `log_additionals` をもたせることにしました。  
システムの規模によっては、ログのためのグローバルな仕組みを自作することを検討します。

[code]

    def save_recodes(key, values, log_additionals=[]):
        for i, value in enumerate(values, 1):
            if is_valid(value, log_additionals):
                save_keyvalue(key, value, log_additionals)
            else:
                # エラーログ
                print(f'WARNING:',  ', '.join([f'invalid value "{value}"'] + log_additionals)
                    , file=sys.stderr)
    
    def read_csv(csvname, log_additionals=[]):
        for lno, line in enumerate(open(csvname), 1):
            key,*values = line.strip().split(',')
            save_recodes(key, values, log_additionals + [f'file "{csvname}", line {lno}'])
    
    import glob
    for csvname in glob.glob('./*.csv'):
        read_csv(csvname, [])
    
[/code]


CC BY-SA 4.0
2020-02-11T06:51:25.477
62950
2020-02-11T06:51:25.477
28998
62929
answer