[python] 선택적 기능을 함수의 주요 목적에서 분리하는 파이썬적인 방법이 있습니까?

문맥

다음 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목록 의 각 요소를 간단히 살펴 보고 결과를 누적하면서 세 번 반으로 줄였습니다. 이 스크립트를 실행 한 결과는 다음과 같습니다.

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

이것은 내가 의도 한 것을 정확하게 달성합니다. 그러나 이것은 함수가 한 가지 작업 만 수행해야한다는 원칙에 위배되며 이제는 코드 example_function가 엄청나게 길고 복잡합니다. 이러한 간단한 함수의 경우 이것은 문제가되지 않지만 내 상황에서 서로 호출하는 매우 복잡한 함수가 있으며 인쇄 명령문에는 종종 여기에 표시된 것보다 더 복잡한 단계가 포함되어 코드의 복잡성이 크게 증가합니다. 내 기능 중 실제 목적과 관련된 줄보다 로깅과 관련된 코드 줄이 더 많았습니다!).

또한 나중에 더 이상 함수에 인쇄 문을 원하지 않는다고 결정하면 이 기능과 관련된 변수와 함께 모든 문을 수동으로 example_function삭제하고 삭제해야합니다 print. 지루하고 오류가있는 프로세스 -경향이 있습니다.

함수 실행 중에 항상 인쇄하거나 인쇄하지 않을 가능성이 있기 때문에 상황이 더욱 악화 print되어 유지 보수가 끔찍한 두 개의 매우 유사한 함수 ( 문이 있거나 하나가없는 함수)를 선언 하거나 다음과 같이 정의하십시오.

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.


질문

인쇄 기능을 원래 기능과 “분리”하는 비법적인 방법이 example_function있습니까?

더 일반적으로, 선택적 기능을 함수의 주요 목적에서 분리하는 파이썬적인 방법이 있습니까?


내가 지금까지 시도한 것 :

내가 지금 찾은 해결책은 디커플링에 콜백을 사용하는 것입니다. 예를 들어 다음 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

그러나 쓰고 읽고 디버깅하는 데 어려움이 있습니다.



답변

함수 내부의 데이터를 사용하기 위해 함수 외부의 기능이 필요한 경우이를 지원하기 위해 함수 내에 일부 메시징 시스템이 있어야합니다. 이 문제를 해결할 방법이 없습니다. 함수의 지역 변수는 외부와 완전히 분리되어 있습니다.

로깅 모듈은 메시지 시스템 설정에 매우 적합합니다. 사용자 정의 핸들러를 사용하여 로그 메시지를 인쇄하는 것만으로 제한되지 않습니다.

메시지 시스템을 추가하는 것은 콜백 예제 (로깅 핸들러)가 처리되는 장소를 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

메시지를 처리 ​​할 수있는 세 위치를 지정합니다. 그 자체로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_function변수들만 사용할 수 example_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
)


답변

인쇄 문 만 사용하려면 콘솔에 인쇄를 켜거나 끄는 인수를 추가하는 데코레이터를 사용할 수 있습니다.

다음은 키워드 전용 인수와 기본값 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.

코딩 철학의 관점에서. 부작용을 일으키지 않는 기능을 갖는 것은 기능적 프로그래밍 패러다임입니다. 파이썬 은 할 수 있습니다 기능적인 언어가 될 수 있지만 독점적으로 그렇게 설계되지는 않았습니다. 항상 사용자를 염두에두고 코드를 디자인합니다.

계산 단계를 인쇄하기위한 옵션을 추가하는 것이 사용자에게 이익이된다면, 그렇게하는 데 아무런 문제 가 없습니다 . 디자인 관점에서 인쇄 / 로깅 명령을 어딘가에 추가해야합니다.


답변

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.


답변

단순화로 답변을 업데이트했습니다. 함수 example_functionexample_function더 이상 통과 여부를 테스트 할 필요 가 없도록 단일 콜백 또는 기본값으로 후크 에 전달됩니다.

hook=lambda *args, **kwargs: None

위의 람다 식은 이 기본값 을 반환 None하고 example_function호출 할 수 있습니다hook 의 함수는 함수 내의 다양한 위치에서 위치 및 키워드 매개 변수를 조합 .

아래 예에서는 "end_iteration"and "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인스턴스를 확보 하고 메시지를 기록 할 수 있습니다 . 필요한 경우 로깅을 풍부하게 할 수 있지만 필요하지 않은 경우 단순성을 얻을 수 있습니다.


답변