オプションの機能を関数の主な目的から切り離すためのpythonicな方法はありますか?


11

環境

次のPythonコードがあるとします。

def example_function(numbers, n_iters):
    sum_all = 0
    for number in numbers:
        for _ in range(n_iters):
            number = halve(number)
        sum_all += number
    return sum_all


ns = [1, 3, 12]
print(example_function(ns, 3))

example_functionここでは、単純にnsリストの各要素を調べ、結果を累積しながら3回に分けます。このスクリプトを実行すると、次のような出力になります。

2.0

1 /(2 ^ 3)*(1 + 3 + 12)= 2なので、

ここで、(何らかの理由で、おそらくデバッグやロギングなどの理由で)が実行している中間ステップに関するある種の情報を表示したいとしましょうexample_function。多分私はこの関数を次のようなものに書き直します:

def example_function(numbers, n_iters):
    sum_all = 0
    for number in numbers:
        print('Processing number', number)
        for i_iter in range(n_iters):
            number = number/2
            print(number)
        sum_all += number
        print('sum_all:', sum_all)
    return sum_all

これで、以前と同じ引数を使用して呼び出されると、以下が出力されます。

Processing number 1
0.5
0.25
0.125
sum_all: 0.125
Processing number 3
1.5
0.75
0.375
sum_all: 0.5
Processing number 12
6.0
3.0
1.5
sum_all: 2.0

これは私が意図したものを正確に達成します。ただし、これは関数が1つのことだけを行うべきであるという原則に少し反し、現在のコードexample_functionは少し長く、複雑です。そのような単純な関数の場合、これは問題ではありませんが、私のコンテキストでは、相互に呼び出す非常に複雑な関数があり、印刷ステートメントは、ここに示すよりも複雑なステップを含むことが多く、コードの複雑さが大幅に増加します(1つの私の関数には、実際の目的に関連する行よりも、ロギングに関連するコードの行の方が多かった!)

さらに、後で自分の関数に印刷ステートメントが不要になったと判断した場合、example_functionすべてのprintステートメントを手動で実行し、この機能に関連する変数をすべて削除する必要があります。これは、面倒でエラーの多いプロセスです。 -傾向がある。

関数の実行中に常に印刷または印刷しない可能性があり、2つの非常に類似した関数(1つはprintステートメントがある、1つはない)を宣言する場合、状況はさらに悪化します。次のようなものを定義するには:

def example_function(numbers, n_iters, debug_mode=False):
    sum_all = 0
    for number in numbers:
        if debug_mode:
            print('Processing number', number)
        for i_iter in range(n_iters):
            number = number/2
            if debug_mode:
                print(number)
        sum_all += number
        if debug_mode:
            print('sum_all:', sum_all)
    return sum_all

これにより、の単純な場合でも、膨らみ、(うまくいけば)不必要に複雑な機能が発生しますexample_function


質問

の元の機能から印刷機能を「分離」するためのpythonicな方法はありexample_functionますか?

より一般的には、オプションの機能を関数の主な目的から切り離すためのPythonの方法はありますか?


これまでに試したこと:

私が現時点で見つけた解決策は、デカップリングにコールバックを使用することです。たとえば、次のexample_functionように書き直すことができます。

def example_function(numbers, n_iters, callback=None):
    sum_all = 0
    for number in numbers:
        for i_iter in range(n_iters):
            number = number/2

            if callback is not None:
                callback(locals())
        sum_all += number
    return sum_all

次に、必要な印刷機能を実行するコールバック関数を定義します。

def print_callback(locals):
    print(locals['number'])

次のexample_functionように呼び出します:

ns = [1, 3, 12]
example_function(ns, 3, callback=print_callback)

次に出力します:

0.5
0.25
0.125
1.5
0.75
0.375
6.0
3.0
1.5
2.0

これにより、印刷機能がの基本機能から切り離されexample_functionます。ただし、このアプローチの主な問題は、コールバック関数がの特定の部分example_function(この場合、現在の数を半分にした直後)でしか実行できず、すべての印刷がそこで正確に実行される必要があることです。これにより、コールバック関数の設計が非常に複雑になる場合があります(一部の動作が実現不可能になります)。

