Чи є пітонічний спосіб від'єднати необов'язковий функціонал від основного призначення функції?


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модуль python
Chris_Rands

@Chris_Rands вірно .. використовуйте модуль реєстрації .. таким чином ви можете включати та вимикати вхід, використовуючи наступне посилання. stackoverflow.com/questions/2266646 / ...
Yatish Kadam

2
Я не бачу, як loggingмодуль міг би тут допомогти. Хоча моє запитання використовує printвисловлювання при налаштуванні контексту, я насправді шукаю рішення, як від'єднати будь-який тип додаткової функціональності від основного призначення функції. Наприклад, можливо, я хочу, щоб функція будувала речі так, як вона працює. У такому випадку я вважаю, що loggingмодуль навіть не застосований.
JLagana

3
@Pythonic - прикметник, який описує синтаксис / стиль / структуру / використання пітона, щоб підтримувати філософію Python. Це не синтаксичне або дизайнерське правило, а підхід, який потрібно відповідально підтримувати, щоб створити чисту і бездоганну базу даних коду python. У вашому випадку наявність декількох рядків трактування або тверджень про друк додає значення для ремонтопридатності, а потім - це; не будьте жорсткі до себе. Розгляньте будь-який із вищезгаданих підходів, які ви вважаєте ідеальними.
Наїр

1
Це питання занадто широке. Ми можемо вирішити конкретні запитання (як свідчать пропозиції щодо використання logging), але не як розділити довільний код.
чепнер

Відповіді:


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модуля, справді більш організований та підтримуваний, ніж те, що я запропонував використовувати printта ifзаяви. Однак це не відокремлює функцію друку від основної функціональності 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ви також побачите оновлену документацію. Оскільки у вашій функції немає докстрингу, вона є лише те, що є в декораторі.

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та ifзаяви. Крім того, йому вдається фактично відокремити частину функціональності друку від example_functionосновної функціональності, що було дуже приємно (мені також сподобалося, що декоратор автоматично додає до docstring, приємний дотик). Однак це не повністю відокремлює функцію друку від основної функціональності 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.


Дякую за відповідь, Герде. Дійсно, ваше рішення позбавляється від потреби у ifвиписках повсюдно, а також полегшує вмикання та вимикання друку. Однак це не відокремлює функціонал друку від основного функціоналу example_function. Порівняйте це, наприклад з моєю пропозицією зворотного дзвінка. Використовуючи зворотні дзвінки, функція example_function має лише один функціонал, а друкований матеріал (або будь-який інший функціонал, який ми хотіли б мати) відбувається поза ним.
JLagana

1

Я оновив свою відповідь спрощенням: функція example_functionпередається одним зворотним дзвоном або гачком зі значенням за замовчуванням, таким чином, що example_functionбільше не потрібно перевіряти, чи було це передано чи ні:

hook=lambda *args, **kwargs: None

Наведене вище - лямбда-вираз, який повертає Noneі example_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
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.