Kontekst
Załóżmy, że mam następujący kod 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_functiontutaj po prostu przeglądam każdy z elementów na nsliście i dzielimy je na trzy razy, jednocześnie kumulując wyniki. Wynikiem uruchomienia tego skryptu jest po prostu:
2.0
Ponieważ 1 / (2 ^ 3) * (1 + 3 + 12) = 2.
Teraz powiedzmy, że (z dowolnego powodu, być może debugowania lub logowania), chciałbym wyświetlić pewne informacje na temat kroków pośrednich, które example_functionpodejmujesz. Może wtedy przepisałbym tę funkcję na coś takiego:
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
który teraz wywoływany z tymi samymi argumentami, co poprzednio, wyświetla następujące wyniki:
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
Osiąga to dokładnie to, co zamierzałem. Jest to jednak trochę sprzeczne z zasadą, że funkcja powinna robić tylko jedną rzecz, a teraz kod dlaexample_function jest nieco dłuższy i bardziej złożony. W przypadku tak prostej funkcji nie stanowi to problemu, ale w moim kontekście mam dość skomplikowane funkcje wywołujące się nawzajem, a instrukcje drukowania często wymagają bardziej skomplikowanych kroków niż pokazano tutaj, co powoduje znaczny wzrost złożoności mojego kodu (na przykład z moich funkcji było więcej linii kodu związanych z logowaniem niż linii związanych z jego faktycznym przeznaczeniem!).
Ponadto, jeśli później zdecyduję, że nie chcę już żadnych instrukcji drukowania w mojej funkcji, musiałbym ręcznie przejrzeć example_functioni usunąć wszystkie printinstrukcje, wraz ze wszystkimi zmiennymi związanymi z tą funkcją, co jest zarówno żmudne, jak i błędne -skłonny.
Sytuacja staje się jeszcze gorsza, jeśli chciałbym zawsze mieć możliwość drukowania lub nie drukowania podczas wykonywania funkcji, co prowadzi mnie do zadeklarowania dwóch bardzo podobnych funkcji (jednej z printinstrukcjami, drugiej bez), co jest straszne dla utrzymania, lub zdefiniować coś takiego:
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
co powoduje rozdętą i (miejmy nadzieję) niepotrzebnie skomplikowaną funkcję, nawet w naszym prostym przypadku example_function.
Pytanie
Czy istnieje pythonowy sposób na „oddzielenie” funkcji drukowania od oryginalnej funkcji example_function ?
Mówiąc bardziej ogólnie, czy istnieje pythonowy sposób na oddzielenie opcjonalnej funkcjonalności od głównego celu funkcji?
Co próbowałem do tej pory:
Rozwiązaniem, które znalazłem w tej chwili, jest użycie wywołań zwrotnych do oddzielenia. Na przykład można przepisać w example_functionten sposób:
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
a następnie zdefiniowanie funkcji zwrotnej, która wykonuje dowolną funkcję drukowania, którą chcę:
def print_callback(locals):
print(locals['number'])
i dzwoniąc w example_functionten sposób:
ns = [1, 3, 12]
example_function(ns, 3, callback=print_callback)
który następnie generuje:
0.5
0.25
0.125
1.5
0.75
0.375
6.0
3.0
1.5
2.0
To skutecznie oddziela funkcję drukowania od podstawowej funkcji example_function. Jednak głównym problemem związanym z tym podejściem jest to, że funkcję zwrotną można uruchomić tylko w określonej częściexample_function (w tym przypadku zaraz po zmniejszeniu o połowę bieżącego numeru), a całe drukowanie musi odbywać się właśnie tam. Czasami wymusza to dość skomplikowane projektowanie funkcji zwrotnej (i uniemożliwia osiągnięcie niektórych zachowań).
Na przykład, jeśli ktoś chciałby uzyskać dokładnie ten sam typ wydruku, co ja w poprzedniej części pytania (pokazując, który numer jest przetwarzany wraz z odpowiadającymi mu połówkami), wynikowe wywołanie zwrotne byłoby:
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)
co daje dokładnie taką samą wydajność jak poprzednio:
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
ale trudno jest pisać, czytać i debugować.
loggingmoduł mógłby tutaj pomóc. Mimo że moje pytanie używa printoświadczeń podczas konfigurowania kontekstu, tak naprawdę szukam rozwiązania, w jaki sposób oddzielić dowolny typ opcjonalnej funkcjonalności od głównego celu funkcji. Na przykład może chcę, aby funkcja wykreśliła rzeczy podczas działania. W takim przypadku uważam, że loggingmoduł nie miałby nawet zastosowania.
loggingpokazują sugestie użycia ), ale nie możemy oddzielić dowolnego kodu.
loggingmoduł python