たとえば、質問の前の部分で行ったのとまったく同じタイプの印刷を実行したい場合(どの番号が処理されているか、および対応する半分になっていることを示しています)、結果のコールバックは次のようになります。

def complicated_callback(locals):
    i_iter = locals['i_iter']
    number = locals['number']
    if i_iter == 0:
        print('Processing number', number*2)
    print(number)
    if i_iter == locals['n_iters']-1:
        print('sum_all:', locals['sum_all']+number)

これにより、以前とまったく同じ出力が得られます。

Processing number 1.0
0.5
0.25
0.125
sum_all: 0.125
Processing number 3.0
1.5
0.75
0.375
sum_all: 0.5
Processing number 12.0
6.0
3.0
1.5
sum_all: 2.0

しかし、書き込み、読み取り、デバッグが面倒です。


6
Python loggingモジュールをチェックしてください
Chris_Rands

@Chris_Randsは正しい..ロギングモジュールを使用する..このようにして、ロギングのオンとオフを切り替えることができます..次のリンクを使用してください。stackoverflow.com/questions/2266646/...
Yatish Kadam

2
loggingここでモジュールがどのように役立つかわかりません。私の質問ではprint、コンテキストを設定するときにステートメントを使用していますが、実際には、任意のタイプのオプション機能を関数の主な目的から切り離す方法の解決策を探しています。たとえば、実行中に物事をプロットする関数が必要な場合があります。その場合、loggingモジュールは適用できないと思います。
JLagana

3
@Pythonicは、Pythonの哲学を支持するpython構文/スタイル/構造/使用法を説明する形容詞です。これは構文規則や設計規則ではなく、クリーンで保守可能なpythonコードベースを生成するために責任を持って支持される必要があるアプローチです。あなたのケースでは、数行のtraceステートメントまたはprintステートメントを使用することで、保守性に値が追加され、それが得られます。自分に負担をかけないでください。あなたが理想的だと思う前述のアプローチのいずれかを検討してください。
Nair

1
この質問は広すぎます。特定の質問に対処することはできるかもしれませんが(loggingデモンストレーションを使用するための提案として)、任意のコードを分離する方法はできません。
chepner

回答:


4

関数内のデータを使用するために関数外の機能が必要な場合、これをサポートするために関数内にメッセージングシステムが必要です。これを回避する方法はありません。関数のローカル変数は外部から完全に分離されています。

ロギングモジュールは、メッセージシステムの設定に非常に優れています。これは、ログメッセージの出力だけに制限されているわけではありません。カスタムハンドラーを使用すれば、何でも実行できます。

メッセージシステムの追加は、コールバックの例と似ていますが、「コールバック」(ロギングハンドラー)が処理される場所をexample_function (ロガーにメッセージを送信することにより)内部のどこにでも指定できます。メッセージを送信するときに、ロギングハンドラで必要な変数を指定できます(引き続きを使用できますがlocals()、必要な変数を明示的に宣言することをお勧めします)。

新しいものexample_functionは次のようになります。

import logging

# Helper function
def send_message(logger, level=logging.DEBUG, **kwargs):
  logger.log(level, "", extra=kwargs)

# Your example function with logging information
def example_function(numbers, n_iters):
    logger = logging.getLogger("example_function")
    # If you have a logging system set up, then we don't want the messages sent here to propagate to the root logger
    logger.propagate = False
    sum_all = 0
    for number in numbers:
        send_message(logger, action="processing", number=number)
        for i_iter in range(n_iters):
            number = number/2
            send_message(logger, action="division", i_iter=i_iter, number=number)
        sum_all += number
        send_message(logger, action="sum", sum=sum_all)
    return sum_all

これは、メッセージを処理できる3つの場所を指定します。これ自体example_functionは、example_functionそれ自体の機能以外には何もしません。何も出力しないか、他の機能を実行しません。

