Im letzten Beitrag (und Video) haben wir das Zufügen von Inhalten zu unserer Kivy App in einen eigenen Thread gepackt.
Darauf hin habe ich von euch ein paar Meldungen bekommen.
Die Fehlermeldung “Cannot create graphics instruction outside the main Kivy thread” trat auf und die App ist einfach abgestürzt.
Das können wir natürlich nicht so stehen lassen.
Kivy Serie
- Erste Schritte mit Fenstern
- Den Benutzer Willkommen heißen
- Die Verpackung zählt!
- Weitere Ansichten (Views)
- Scrollbare Liste mit Aktiendaten
- Aktiendaten API anbinden (yfinance)
- Fehler “Instruction outside the main Kivy thread”
- Element aus dem Layout entfernen
- Aktien in der Watchlist speichern
Warum habe ich den Fehler nicht?
Bei mir liefen Anaconda 3.9 und Kivy 2.0
Nachdem ich auf Standard Python 3.10 und Kivy 2.1 umgestiegen bin, kam es auch bei mir zu dem Fehler.
Anaconda muss für das eigene Repository teilweise Pakete anpassen. Ich vermute, hier kam es zu einer Änderung.
Oder der Versionssprung von Kivy 2.0 auf Kivy 2.1 hat eine Änderung eingeführt, die in den Fehler resultiert.
Ich habe mich nicht tiefer damit beschäftigt.
Woher kommt der Fehler?
Die Funktion query_stock_data() haben wir in einen eigenen Thread gepackt.
Sie läuft also unabhängig zu unserem Kivy Thread.
In der Funktion rufen wir wiederum add_ticker_row() auf und fügen damit neue Label zur Kivy App zu.
Und genau das gefällt Kivy nicht.
Damit greifen wir von “außen” auf den Kivy Thread zu und versuchen etwas an ihm zu ändern.
Kivy haut uns dann mit dem Cannot create graphics instruction outside the main Kivy thread auf die Finger.
Wie beheben wir das jetzt?
Die Lösung ist denkbar einfach: Wir müssen Kivy die Änderung vornehmen lassen
Kivy gibt uns mit der Klasse Clock eine Möglichkeit an die Hand.
Damit können wir von außen Funktionsaufrufe einplanen lassen.
Kivy kümmert sich dann darum, die Funktion einzuplanen und auszuführen.
Wir ersetzen also:
def query_stock_data(self):
symbol = self.ticker_input.text.strip()
stock = yfinance.Ticker(ticker=symbol)
data = stock.info
if 'regularMarketPrice' in data and data.get('regularMarketPrice') is not None:
price = f'{data.get("regularMarketPrice"):.2f} {data.get("currency")}'
self.add_ticker_row(data.get('symbol'), data.get('longName'), price)
else:
self.message.text = f'Für das angegebene Symbol {symbol} gab es kein Ergebnis.'
Mit einem Aufruf über Clock.schedule_once()
# an den Anfang der Datei zu den Importen
from functools import partial
def query_stock_data(self):
symbol = self.ticker_input.text.strip()
stock = yfinance.Ticker(ticker=symbol)
data = stock.info
if 'regularMarketPrice' in data and data.get('regularMarketPrice') is not None:
price = f'{data.get("regularMarketPrice"):.2f} {data.get("currency")}'
Clock.schedule_once(partial(self.add_ticker_row, data.get('symbol'), data.get('longName'), price), 0.5)
else:
self.message.text = f'Für das angegebene Symbol {symbol} gab es kein Ergebnis.'
Was passiert hier?
Die Funktion schedule_once() kennen wir schon aus dem Teil für weitere Ansichten.
Sie nimmt eine Referenz auf eine Funktion und einen timeout entgegen.
Damit gibst du an, welche Funktion du ausführen möchtest und wann sie ausgeführt werden soll.
Bei Funktionsreferenzen gibt es allerdings immer wieder das Problem, dass keine Parameter übergeben werden können.
Das haben wir hier auch. Unsere Funktion add_ticker_row() braucht die Parameter ticker, name, price.
Und für solche Fälle gibt uns Python das Modul functools mit der Funktion partial an die Hand.
Die Funktion partial benötigt als ersten Parameter die Funktionsreferenz. Alle weiteren Parameter werden dann als Parameter an diese Referenz übergeben.
Aus dem Paket baut uns partial dann wiederum eine Funktionsreferenz.
Klingt etwas kompliziert, aber im Grunde packt uns partial einfach nur die Funktionsreferenz mit allen benötigten Parametern zu einem hübschen Paket zusammen.
Und das Paket können wir dann wieder an schedule_once() übergeben.
Ein letzter Schritt
Ein letzter Schritt fehlt uns noch.
Clock.schedule_once() hat die Angewohnheit uns den timeout mit an die Funktion zu übergeben.
Das heißt so wie er jetzt geschrieben ist, fällt uns der Code wieder mit einer Exception auf die Nase:
TypeError: StockView.add_ticker_row() takes 4 positional arguments but 5 were given
Dagegen können wir nicht viel machen.
Als Lösung können wir einfach einen Sammelparameter zu unserer add_ticker_row() Funktion hinzufügen:
def add_ticker_row(self, ticker, name, price, *args):
Durch das *args werden jetzt alle weiteren Parameter einfach in einer Liste gesammelt.
Und schon ist auch der Error beseitigt.
Zusammenfassung
Wir dürfen den main Thread von Kivy nicht einfach von außen manipulieren.
Als Lösung liefert uns Kivy die Klasse Clock, mit der sich Aktionen durch Kivy einplanen und steuern lassen.
Die Funktion schedule_once in Verbindung mit functools.partial ermöglicht die Änderung von außen.
Ich hoffe, der Beitrag konnte dir erklären, warum es zu dem Problem kommt und wie du es lösen kannst.
Sollten trotzdem noch Fragen offen sein, schreib mir hier einfach einen Kommentar oder schau im Discord vorbei.
Ich freu mich auf dich 🙂
Ingo Janssen ist ein Softwareentwickler mit über 10 Jahren Erfahrung in der Leitung seines eigenen Unternehmens.
Er studierte Wirtschaftsinformatik an der TH Deggendorf und hat Softwareentwicklung an der FOM Hochschule in München unterrichtet.
Ingo hat mit einer Vielzahl von Unternehmen zusammengearbeitet, von kleinen und mittelständischen Unternehmen bis hin zu MDAX- und DAX-gelisteten Unternehmen.
Ingo ist leidenschaftlich daran interessiert, sein Wissen und seine Expertise mit anderen zu teilen. Aus diesem Grund betreibt er einen YouTube-Kanal mit Programmier-Tutorials und eine Discord-Community, in der Entwickler miteinander in Kontakt treten und voneinander lernen können.
Sie können Ingo auch auf LinkedIn, Xing und Gulp finden, wo er Updates über seine Arbeit teilt und Einblicke in die Tech-Branche gibt.
YouTube | Discord | LinkedIn | Xing | Gulp Profile