Kontext
Angenommen, ich habe den folgenden Python-Code:
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
Hier gehen Sie einfach jedes der Elemente in der ns
Liste durch und halbieren sie dreimal, während Sie die Ergebnisse akkumulieren. Die Ausgabe dieses Skripts lautet einfach:
2.0
Da 1 / (2 ^ 3) * (1 + 3 + 12) = 2.
Nehmen wir nun an, ich möchte (aus irgendeinem Grund, vielleicht zum Debuggen oder Protokollieren) Informationen zu den Zwischenschritten anzeigen, die example_function
das Unternehmen ausführt. Vielleicht würde ich diese Funktion dann in so etwas umschreiben:
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
Wenn jetzt mit denselben Argumenten wie zuvor aufgerufen wird, wird Folgendes ausgegeben:
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
Damit wird genau das erreicht, was ich beabsichtigt habe. Dies widerspricht jedoch ein wenig dem Prinzip, dass eine Funktion nur eines tun sollte, und jetzt ist der Code für example_function
etwas länger und komplexer. Für eine so einfache Funktion ist dies kein Problem, aber in meinem Kontext habe ich ziemlich komplizierte Funktionen, die sich gegenseitig aufrufen, und die Druckanweisungen umfassen oft kompliziertere Schritte als hier gezeigt, was zu einer erheblichen Zunahme der Komplexität meines Codes führt (zum einen) Von meinen Funktionen gab es mehr Codezeilen im Zusammenhang mit der Protokollierung als Zeilen im Zusammenhang mit dem eigentlichen Zweck!).
Wenn ich später beschließe, dass ich keine Druckanweisungen mehr in meiner Funktion haben möchte, müsste ich außerdem example_function
alle print
Anweisungen zusammen mit allen Variablen, die mit dieser Funktionalität zusammenhängen, manuell durchgehen und löschen. Dies ist ein Prozess, der sowohl langwierig als auch fehlerhaft ist -anfällig.
Die Situation wird noch schlimmer, wenn ich während der Funktionsausführung immer die Möglichkeit haben möchte, zu drucken oder nicht zu drucken, was mich dazu veranlasst, entweder zwei extrem ähnliche Funktionen zu deklarieren (eine mit den print
Anweisungen, eine ohne), was für die Wartung schrecklich ist, oder etwas definieren wie:
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
was zu einer aufgeblähten und (hoffentlich) unnötig komplizierten Funktion führt, selbst im einfachen Fall unserer example_function
.
Frage
Gibt es eine pythonische Möglichkeit, die Druckfunktionalität von der ursprünglichen Funktionalität der zu "entkoppeln"? example_function
?
Gibt es allgemein eine pythonische Möglichkeit, optionale Funktionen vom Hauptzweck einer Funktion zu entkoppeln?
Was ich bisher versucht habe:
Die Lösung, die ich im Moment gefunden habe, besteht darin, Rückrufe für die Entkopplung zu verwenden. Zum Beispiel kann man Folgendes umschreiben 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
und dann Definieren einer Rückruffunktion, die die gewünschte Druckfunktion ausführt:
def print_callback(locals):
print(locals['number'])
und so anrufen example_function
:
ns = [1, 3, 12]
example_function(ns, 3, callback=print_callback)
welches dann ausgibt:
0.5
0.25
0.125
1.5
0.75
0.375
6.0
3.0
1.5
2.0
Dadurch wird die Druckfunktionalität erfolgreich von der Basisfunktionalität von entkoppelt example_function
. Das Hauptproblem bei diesem Ansatz besteht jedoch darin, dass die Rückruffunktion nur an einem bestimmten Teil der ausgeführt werden kannexample_function
(in diesem Fall direkt nach der Halbierung der aktuellen Nummer) ausgeführt werden kann und der gesamte Druck genau dort stattfinden muss. Dies erzwingt manchmal, dass das Design der Rückruffunktion ziemlich kompliziert ist (und macht es unmöglich, einige Verhaltensweisen zu erreichen).
Wenn man zum Beispiel genau die gleiche Art des Druckens wie in einem vorherigen Teil der Frage erzielen möchte (wobei angegeben wird, welche Nummer verarbeitet wird, zusammen mit den entsprechenden Halbierungen), wäre der resultierende Rückruf:
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)
was zu genau der gleichen Ausgabe wie zuvor führt:
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
Aber es ist ein Schmerz zu schreiben, zu lesen und zu debuggen.
logging
logging
Modul hier helfen würde. Obwohl meine Frageprint
beim Einrichten des Kontexts Anweisungen verwendet , suche ich tatsächlich nach einer Lösung, wie jede Art von optionaler Funktionalität vom Hauptzweck einer Funktion entkoppelt werden kann. Zum Beispiel möchte ich vielleicht, dass eine Funktion Dinge zeichnet, während sie ausgeführt wird. In diesem Fall glaube ich, dass daslogging
Modul nicht einmal anwendbar wäre.logging
zeigen), aber nicht, wie beliebiger Code getrennt werden kann.Antworten:
Wenn Sie Funktionen außerhalb der Funktion benötigen, um Daten innerhalb der Funktion zu verwenden, muss innerhalb der Funktion ein Nachrichtensystem vorhanden sein, das dies unterstützt. Daran führt kein Weg vorbei. Lokale Variablen in Funktionen sind von außen vollständig isoliert.
Das Protokollierungsmodul ist ziemlich gut darin, ein Nachrichtensystem einzurichten. Es ist nicht nur auf das Ausdrucken der Protokollnachrichten beschränkt - mit benutzerdefinierten Handlern können Sie alles tun.
Das Hinzufügen eines Nachrichtensystems ähnelt Ihrem Rückrufbeispiel, mit der Ausnahme, dass die Stellen, an denen die "Rückrufe" (Protokollierungshandler) verarbeitet werden, an einer beliebigen Stelle im
example_function
(durch Senden der Nachrichten an den Protokollierer) angegeben werden können. Alle Variablen, die von den Protokollierungshandlern benötigt werden, können beim Senden der Nachricht angegeben werden (Sie können sie weiterhin verwendenlocals()
, es ist jedoch am besten, die benötigten Variablen explizit zu deklarieren).Ein neues
example_function
könnte aussehen wie:Dies gibt drei Orte an, an denen die Nachrichten verarbeitet werden könnten. Dies allein
example_function
wird nichts anderes als die Funktionalität des Selbstexample_function
bewirken. Es wird nichts ausgedruckt oder andere Funktionen ausgeführt.Um dem Funktionen zusätzliche Funktionen
example_function
hinzuzufügen, müssen Sie dem Logger Handler hinzufügen.Wenn Sie beispielsweise die gesendeten Variablen ausdrucken möchten (ähnlich wie in Ihrem
debugging
Beispiel), definieren Sie den benutzerdefinierten Handler und fügen ihn demexample_function
Logger hinzu:Wenn Sie die Ergebnisse in einem Diagramm darstellen möchten, definieren Sie einfach einen anderen Handler:
Sie können beliebige Handler definieren und hinzufügen. Sie sind völlig unabhängig von der Funktionalität der
example_function
und können nur die Variablen verwenden, dieexample_function
sie ihnen geben.Obwohl die Protokollierung als Messagingsystem verwendet werden kann, ist es möglicherweise besser, auf ein vollwertiges Messagingsystem wie PyPubSub umzusteigen , damit die tatsächliche Protokollierung, die Sie möglicherweise durchführen , nicht beeinträchtigt wird:
quelle
logging
Modul bereitgestellt haben, ist in der Tat besser organisiert und wartbarer als der von mir vorgeschlagene Codeprint
und dieif
Anweisungen. Die Druckfunktionalität wird jedoch nicht von der Hauptfunktionalität der Funktion entkoppeltexample_function
. Das heißt, das Hauptproblem,example_function
zwei Dinge gleichzeitig erledigt zu haben, bleibt bestehen, was den Code komplizierter macht, als ich es gerne hätte.example_function
jetzt nur noch eine Funktion, und das Drucken (oder eine andere Funktion, die wir gerne hätten) findet außerhalb davon statt.example_function
ist von der Druckfunktion entkoppelt - die einzige zusätzliche Funktion, die der Funktion hinzugefügt wird, ist das Senden der Nachrichten. Es ähnelt Ihrem Rückrufbeispiel, sendet jedoch nur bestimmte Variablen, die Sie möchten, und nicht allelocals()
. Es liegt an den Protokollhandlern (die Sie an einer anderen Stelle an den Protokollierer anhängen), die zusätzlichen Funktionen (Drucken, Zeichnen usw.) auszuführen. Sie müssen überhaupt keine Handler anhängen. In diesem Fall passiert beim Senden der Nachrichten nichts. Ich habe meinen Beitrag aktualisiert, um dies klarer zu machen.example_function
. Vielen Dank, dass Sie es jetzt besonders deutlich gemacht haben! Diese Antwort gefällt mir sehr gut. Der einzige Preis, der gezahlt wird, ist die zusätzliche Komplexität der Nachrichtenübermittlung, die, wie Sie bereits erwähnt haben, unvermeidlich zu sein scheint. Vielen Dank auch für den Verweis auf PyPubSub, der mich dazu brachte, das Beobachtermuster nachzulesen .Wenn Sie sich nur an Druckanweisungen halten möchten, können Sie einen Dekorateur verwenden, der ein Argument hinzufügt, mit dem der Druck zur Konsole ein- und ausgeschaltet wird.
Hier ist ein Dekorateur, der
verbose=False
jeder Funktion das Nur- Schlüsselwort-Argument und den Standardwert hinzufügt und die Dokumentzeichenfolge und Signatur aktualisiert. Wenn Sie die Funktion unverändert aufrufen, wird die erwartete Ausgabe zurückgegeben. Wenn Sie die Funktion mitverbose=True
aufrufen, werden die print-Anweisungen aktiviert und die erwartete Ausgabe zurückgegeben. Dies hat den zusätzlichen Vorteil, dass nicht jedem Druck einif debug:
Block vorangestellt werden muss .Wenn Sie Ihre Funktion jetzt einpacken, können Sie die Druckfunktionen mithilfe von aktivieren / deaktivieren
verbose
.Beispiele:
Bei der Inspektion
example_function
wird auch die aktualisierte Dokumentation angezeigt. Da Ihre Funktion keine Dokumentzeichenfolge hat, ist es genau das, was sich im Dekorator befindet.In Bezug auf die Codierungsphilosophie. Eine Funktion zu haben, die keine Nebenwirkungen hat, ist ein funktionales Programmierparadigma. Python kann eine funktionale Sprache sein, ist jedoch nicht ausschließlich so konzipiert. Ich gestalte meinen Code immer mit Blick auf den Benutzer.
Wenn das Hinzufügen der Option zum Drucken der Berechnungsschritte für den Benutzer von Vorteil ist, ist daran NICHTS falsch. Vom Standpunkt des Designs aus werden Sie nicht mehr in der Lage sein, die Druck- / Protokollierungsbefehle irgendwo hinzuzufügen.
quelle
print
undif
Anweisungen enthält. Darüber hinaus gelingt es ihm, einen Teil der Funktionalität des Druckens tatsächlich von der Hauptfunktionalität zu entkoppelnexample_function
, was sehr schön war (mir hat auch gefallen, dass der Dekorateur automatisch an die Dokumentenkette anhängt, nette Geste). Die Druckfunktionalität wird jedoch nicht vollständig von der Hauptfunktionalität entkoppeltexample_function
: Sie müssen dieprint
Anweisungen und die zugehörige Logik noch zum Funktionskörper hinzufügen .example_function
Körpers des Körpers befinden, damit seine Komplexität nur mit der Komplexität seiner Hauptfunktionalität verbunden bleibt. In meiner realen Anwendung von all dem habe ich eine Hauptfunktion, die bereits sehr komplex ist. Das Hinzufügen von Druck- / Plot- / Protokollierungsanweisungen zu seinem Körper macht ihn zu einem Biest, dessen Wartung und Fehlerbehebung ziemlich schwierig war.Sie können eine Funktion definieren, die die
debug_mode
Bedingung kapselt , und die gewünschte optionale Funktion und ihre Argumente an diese Funktion übergeben (wie hier vorgeschlagen ):Beachten Sie, dass
debug_mode
vor dem Aufruf offensichtlich ein Wert zugewiesen worden sein mussDEBUG
.Es ist natürlich möglich, andere Funktionen als aufzurufen
print
.Sie können dieses Konzept auch auf mehrere Debug-Ebenen erweitern, indem Sie einen numerischen Wert für verwenden
debug_mode
.quelle
if
Aussagen überall überflüssig und erleichtert auch das Ein- und Ausschalten des Druckvorgangs. Es entkoppelt jedoch nicht die Druckfunktionalität von der Hauptfunktionalität vonexample_function
. Vergleichen Sie dies zum Beispiel mit meinem Rückrufvorschlag. Bei Verwendung von Rückrufen verfügt example_function nur noch über eine Funktion, und das Druckmaterial (oder eine andere Funktion, die wir gerne hätten) wird außerhalb der Funktion ausgeführt.Ich habe meine Antwort mit einer Vereinfachung aktualisiert: Der Funktion
example_function
wird ein einzelner Rückruf oder Hook mit einem Standardwert übergeben, sodassexample_function
nicht mehr getestet werden muss, ob sie übergeben wurde oder nicht:Das Obige ist ein Lambda-Ausdruck, der diesen Standardwert für eine beliebige Kombination von Positions- und Schlüsselwortparametern an verschiedenen Stellen innerhalb der Funktion zurückgibt
None
undexample_function
aufrufen kannhook
.Im folgenden Beispiel interessieren mich nur die
"end_iteration"
und"result
"Ereignisse.Drucke:
Die Hook-Funktion kann so einfach oder so aufwendig sein, wie Sie möchten. Hier wird der Ereignistyp überprüft und einfach gedruckt. Es könnte jedoch eine
logger
Instanz erhalten und die Nachricht protokollieren. Sie können die Fülle der Protokollierung nutzen, wenn Sie sie benötigen, aber die Einfachheit, wenn Sie dies nicht tun.quelle
example_function
.if
Anweisungen zu entfernen :)