に追加の機能を追加するにはexample_function、ロガーにハンドラーを追加する必要があります。

たとえば、送信した変数を印刷する場合(debugging例と同様)、カスタムハンドラーを定義してexample_functionロガーに追加します。

class ExampleFunctionPrinter(logging.Handler):
    def emit(self, record):
        if record.action == "processing":
          print("Processing number {}".format(record.number))
        elif record.action == "division":
          print(record.number)
        elif record.action == "sum":
          print("sum_all: {}".format(record.sum))

example_function_logger = logging.getLogger("example_function")
example_function_logger.setLevel(logging.DEBUG)
example_function_logger.addHandler(ExampleFunctionPrinter())

結果をグラフにプロットする場合は、別のハンドラーを定義するだけです。

class ExampleFunctionDivisionGrapher(logging.Handler):
    def __init__(self, grapher):
      self.grapher = grapher

    def emit(self, record):
      if record.action == "division":
        self.grapher.plot_point(x=record.i_iter, y=record.number)

example_function_logger = logging.getLogger("example_function")
example_function_logger.setLevel(logging.DEBUG)
example_function_logger.addHandler(
    ExampleFunctionDivisionGrapher(MyFancyGrapherClass())
)

必要なハンドラを定義して追加できます。これらはの機能から完全に分離され、example_functionexample_function提供する変数のみを使用できます。

ロギングはメッセージングシステムとして使用できますが、実際のロギングを妨げないように、PyPubSubなどの本格的なメッセージングシステムに移行することをお勧めします。

from pubsub import pub

# Your example function
def example_function(numbers, n_iters):
    sum_all = 0
    for number in numbers:
        pub.sendMessage("example_function.processing", number=number)
        for i_iter in range(n_iters):
            number = number/2
            pub.sendMessage("example_function.division", i_iter=i_iter, number=number)
        sum_all += number
        pub.sendMessage("example_function.sum", sum=sum_all)
    return sum_all

# If you need extra functionality added in, then subscribe to the messages.
# Otherwise nothing will happen, other than the normal example_function functionality.
def handle_example_function_processing(number):
    print("Processing number {}".format(number))

def handle_example_function_division(i_iter, number):
    print(number)

def handle_example_function_sum(sum):
    print("sum_all: {}".format(sum))

pub.subscribe(
    "example_function.processing",
    handle_example_function_processing
)
pub.subscribe(
    "example_function.division",
    handle_example_function_division
)
pub.subscribe(
    "example_function.sum",
    handle_example_function_sum
)

答えをありがとう、RPalmer。loggingモジュールを使用して提供したコードは、実際に、私が提案したprintand ifステートメントを使用して提案したものよりも整理され、保守可能です。ただし、関数の主な機能から印刷機能を切り離すことはありませんexample_function。つまり、example_function2つのことを同時に実行するという主な問題が依然として残っており、コードが思ったよりも複雑になっています。
JLagana

これを、たとえば私のコールバックの提案と比較してください。コールバックを使用するexample_functionと、機能が1つだけになり、印刷機能(または必要なその他の機能)はその外部で発生します。
JLagana

@JLagana様、こんにちは。私example_functionは印刷機能から切り離されています-関数に追加された唯一の機能はメッセージの送信です。これはコールバックの例に似ていますが、すべてのを送信するのではなく、必要な特定の変数のみを送信する点が異なりますlocals()。追加機能(印刷、グラフ作成など)を実行するのは、ログハンドラー(ロガーにどこかに接続する)です。ハンドラをアタッチする必要はまったくありません。その場合、メッセージが送信されても​​何も起こりません。これをより明確にするために投稿を更新しました。
RPalmer

私は正直に言って、あなたの例は印刷機能をの主な機能から切り離しましたexample_function。それをさらに明確にしてくれてありがとう!私はこの答えが本当に好きです。支払われる唯一の代償は、メッセージの受け渡しの複雑さが加わることです。PyPubSubへの参照もありがとうございました。これにより、オブザーバーパターンについて説明しました
JLagana

