Notez que l'éditeur de contenu a supprimé un retour chariot du contenu de defs.py et aurait dû s'afficher sous la forme :
def f(x):
return x*x
Pendant les vacances, j'ai eu le temps de bricoler avec Python. Le problème que nous avons rencontré avec le traitement parallèle à partir de l'interface Python/JSL est que Python lance généralement plusieurs copies de JMP, pas Python. La raison en est que nous intégrons Python dans JMP, donc l'exécutable est JMP au lieu de Python. J'ai découvert que le package de multitraitement permet de configurer l'exécutable pour qu'il se lance lors de l'exécution du code parallèle. C'est la pièce manquante du puzzle.
Le script ci-dessous que j'ai testé sur JMP 15.2.1 sur Mac et Windows avec Python 3.9. Sur ma machine Windows, je devais utiliser l'argument Path() pour Python Init(). J'ai plusieurs versions de Python et je veux m'assurer que la bonne est choisie. Sur mon Mac, Python Init() sans arguments fonctionne bien pour ma configuration. Il y a un problème que j'ai rencontré sous Windows, j'ai dû coder en dur le chemin d'accès à mon pythonw.exe. Vous devrez modifier cela pour qu'il corresponde à votre emplacement Python. Recherchez FixMe :
Outre les paramètres Python Init() et le chemin d'accès à pythonw.exe sous Windows, cela devrait fonctionner sans problème sur JMP 15.2.1 et plus récent sur Mac ou Windows. Peut fonctionner sur les anciennes versions de JMP qui prennent en charge Python, mais qui n'ont pas été testées.
Vous devrez créer un fichier defs.py dans le même répertoire que ce script. Le package de multitraitement nécessite que le code à exécuter en parallèle provienne du code importable, et non local du script. Ainsi, la plupart du travail dans ce script consistait à s'assurer que sys.path est correctement configuré afin que le fichier defs.py puisse être simplement importé par le script.
L'intégralité du contenu du fichier defs.py se trouve dans les commentaires du fichier, ce qui facilite le collage dans un nouveau fichier. Ce contenu dupliqué ci-dessous est :
def f(x):
return x*x
Le script prend une liste de nombres [1,2,3] et en parallèle appelle f(x) pour mettre au carré la valeur donnant les résultats [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)
]\");