comPYner
Warum comPYner?
Wir benutzen SPIKE Legacy/v2 als Firmware für unseren Wettbewerbsroboter. Dies bringt viele Vorteile, aber auch einige Nachteile mit sich.
Eines der größten Probleme mit der offiziellen LEGO-Firmware ist, dass wir nur eine Datei hochladen können. Den Code tatsächlich in eine einzelne Datei zu schreiben, macht Wartung und Entwicklung sehr mühsam.
Bisher hatten wir die (inoffizielle) VSCode Extension für SPIKE Prime verwendet, und natürlich waren wir nicht die einzigen mit diesem Problem. So gab es zum Beispiel einen PR (#58) vom FLL-Team GreenSubMarine, indem die Funktion hinzugefügt wurde, mit from <file> import *
den Inhalt von <file>
an dieser Stelle einzufügen, sodass man den Code in mehrere Dateien aufteilen kann, die später zu einer einzelnen zusammengeflickt werden.
Während das die Situation bereits deutlich verbessert, wurden wir durch diesen PR dazu angeregt, das Ganze zu verbessern. Wer schon länger Python programmiert, weiß, dass ein Start-Import vermieden werden sollte, damit man Funktionen nicht überschreibt und keine Namespace-Pollution betreibt. Was wäre also, wenn wir die anderen Import-Typen ermöglichen?
Wie funktioniert comPYner?
ComPYner zu Entwickeln hat mehrere Ansätze gebraucht. Die meisten haben in CPython wunderbar funktioniert, allerdings nicht auf dem Prime Hub.
Wir schauen uns die verschiedenen Wege an diesem Beispiel an:
# module.py
def greet(name: str):
print("Hello", name)
# main.py
import module
module.greet("comPYner")
1. Funktionen
Eine Idee war es, den Inhalt jeder importierten Datei in eine Funktion zu schreiben. diese würde dann locals()
zurückgeben. So könnte man dann zum importieren die Funktion aufrufen und das Ergebnis in einer Variable speichern. So würde das obenstehende Beispiel umgewandelt werden.
def _import_module():
def greet(name: str)name):
print("Hello", name)
return locals()
def _import_main():
module = module()
module.greet("comPYner")
_import_main()
Sowohl in der Theorie, als auch in CPython funktioniert das — jedoch nicht in Micropython. In Micropython gibt locals()
immer {}
zurück, da die Namen lokaler Variablen nicht gespeichert werden.
2. Zurücksetzen des globalen Namespace
Weiterhin war eine Überlegung, jede Datei hintereinander zu schreiben, globals()
zu speichern und danach alle Variablen in globals()
zu löschen. Das hätte so ausgesehen:
_modules = {}
def greet(name: str)name):
print("Hello", name)
_module = globals()
for key in _module:
if _key == "_modules":
continue
delete globals()[key]
_modules["module"] = _module
module = module()
module.greet("comPYner")
_module = globals()
for key in _module:
if _key == "_modules":
continue
delete globals()[key]
_modules["main"] = _module
Dieser Ansatz funktioniert jedoch nicht einmal in CPython, da alle globalen Variablen einer jeden Datei nur beim initialisieren verfügbar sind. Zu dem Zeitpunkt, wo greet()
aufgerufen wird, sind alle anderen Variablen in module.py
bereits gelöscht. Würde greet()
z.B. eine andere Funktion aufrufen, die ebenfalls in module.py
liegt, wäre diese zum Zeitpunkt des Aufrufs bereits gelöscht und nurnoch unter _modules["module"][name]
zu finden.
3. Folgende Versuche
Darauffolgende Versuche waren darauf konzentriert, die Probleme von 2. zu lösen, ohne viel grundlegend zu ändern. Das Problem war hier, das es mit solchen halbherzigen Lösungen wie in Funktionen globale Variablen durch etwas wie _modules["module"][name]
auszutauschen sehr schwierig ist das Verhalten von CPython zu reproduzieren. Es gab hier viele Probleme, auf die ich jedoch nicht weiter eingehe.
4. Wie es jetzt funktioniert
[WIP]