1

印刷ステートメントだけを使いたい場合は、コンソールへの印刷をオン/オフにする引数を追加するデコレータを使用できます。

以下は、キーワードのみの引数とデフォルト値をverbose=False関数に追加し、docstringと署名を更新するデコレーターです。関数をそのまま呼び出すと、期待される出力が返されます。で関数を呼び出すverbose=Trueと、印刷ステートメントがオンになり、期待される出力が返されます。これには、すべてのプリントの前にif debug:ブロックを付ける必要がないという追加の利点があります。

from functools import wraps
from inspect import cleandoc, signature, Parameter
import sys
import os

def verbosify(func):
    @wraps(func)
    def wrapper(*args, **kwargs):
        def toggle(*args, verbose=False, **kwargs):
            if verbose:
                _stdout = sys.stdout
            else:
                _stdout = open(os.devnull, 'w')
            with redirect_stdout(_stdout):
                return func(*args, **kwargs)
        return toggle(*args, **kwargs)
    # update the docstring
    doc = '\n\nOption:\n-------\nverbose : bool\n    '
    doc += 'Turns on/off print lines in the function.\n '
    wrapper.__doc__ = cleandoc(wrapper.__doc__ or '\n') + doc
    # update the function signature to include the verbose keyword
    sig = signature(func)
    param_verbose = Parameter('verbose', Parameter.KEYWORD_ONLY, default=False)
    sig_params = tuple(sig.parameters.values()) + (param_verbose,)
    sig = sig.replace(parameters=sig_params)
    wrapper.__signature__ = sig
    return wrapper

関数をラップすると、を使用して印刷機能のオン/オフを切り替えることができますverbose

@verbosify
def example_function(numbers, n_iters):
    sum_all = 0
    for number in numbers:
        print('Processing number', number)
        for i_iter in range(n_iters):
            number = number/2
            print(number)
        sum_all += number
        print('sum_all:', sum_all)
    return sum_all

例:

example_function([1,3,12], 3)
# returns:
2.0

example_function([1,3,12], 3, verbose=True)
# returns/prints:
Processing number 1
0.5
0.25
0.125
sum_all: 0.125
Processing number 3
1.5
0.75
0.375
sum_all: 0.5
Processing number 12
6.0
3.0
1.5
sum_all: 2.0
2.0

を検査example_functionすると、更新されたドキュメントも表示されます。関数にはdocstringがないため、それはデコレーターにあるものにすぎません。

help(example_function)
# prints:
Help on function example_function in module __main__:

example_function(numbers, n_iters, *, verbose=False)
    Option:
    -------
    verbose : bool
        Turns on/off print lines in the function.

コーディング哲学の観点から。副作用を引き起こさない機能を持つことは、関数型プログラミングのパラダイムです。Python 関数型言語にすることできますが、それだけに設計されているわけではありません。私は常にユーザーを念頭に置いてコードを設計します。

計算ステップを印刷するオプションを追加することがユーザーにとってメリットである場合、それを行うことには何の問題もありません。設計の観点からは、print / loggingコマンドをどこかに追加することに行き詰まります。


答えてくれてありがとう、ジェームズ。提供されたコードは、実際より組織し、私が提案したものよりも維持することが使用していることprintif声明。さらに、印刷機能の一部をexample_functionのメイン機能から実際に切り離すことができました。これはとても良かったです(デコレータが自動的にdocstringに自動的に追加されるのもいい感じです)。ただし、印刷機能をの主な機能から完全に分離するわけではありませんexample_function。それでも、printステートメントと付随するロジックを関数の本体に追加する必要があります。
JLagana

これを、たとえば私のコールバックの提案と比較してください。コールバックを使用すると、example_functionは1つの機能のみを持ち、印刷機能(または必要なその他の機能)はその外部で発生します。
JLagana

