Nicht immer ist es möglich oder sinnvoll Anwendungen „aus einem Guss“ bzw. in einer Programmiersprache zu entwickeln oder zu betreiben. Unabhängig vom Konzept der Modularisierung, sollte, meiner Meinung nach, auch immer der Ansatz der „Best Available Technology (BAT)“ (im Deutschen wohl am besten zu übersetzen mit „Stand der Technik“) berücksichtigt werden. Viel zu häufig wird – gerade bei Konzeptionierung neuer Anwendungen – die Wahl der verwendeten Sprachen, Frameworks und Techniken darauf basierend getroffen, welche Kenntnisse der jeweilige Dienstleister anbieten kann und nicht welche Auswahl für die Lösung einer bestimmten Fragestellung am sinnvollsten wäre. Auch kann die Kombination relativ junger Technologien mit bereits etablierten Frameworks eine Herausforderung darstellen. Dazu ein konkretes Beispiel:
Eine bereits existierende, auf .net basierende Anwendung soll um Features aus dem Bereich Deep Learning erweitert werden. Neben den .net-Entwicklern, arbeiten an diesem Projekt natürlich auch Data Scientists mit, die u.a. für die Entwicklung der künstlichen neuronalen Netzwerke zuständig sind – dies aber natürlich in Python tun. Die Aufgabe besteht nun darin diese beiden Technologien zu kombinieren. Was also tun?
Option 1: Warum (überhaupt) Python?
Mit ML.Net steht ein Framework aus dem Hause Microsoft zur Verfügung, mit dessen Hilfe sich verhältnismäßig einfach machine learning-Lösungen in C# erstellen lassen. Die Daten-Wissenschaftler würden also (versuchen) ihre Konzepte in einer Sprache zu realisieren, die eine Integration unnötig macht und ggf. im Rahmen eines kompilierten Assemblies recht einfach in die bestehende Anwendung integriert werden kann.
Unabhängig davon ob sich die betroffene Fachgruppe darüber freuen würde bei der Wahl der zu verwendenden Tools eingeschränkt zu werden, gibt es einige Gründe warum sich Python und damit verbundene Frameworks im Bereich machine learning durchgesetzt haben. Mit dieser Lösung würde man sich ggf. einen Sonderfall ins Haus holen, bei dem man nur mit deutlich erhöhtem Aufwand auf dem aktuellen Stand der Technik bleiben und somit gegen den BAT-Grundsatz verstoßen.
Option 2: Python in .net interpretieren
Es gibt eine Reihe recht guter Projekte, wie z.B. Python.net oder IronPython, die Bibliotheken bereitstellen um Python-Skripe in .net-Anwendungen auszuführen und zu interpretieren. Mithilfe verschiedener Injection-Szenarien ist es sogar möglich Variablen und Funktionen bereitzustellen, die üblicherweise nicht Bestandteil der (Core-)Python-Umgebung sind.
Leider kommen diese Interpreter gerade bei wissenschaftlichen Bibliotheken an ihre Grenzen. Um z.B. aufwändige mathematische Operationen oder (wissenschaftliche) Berechnungen durchführen zu können, stellen Frameworks wie Anaconda zusätzliche Funktionen und Bibliotheken bereit, die bei den genannten Interpretern nicht zur Verfügung stehen. Somit stellt dies nur eine engschränkt nutzbare Lösung dar.
Option 3: Die Python-Skripte als externe Prozesse ausführen
In einem Artikel in der dotnetpro vom März 2019 wird – neben einem Beispiel für die Verwendung von IronPython – genau dieses Szenario beschrieben, mit dem Lösungsansatz das Python-Skript mithilfe der .net-eigenen Process-Klasse aufzurufen, die Ausgabe zu überwachen und in der Core-Anwendung abzuwarten bis das Skript vollständig durchlaufen wurde bis die entstandenen Daten weiterverarbeitet werden.
Natürlich funktioniert dies nicht nur für Python-Skripte, sondern für alle Arten externer Skripte und Anwendungen – und das ganz ohne z.B. tiefgreifende Anpassungen an der Original-Anwendung durchführen zu müssen. Möchte man jedoch nicht jedes mal aufs Neue
- die Process-Parameter, inkl. ggf. notwendiger für das externe Skript oder die Anwendung notwendige Kommandozeilenparameter, konfigurieren
- den Ablauf überwachen und auf auftretende Output- oder Fehler-Events reagieren
empfiehlt sich die Kapselung in eine dafür optimierte Klasse.
Mit der ManagedProcess-Klasse stellt das aktuelle Release (1.4.1) des BYTES.NET-Frameworks genau eine derartige Kapselung bereit.
Private Async Sub RunScript() 'create the process class instance Dim process As ManagedProcess = New ManagedProcess With process .Executable = "C:\Windows\System32\cmd.exe" .Arguments = {"/c " & """" & "%InstallationDir%..\..\..\..\samples\Scripts\LongRunningSampleScript.cmd" & """" & " TestValue"} .ShowUI = Me.ShowUI End With 'handle the events AddHandler _process.OutputReceived, Sub(data As LogEntry) _log.Add(data) End Sub AddHandler _process.Exited, Sub(code As Integer) MsgBox("Process finished with: " & code) End Sub Await _process.RunAsync() End Function
Mithilfe weniger Zeilen Code ist es z.B. möglich ein Skript (mithilfe eines beliebigen externen Interpreters) auszuführen und sowohl auf Rückgaben zu reagieren als auch auf den Abschluss des Runs zu warten. Zudem bietet die Klasse die Vorteile, dass
- mit RunAsync bereits eine Methode bereitgestellt wird, die die Nutzeroberfläche während (eines beliebig langen) Durchlaufs nicht blockiert
- ein einheitliches Event (OutputReceived) zur Verfügung steht, dass die Verarbeitung der Rückgaben ermöglicht; Fehler können mithilfe des InformationLevel des zurückgegebenen LogEntry-Objekts identifiziert werden
- relative Pfade – z.B. mithilfe der generischen %InstallationDir%-Variable – für den Interpreter-Pfad unterstützt werden, so dass auch Interpreter wie z.B. Anaconda in einer portable-Version (z.B. als Bestandteil eines Installationspakets) genutzt werden können
Ein detailliertes Beispiel für den Einsatz mit Windows-Batch (*.cmd) und Python (*.py) ist im zugehörigen Repository auf Github verfügbar.