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


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

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

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


6
파이썬 logging모듈을 확인하십시오
Chris_Rands

@Chris_Rands가 옳습니다. 로깅 모듈을 사용하십시오. 그러면 로깅을 켜고 끌 수 있습니다. 다음 링크를 사용하십시오. stackoverflow.com/questions/2266646/…
Yatish Kadam

2
logging모듈이 어떻게 도움이 되는지 모르겠습니다 . 내 질문 print에 컨텍스트를 설정할 때 명령문을 사용하지만 실제로 함수의 주요 목적에서 모든 유형의 선택적 기능을 분리하는 방법에 대한 솔루션을 찾고 있습니다. 예를 들어, 함수가 실행될 때 플롯하는 함수를 원할 수도 있습니다. 이 경우 logging모듈이 적용되지 않을 것이라고 생각합니다 .
JLagana

3
@Pythonic은 파이썬의 철학을 고수하기 위해 파이썬 구문 / 스타일 / 구조 / 사용법을 설명하는 형용사입니다. 이것은 구문이나 디자인 규칙이 아니라 깨끗하고 유지 관리 가능한 파이썬 코드베이스를 생성하기 위해 책임감있게 유지 해야하는 접근법입니다. 귀하의 경우, 추적 또는 인쇄 문이 적은 행은 유지 보수성에 값을 추가 한 후 유지합니다. 자기 자신을 힘들게하지 마십시오. 이상적이라고 생각되는 전술 한 접근법 중 하나를 고려하십시오.
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

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

답을 주셔서 감사합니다, RPalmer. logging모듈을 사용하여 제공 한 코드 는 실제로 사용 printif문에서 제안한 것보다 더 체계적이고 유지 관리가 쉽습니다. 그러나 인쇄 기능과 기능의 주요 기능을 분리하지는 않습니다 example_function. 즉, example_function한 번에 두 가지 작업 을 수행 하는 주된 문제는 여전히 남아 있기 때문에 코드를 내가 원하는 것보다 더 복잡하게 만듭니다.
JLagana

예를 들어 내 콜백 제안과 비교하십시오. 콜백을 사용하면 example_function이제 하나의 기능 만 있으며 인쇄 기능 (또는 다른 기능)은 외부에서 발생합니다.
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.

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

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


답변 주셔서 감사합니다, 제임스 제공된 코드는 실제로 사용 print하고 if진술 한 것보다 더 체계적이고 유지 관리가 가능합니다 . 또한 인쇄 기능의 일부를 실제로 example_function주요 기능과 분리하는 데 성공했습니다 . 그러나 인쇄 기능과 주요 기능을 완전히 분리하지는 않습니다 . 함수 본문에 명령문과 관련 논리를 example_function추가해야 print합니다.
JLagana

예를 들어 내 콜백 제안과 비교하십시오. 콜백을 사용하여 example_function에는 이제 하나의 기능 만 있으며 인쇄 기능 (또는 다른 기능)은 외부에서 발생합니다.
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.


답변 주셔서 감사합니다, Gerd. 실제로 솔루션은 if모든 곳에서 진술 의 필요성을 없애고 인쇄를 쉽게 켜고 끌 수 있습니다. 그러나 인쇄 기능을의 주요 기능에서 분리하지는 않습니다 example_function. 예를 들어 내 콜백 제안과 비교하십시오. 콜백을 사용하여 example_function에는 이제 하나의 기능 만 있으며 인쇄 기능 (또는 다른 기능)은 외부에서 발생합니다.
JLagana

1

단순화로 답변을 업데이트했습니다. 함수 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인스턴스를 확보 하고 메시지를 기록 할 수 있습니다 . 필요한 경우 로깅을 풍부하게 할 수 있지만 필요하지 않은 경우 단순성을 얻을 수 있습니다.


답변 주셔서 감사합니다, Ronald. 콜백 아이디어를 확장하여 함수의 다른 부분에서 콜백을 실행하고 컨텍스트 변수를 전달하는 아이디어는 실제로 가장 좋은 방법처럼 보입니다. 콜백을 훨씬 쉽게 작성하고보다 복잡한 추가 비용으로 합리적인 가격으로 글을 작성할 수 example_function있습니다.
JLagana

기본값을 잘 터치하십시오. 그것은 많은 if진술 을 제거하는 간단한 방법입니다 :)
JLagana
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.