最後に、計算ステップを印刷することがユーザーにとってメリットである場合、どこかに印刷コマンドを追加することに行き詰まることに同意します。しかし、私はそれらがexample_functionの本体の外にあるようにしたいので、その複雑さは、主な機能の複雑さにのみ関連し続けます。これらすべての私の現実のアプリケーションでは、私はすでにかなり複雑なメイン機能を持っています。本体に印刷/プロット/ログ記録ステートメントを追加すると、保守とデバッグが非常に困難な獣になります。
JLagana

1

debug_mode条件をカプセル化する関数を定義し、目的のオプション関数とその引数をその関数に渡すことができます(ここで提案されています)。

def DEBUG(function, *args):
    if debug_mode:
        function(*args)

def example_function(numbers, n_iters):
    sum_all = 0
    for number in numbers:
        DEBUG(print, 'Processing number', number)
        for i_iter in range(n_iters):
            number = number/2
            DEBUG(print, number)
        sum_all += number
        DEBUG(print, 'sum_all:', sum_all)
    return sum_all

ns = [1, 3, 12]
debug_mode = True
print(example_function(ns, 3))

debug_mode呼び出す前に明らかに値が割り当てられている必要があることに注意してくださいDEBUG

もちろん、以外の関数を呼び出すこともできprintます。

の数値を使用して、この概念をいくつかのデバッグレベルに拡張することもできますdebug_mode


答えてくれてありがとう、ゲルト。確かに、あなたのソリューションは、あちこちのifステートメントの必要性を取り除き、印刷のオンとオフを簡単にすることもできます。ただし、印刷機能をの主要機能から切り離すことはありませんexample_function。これを、たとえば私のコールバックの提案と比較してください。コールバックを使用すると、example_functionは1つの機能のみを持ち、印刷機能(または必要なその他の機能)はその外部で発生します。
JLagana

1

単純化して私の回答を更新しました。関数にexample_functionは単一のコールバックまたはフックがデフォルト値で渡されるため、渡されたexample_functionかどうかをテストする必要はありません。

hook=lambda *args, **kwargs: None

上記はラムダ式であり、関数内のさまざまな場所で位置パラメータとキーワードパラメータの任意の組み合わせでこのデフォルト値を返しNoneexample_function呼び出すことができますhook

以下の例では、"end_iteration"および"result"イベントのみに関心があります。

def example_function(numbers, n_iters, hook=lambda *args, **kwargs: None):
    hook("init")
    sum_all = 0
    for number in numbers:
        for i_iter in range(n_iters):
            hook("start_iteration", number)
            number = number/2
            hook("end_iteration", number)
        sum_all += number
    hook("result", sum_all)
    return sum_all

if __name__ == '__main__':
    def my_hook(event_type, *args):
        if event_type in ["end_iteration", "result"]:
            print(args[0])

    print('sum = ', example_function([1, 3, 12], 3))
    print('sum = ', example_function([1, 3, 12], 3, my_hook))

プリント:

sum =  2.0
0.5
0.25
0.125
1.5
0.75
0.375
6.0
3.0
1.5
2.0
sum =  2.0

フック関数は、必要に応じて単純にすることも、複雑にすることもできます。ここでは、イベントタイプのチェックと簡単な印刷を行っています。しかし、loggerインスタンスを取得してメッセージをログに記録する可能性があります。必要な場合は、ロギングの豊富な機能をすべて使用できますが、必要ない場合は、単純にすることができます。


答えてくれてありがとう、ロナルド。コールバックのアイデアを拡張して、関数のさまざまな部分でコールバックを実行する(そしてコンテキスト変数をそれらに渡す)のアイデアは、実際には最善の方法のようです。これにより、コールバックの記述がはるかに簡単になり、手頃な価格でに複雑さが加わりますexample_function
JLagana

デフォルト値でいい感じ。これは多くのifステートメントを削除する簡単な方法です:)
JLagana
弊社のサイトを使用することにより、あなたは弊社のクッキーポリシーおよびプライバシーポリシーを読み、理解したものとみなされます。
Licensed under cc by-sa 3.0 with attribution required.