2016-02-03 44 views
5

Ich habe ein Problem, wo ich den Index eines Arrays an eine Funktion übergeben muss, die ich inline definieren. Die Funktion wird dann als Parameter an eine andere Funktion übergeben, die sie schließlich als Rückruf bezeichnet.Odd Threading-Verhalten in Python

Die Sache ist, wenn der Code aufgerufen wird, ist der Wert des Index alles falsch. Ich habe das schließlich gelöst, indem ich einen hässlichen Workaround erstellt habe, aber ich bin daran interessiert zu verstehen, was hier passiert. Ich habe ein minimales Beispiel, das Problem zu demonstrieren:

from __future__ import print_function 
import threading 


def works_as_expected(): 
    for i in range(10): 
     run_in_thread(lambda: print('the number is: {}'.format(i))) 

def not_as_expected(): 
    for i in range(10): 
     run_later_in_thread(lambda: print('the number is: {}'.format(i))) 

def run_in_thread(f): 
    threading.Thread(target=f).start() 

threads_to_run_later = [] 
def run_later_in_thread(f): 
    threads_to_run_later.append(threading.Thread(target=f)) 


print('this works as expected:\n') 
works_as_expected() 

print('\nthis does not work as expected:\n') 
not_as_expected() 
for t in threads_to_run_later: t.start() 

Hier ist die Ausgabe:

this works as expected: 

the number is: 0 
the number is: 1 
the number is: 2 
the number is: 3 
the number is: 4 
the number is: 6 
the number is: 7 
the number is: 7 
the number is: 8 
the number is: 9 

this does not work as expected: 

the number is: 9 
the number is: 9 
the number is: 9 
the number is: 9 
the number is: 9 
the number is: 9 
the number is: 9 
the number is: 9 
the number is: 9 
the number is: 9 

Kann jemand erklären, was hier geschieht? Ich nehme an, es hat damit zu tun, den Bereich oder etwas zu umschließen, aber eine Antwort mit einem Hinweis, der diese dunkle (für mich) Ecke des Python-Scopings erklärt, wäre für mich wertvoll.

Ich laufe dies auf Python 2.7.11

+1

Ich empfehle, einen Debugger [pdb] starten (https://docs.python.org/2/library/pdb.html) wird ausreichen, entfernen Sie die Arbeit als erwartet Teil des Codes und nur Schritt für Schritt durch die Ausführung Es wird dir sehr schnell klar machen, was genau passiert. –

+0

@TymoteuszPaul Ich glaube, du verstehst den Punkt meiner Frage falsch. Es ist nicht so, dass ich nicht verstehe, was der Code macht. Ich verstehe nicht, warum es so ist. Ich suche nach einer Antwort, die mir hilft zu verstehen, wie die * Sprache * tatsächlich funktioniert, damit ich besser über den Code nachdenken kann. Danke für die Anregung, ich mag pdb ziemlich. – Stephen

Antwort

2

Dies ist ein Ergebnis davon, wie Schließungen und Bereiche in Python arbeiten.

Was passiert ist, dass i im Rahmen der not_as_expected Funktion gebunden ist. Auch wenn Sie dem Thread eine lambda-Funktion zuführen, wird die Variable, die er verwendet, zwischen jedem Lambda und jedem Thread geteilt.

Betrachten Sie dieses Beispiel:

def make_function(): 
    i = 1 
    def inside_function(): 
     print i 
    i = 2 
    return inside_function 

f = make_function() 
f() 

Welche Zahl denken Sie, es gedruckt wird? Die i = 1 vor der Funktion wurde definiert oder die i = 2 nach?

Es wird der aktuelle Wert von i (d. H. 2) gedruckt werden. Es spielt keine Rolle, was der Wert von i war, als die Funktion gemacht wurde, es wird immer den aktuellen Wert verwenden. Das Gleiche passiert mit Ihren lambda Funktionen.

Auch in Ihrem erwarteten Ergebnisse Sie es nicht immer richtig funktioniert hat sehen können, übersprungen es 5 und 7 zwei Mal angezeigt. Was in diesem Fall passiert, ist, dass jedes Lambda normalerweise läuft, bevor die Schleife zur nächsten Iteration kommt. Aber in einigen Fällen (wie der 5) schafft die Schleife es, zwei Iterationen zu durchlaufen, bevor die Kontrolle an einen der anderen Threads übergeben wird, und i wird zweimal erhöht und eine Zahl wird übersprungen. In anderen Fällen (wie der 7) können zwei Threads ausgeführt werden, während sich die Schleife noch in der gleichen Iteration befindet, und da i zwischen den beiden Threads nicht geändert wird, wird derselbe Wert gedruckt.

Wenn Sie stattdessen tat dies:

def function_maker(i): 
    return lambda: print('the number is: {}'.format(i)) 

def not_as_expected(): 
    for i in range(10): 
     run_later_in_thread(function_maker(i)) 

Die i Variable wird innerhalb function_maker zusammen mit der lambda Funktion gebunden. Jede Lambda-Funktion referenziert eine andere Variable und funktioniert wie erwartet.

+0

Fantastisch. Ich habe nicht einmal bemerkt, dass die * erwarteten Ergebnisse * die '5' und '7' Anomalie hatten. – Stephen

1

Eine Schließung in Python erfasst die freien Variablen, nicht ihre aktuellen Werte zum Zeitpunkt der Erstellung der Schließung. Zum Beispiel:

def make_closures(): 
    L = [] 

    # Captures variable L 
    def push(x): 
     L.append(x) 
     return len(L) 

    # Captures the same variable 
    def pop(): 
     return L.pop() 

    return push, pop 

pushA, popA = make_closures() 
pushB, popB = make_closures() 

pushA(10); pushB(20); pushA(30); pushB(40) 
print(popA(), popA(), popB(), popB()) 

wird 30 angezeigt werden, 10, 40, 20: Dies geschieht, weil das erste Paar von Verschlüssen pushA, popA eine Liste bezieht L und das zweite Paar pushB, popB wird auf einem andere unabhängige Liste verweisen .

Der wichtige Punkt ist, dass in jedem Paar push und pop Verschlüsse auf die gleiche Liste beziehen, das heißt, sie erfasst den variableL und nicht den Wert von L zum Zeitpunkt der Erstellung. Wenn L durch einen Abschluss mutiert ist, sieht der andere die Änderungen.

Ein häufiger Fehler ist zum Beispiel erwarten, dass

L = [] 
for i in range(10): 
    L.append(lambda : i) 
for x in L: 
    print(x()) 

werden die Zahlen von 0 bis 9 anzuzeigen ... all die ungenannten Verschlüsse hier erfasst die gleiche Variable i Schleife verwendet und alle von ihnen werden Bei Aufruf den gleichen Wert zurückgeben.

Das gemeinsame Python Idiom, dieses Problem zu lösen, ist

L.append(lambda i=i: i) 

das heißt unter Verwendung der Tatsache, daß Standardwerte für die Parameter zu der Zeit ausgewertet werden die Funktion erstellt wird. Bei diesem Ansatz gibt jeder Abschluss einen anderen Wert zurück, da sie ihre private lokale Variable zurückgeben (einen Parameter, der einen Standardwert hat).