Beachten Sie, dass der Inhaltseditor einen Wagenrücklauf aus dem Inhalt von defs.py entfernt hat und wie folgt hätte angezeigt werden sollen:
def f(x):
return x*x
Über die Feiertage hatte ich Zeit, an Python zu basteln. Das Problem, auf das wir bei der parallelen Verarbeitung von der Python/JSL-Schnittstelle gestoßen sind, ist, dass Python normalerweise mehrere Kopien von JMP startet, nicht Python. Der Grund dafür ist, dass wir Python in JMP einbetten, sodass die ausführbare Datei JMP anstelle von Python ist. Ich habe entdeckt, dass das Multiprocessing-Paket es ermöglicht, die ausführbare Datei so einzustellen, dass sie beim Ausführen des parallelen Codes gestartet wird. Dies ist das fehlende Puzzleteil.
Das folgende Skript habe ich auf JMP 15.2.1 auf Mac und Windows mit Python 3.9 getestet. Auf meinem Windows-Rechner musste ich das Path()-Argument für Python Init() verwenden. Ich habe mehrere Python-Versionen und möchte sicherstellen, dass die richtige ausgewählt wird. Auf meinem Mac funktioniert Python Init() ohne Argumente für meine Konfiguration gut. Es gibt ein Problem, auf das ich unter Windows gestoßen bin, ich musste den Pfad zu meiner pythonw.exe hartcodieren. Sie müssen dies ändern, damit es Ihrem Python-Standort entspricht. Suche nach FixMe:
Abgesehen von den Python-Init()-Parametern und dem Pfad zu pythonw.exe unter Windows sollte dies problemlos unter JMP 15.2.1 und neuer auf Mac oder Windows ausgeführt werden. Kann auf älteren Versionen von JMP ausgeführt werden, die Python-Unterstützung bieten, dies wurde jedoch nicht getestet.
Sie müssen eine defs.py-Datei im selben Verzeichnis wie dieses Skript erstellen. Das Multiprocessing-Paket erfordert, dass der Code, der parallel ausgeführt werden soll, aus importierbarem Code stammt, nicht aus lokalem Code. Die meiste Arbeit in diesem Skript bestand also darin, sicherzustellen, dass sys.path richtig eingerichtet ist, damit die Datei defs.py einfach vom Skript importiert werden kann.
Der gesamte Inhalt der Datei defs.py befindet sich in den Dateikommentaren, was das Einfügen in eine neue Datei erleichtert. Der unten duplizierte Inhalt ist:
def f(x):
return x*x
Das Skript nimmt eine Liste von Zahlen [1,2,3] und ruft parallel f(x) auf, um den Wert zu quadrieren, der die Ergebnisse liefert [1,4,9].
pyParallel.jsl
Names Default To Here( 1 );
/* Description: Python mulltiprocessing from JMP JSL
* File: pyParallel.jsl, and defs.py
* Author: Paul Nelson
* JMP Statistical Discovery, LLC
*
* Python multiprocessing from an embedded context adds additional challenges.
* Just attempting to run Python parallel processing modules, typically launches
* multiple copies of JMP, not executing the Python code in parallel.
*
* The multiprocessing module allows setting the executable to launch, since we
* are loading Python into JMP, it's an embedded case thus sys.executable => JMP, not Python.
*
*
* Note: Functions to be run in parallel must come from importable code, not local
* code definitions.
* A multiprocessing 'Feature', not bug, See: https://bugs.python.org/issue25053
*
* Known Bugs:
* On Windows within JMP, sys.exec_prefix is empty, so path to pythonw.exe has to be set explicitly
*
* Create a defs.py file with the contents below. Place in the same directory as this JSL file.
def f(x):
return x*x
*/
// On my windows machine since I have multiple versions of Python, I have to specify the
// full path to the python39.dll, on my Mac it just works.
If( Host Is("Windows"),
Python Init(Path("C:\\Users\\panels\\AppData\\Local\\Programs\\Python\\Python39\\python39.dll")),
Python Init()
);
// send the current working directory where we loaded this script, down to Python so we can find defs.py
script_path = Get Default Directory();
Python Send(script_path);
Python Submit("\[
import sys
import os
import multiprocessing
from multiprocessing import Pool
import platform
# The loaded Python does not have the current working directory initialized, so change it
# to the directory of the script, as sent in from JSL
print('script_path: ' + script_path)
hostPlatform = platform.system()
print(hostPlatform)
try:
if 'Windows' == hostPlatform:
# strip leading / off from path if it exists
if '/' == script_path[0]:
script_path = script_path[1:]
norm_path = os.path.normpath(script_path)
print(norm_path)
os.chdir(norm_path)
else:
os.chdir(script_path)
except:
print("Unable to change current working directory", sys.exc_info())
finally:
print(os.getcwd())
# if '.' is not at the beginning of sys.path, prepend so we can look for modules within CWD
if sys.path[0] != '.':
sys.path.insert(0, '.')
print( sys.path )
# here argv is [''] empty string, doesn't seem to bother us in this use case.
#print( 'argv: ', sys.argv )
# prints the path to JMP, not Python
print('sys.executable: ' + sys.executable)
# build up path prefix to python3 exectable
if 'Darwin' == hostPlatform:
pyPath = os.path.join(sys.exec_prefix, 'bin', 'python3')
elif 'Windows' == hostPlatform:
# Bug - on windows sys.exec_prefix is empty in Python within JMP!
pyPath = None
if sys.exec_prefix:
pyPath = os.path.join(sys.exec_prefix, 'pythonw.exe')
else:
# FixMe: hardcode for now
pyPath = 'C:\\Users\\panels\\AppData\\Local\\Programs\\Python\\Python39\\pythonw.exe'
print ('Python Executable: ' + pyPath)
# Finally down to running something in parallel.
#
# Import the function code that will be executed in parallel.
# JMP is not reporting ImportError or ModuleNotFound errors, the scripting just returns -1
# on an import failure, so wrap it within our own try: except: block
try:
import defs as d
# print( d.f(2) )
# Tell multiprocessing to use the Python executable, not JMP to spawn the workers.
multiprocessing.set_executable(pyPath)
# Run the tasks in parallel - (separate Python instances)
with Pool(5) as p:
print(p.map(d.f, [1,2,3])) # Results in [1,4,9] the square of the numbers
except ModuleNotFoundError as mnf:
print(mnf)
print('Check that the moudule is located on your sys.path:')
print( sys.path )
except ImportError as error:
print(error)
except Exception as ex:
print(ex)
]\");