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_function
tutaj po prostu przeglądam każdy z elementów na ns
liś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_function
podejmujesz. 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_function
i usunąć wszystkie print
instrukcje, 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 print
instrukcjami, 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_function
ten 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_function
ten 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ć.
logging
moduł pythonlogging
moduł mógłby tutaj pomóc. Mimo że moje pytanie używaprint
oś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, żelogging
moduł nie miałby nawet zastosowania.logging
pokazują sugestie użycia ), ale nie możemy oddzielić dowolnego kodu.Odpowiedzi:
Jeśli potrzebujesz funkcji spoza funkcji, aby korzystać z danych z wnętrza funkcji, musi istnieć jakiś system przesyłania wiadomości wewnątrz funkcji, aby to obsługiwać. Nie można tego obejść. Zmienne lokalne w funkcjach są całkowicie odizolowane od zewnątrz.
Moduł logowania jest całkiem dobry w konfigurowaniu systemu komunikatów. Nie ogranicza się tylko do drukowania komunikatów dziennika - za pomocą niestandardowych programów obsługi możesz zrobić wszystko.
Dodanie systemu komunikatów jest podobne do przykładu wywołania zwrotnego, z tym wyjątkiem, że miejsca, w których obsługiwane są „wywołania zwrotne” (procedury obsługi rejestrowania), można określić w dowolnym miejscu
example_function
(wysyłając wiadomości do programu rejestrującego). Wszelkie zmienne, które są wymagane przez procedury obsługi rejestrowania, można określić podczas wysyłania wiadomości (nadal można jej używać)locals()
, ale najlepiej jest jawnie zadeklarować potrzebne zmienne).Nowy
example_function
może wyglądać następująco:Określa trzy lokalizacje, w których można obsługiwać wiadomości. Na własną rękę, to
example_function
nie będzie coś innego niż funkcjonalności zrobićexample_function
sama. Nie wydrukuje niczego ani nie wykona żadnej innej funkcji.Aby dodać dodatkową funkcjonalność do
example_function
, musisz dodać programy obsługi do rejestratora.Na przykład, jeśli chcesz wydrukować wysłane zmienne (podobnie jak w twoim
debugging
przykładzie), zdefiniuj niestandardową procedurę obsługi i dodaj ją do programuexample_function
rejestrującego:Jeśli chcesz wykreślić wyniki na wykresie, po prostu zdefiniuj inny moduł obsługi:
Możesz zdefiniować i dodać dowolne programy obsługi. Będą one całkowicie oddzielone od funkcji
example_function
i będą mogły używać tylko zmiennych, któreexample_function
im daje.Chociaż rejestrowania można używać jako systemu przesyłania wiadomości, lepiej jest przejść do w pełni rozwiniętego systemu przesyłania wiadomości, takiego jak PyPubSub , aby nie zakłócał żadnego rzeczywistego rejestrowania:
źródło
logging
modułu jest rzeczywiście bardziej zorganizowany i łatwiejszy w utrzymaniu niż to, co zaproponowałem przy użyciuprint
iif
instrukcji. Jednak nie oddziela funkcji drukowania od głównej funkcjonalnościexample_function
funkcji. Oznacza to, że główny problemexample_function
zrobienia dwóch rzeczy naraz wciąż pozostaje, co sprawia, że jego kod jest bardziej skomplikowany niż chciałbym.example_function
ma teraz tylko jedną funkcję, a drukowanie (lub jakakolwiek inna funkcja, którą chcielibyśmy mieć) dzieje się poza nią.example_function
jest oddzielony od funkcji drukowania - jedyną dodatkową funkcją tej funkcji jest wysyłanie wiadomości. Jest podobny do przykładu wywołania zwrotnego, z tym wyjątkiem, że wysyła tylko określone zmienne, które chcesz, a nie wszystkielocals()
. Dodatkowe funkcje (drukowanie, tworzenie wykresów itp.) Należy do programów obsługi dziennika (które podłączasz do rejestratora w innym miejscu). W ogóle nie musisz dołączać żadnych programów obsługi, w którym to przypadku nic się nie stanie po wysłaniu wiadomości. Zaktualizowałem mój post, aby to wyjaśnić.example_function
. Dzięki za wyjaśnienie teraz! Naprawdę podoba mi się ta odpowiedź, jedyną płaconą ceną jest dodatkowa złożoność przekazywania wiadomości, która, jak wspomniałeś, wydaje się nieunikniona. Dziękuję również za odniesienie do PyPubSub, które doprowadziło mnie do odczytania wzorca obserwatora .Jeśli chcesz trzymać się tylko instrukcji drukowania, możesz użyć dekoratora, który dodaje argument, który włącza / wyłącza drukowanie na konsoli.
Oto dekorator, który dodaje argument tylko do słowa kluczowego i domyślną wartość
verbose=False
dowolnej funkcji, aktualizuje dokumentację i podpis. Wywołanie funkcji „tak jak jest” zwraca oczekiwane wyjście. Wywołanie funkcji zverbose=True
spowoduje włączenie instrukcji print i zwrócenie oczekiwanego wyniku. Ma to tę dodatkową zaletę, że nie trzeba poprzedzać każdego wydrukuif debug:
blokiem.Zawijanie funkcji umożliwia teraz włączanie / wyłączanie funkcji drukowania za pomocą
verbose
.Przykłady:
Podczas inspekcji
example_function
zobaczysz również zaktualizowaną dokumentację. Ponieważ twoja funkcja nie ma dokumentów, jest to tylko to, co jest w dekoratorze.Pod względem filozofii kodowania. Funkcjonowanie, które nie powoduje efektów ubocznych, jest funkcjonalnym paradygmatem programowania. Python może być językiem funkcjonalnym, ale nie został zaprojektowany wyłącznie w taki sposób. Zawsze projektuję kod z myślą o użytkowniku.
Jeśli dodanie opcji drukowania kroków obliczeniowych jest korzyścią dla użytkownika, to nie ma w tym nic złego. Z punktu widzenia projektowania, utkniesz z dodawaniem gdzieś poleceń drukowania / logowania.
źródło
print
iif
oświadczeń. Co więcej, udaje mu się oddzielić część funkcjonalności drukowania odexample_function
głównej funkcjonalności, co było bardzo miłe (podobało mi się również, że dekorator automatycznie dołącza się do docstring, miły akcent). Jednak nie oddziela w pełni funkcji drukowania od głównej funkcjonalnościexample_function
: nadal musisz dodaćprint
instrukcje i wszelką towarzyszącą logikę do ciała funkcji.example_function
ciałem, aby jego złożoność pozostała jedynie związana ze złożonością jego głównej funkcjonalności. W moim rzeczywistym zastosowaniu tego wszystkiego mam główną funkcję, która jest już znacznie złożona. Dodanie instrukcji drukowania / kreślenia / rejestrowania do jego ciała sprawia, że staje się bestią, której utrzymanie i debugowanie jest dość trudne.Możesz zdefiniować funkcję enkapsulującą
debug_mode
warunek i przekazać żądaną funkcję opcjonalną i jej argumenty do tej funkcji (zgodnie z sugestią tutaj ):Pamiętaj, że
debug_mode
oczywiście przed wywołaniem musiała zostać mu przypisana wartośćDEBUG
.Oczywiście można wywoływać funkcje inne niż
print
.Możesz także rozszerzyć tę koncepcję na kilka poziomów debugowania, używając wartości liczbowej dla
debug_mode
.źródło
if
wyciągów w dowolnym miejscu, a także ułatwia włączanie i wyłączanie drukowania. Jednak nie oddziela funkcji drukowania od głównej funkcjonalnościexample_function
. Porównaj to na przykład z moją sugestią oddzwonienia. Korzystając z wywołań zwrotnych, funkcja example_funkcja ma teraz tylko jedną funkcję, a drukowanie (lub jakakolwiek inna funkcja, którą chcielibyśmy mieć) dzieje się poza nią.Zaktualizowałem moją odpowiedź z uproszczeniem: funkcja
example_function
przechodzi pojedyncze wywołanie zwrotne lub przechwytuje z wartością domyślną, tak żeexample_function
nie trzeba już sprawdzać, czy została ona przekazana, czy nie:Powyższe jest wyrażeniem lambda, które zwraca
None
iexample_function
może wywoływać tę wartość domyślną dlahook
dowolnej kombinacji parametrów pozycyjnych i słów kluczowych w różnych miejscach funkcji.W poniższym przykładzie interesują mnie tylko wydarzenia „
"end_iteration"
i"result
”.Wydruki:
Funkcja zaczepu może być tak prosta lub tak rozbudowana, jak tylko chcesz. Tutaj sprawdza typ zdarzenia i wykonuje prosty wydruk. Ale może uzyskać
logger
instancję i zapisać komunikat. Możesz mieć całe bogactwo logowania, jeśli potrzebujesz, ale prostotę, jeśli nie.źródło
example_function
.if
wyciągów :)