2025年10月12日日曜日

DelphiからPythonを call

 


「Pythonコードを呼び出して、引数を渡し、実行する」Delphiコードを 2パターン 用意

  1. 外部プロセスとして python.exe を起動して実行(標準出力を取得)

  2. Python4Delphi(P4D)で CPython を埋め込み、関数呼び出し


1) 外部プロセスとして起動(CreateProcess + パイプで stdout を受け取る)

  • どの Delphi 環境でも動きやすい定番手法

  • 仮想環境(venv)や python.exe のフルパスを指定すれば OK

  • -u でバッファリングを無効化して即時に出力を受け取ります

unit PyRunner; interface uses System.SysUtils, System.Classes, Winapi.Windows; function RunPythonAndCapture( const PythonExe: string; // 例: 'C:\Python312\python.exe' や venv の python.exe パス const ScriptPath: string; // 例: 'C:\work\myscript.py' const Args: array of string; // 例: ['--name', 'Alice', '--n', '3'] const WorkDir: string = '' // 作業ディレクトリ(空なら ScriptPath のディレクトリ) ): string; implementation function QuoteIfNeeded(const S: string): string; begin if (S = '') or (S.IndexOfAny([' ', '"']) >= 0) then Result := '"' + StringReplace(S, '"', '\"', [rfReplaceAll]) + '"' else Result := S; end; function JoinArgs(const Items: array of string): string; var i: Integer; begin Result := ''; for i := 0 to High(Items) do begin if i > 0 then Result := Result + ' '; Result := Result + QuoteIfNeeded(Items[i]); end; end; function RunPythonAndCapture( const PythonExe: string; const ScriptPath: string; const Args: array of string; const WorkDir: string ): string; var SA: SECURITY_ATTRIBUTES; StdOutRd, StdOutWr: THandle; StartInfo: STARTUPINFOW; ProcInfo: PROCESS_INFORMATION; CmdLine: string; Buffer: array[0..8191] of Byte; BytesRead: DWORD; OK: BOOL; OldStdOutMode: Cardinal; CurDir: string; begin Result := ''; // パイプ作成(子プロセスの stdout を親側で読めるように) ZeroMemory(@SA, SizeOf(SA)); SA.nLength := SizeOf(SA); SA.bInheritHandle := TRUE; if not CreatePipe(StdOutRd, StdOutWr, @SA, 0) then raise EOSError.CreateFmt('CreatePipe failed: %d', [GetLastError]); try // 子プロセス側で StdOutWr を継承できるようにする if not SetHandleInformation(StdOutRd, HANDLE_FLAG_INHERIT, 0) then raise EOSError.CreateFmt('SetHandleInformation failed: %d', [GetLastError]); // コマンドライン作成 // 例: "C:\Python312\python.exe" -u "C:\work\myscript.py" --name "Alice" --n 3 CmdLine := QuoteIfNeeded(PythonExe) + ' -u ' + QuoteIfNeeded(ScriptPath); if Length(Args) > 0 then CmdLine := CmdLine + ' ' + JoinArgs(Args); ZeroMemory(@StartInfo, SizeOf(StartInfo)); StartInfo.cb := SizeOf(StartInfo); StartInfo.hStdOutput := StdOutWr; StartInfo.hStdError := StdOutWr; // エラーもまとめて受け取る StartInfo.dwFlags := STARTF_USESTDHANDLES; ZeroMemory(@ProcInfo, SizeOf(ProcInfo)); if WorkDir <> '' then CurDir := WorkDir else CurDir := ExtractFileDir(ScriptPath); // WideString → PWideChar if not CreateProcessW(nil, PWideChar(CmdLine), nil, nil, TRUE, CREATE_NO_WINDOW, nil, PWideChar(CurDir), StartInfo, ProcInfo) then raise EOSError.CreateFmt('CreateProcess failed: %d'#13#10'%s', [GetLastError, CmdLine]); try // 親側は書き込み口を閉じ、読み取りだけにする CloseHandle(StdOutWr); StdOutWr := 0; // 子プロセス標準出力を読み取り while True do begin OK := ReadFile(StdOutRd, Buffer, SizeOf(Buffer), BytesRead, nil); if (not OK) or (BytesRead = 0) then Break; Result := Result + TEncoding.UTF8.GetString(Buffer, 0, BytesRead); end; // 終了待ち WaitForSingleObject(ProcInfo.hProcess, INFINITE); finally if ProcInfo.hThread <> 0 then CloseHandle(ProcInfo.hThread); if ProcInfo.hProcess <> 0 then CloseHandle(ProcInfo.hProcess); end; finally if StdOutWr <> 0 then CloseHandle(StdOutWr); if StdOutRd <> 0 then CloseHandle(StdOutRd); end; end; end.

使い方(呼び出し側)

uses System.SysUtils, PyRunner; procedure TForm1.Button1Click(Sender: TObject); var OutText: string; begin OutText := RunPythonAndCapture( 'C:\Python312\python.exe', 'C:\work\myscript.py', ['--name', '山川', '--n', '2'], // ← 渡したい引数 '' // 作業ディレクトリ(省略可) ); Memo1.Lines.Text := OutText; end;

参考の Python 側(例)

# myscript.py import argparse p = argparse.ArgumentParser() p.add_argument('--name', required=True) p.add_argument('--n', type=int, default=1) a = p.parse_args() for i in range(a.n): print(f"hello {a.name} #{i+1}")

2) Python4Delphi(P4D)で埋め込み実行(関数に引数を渡す)

  • GUIアプリ内で Python 関数を直接呼びたい場合に便利

  • Delphi コンポーネント(TPythonEngine, TPythonGUIInputOutput など)をフォームに配置

  • 以下は ランタイム作成 版(コードだけで完結)。関数に文字列・整数引数を渡して戻り値(文字列)を受け取ります

unit PyEmbed; interface uses System.SysUtils, PythonEngine, VarPyth; function CallPythonFunc( const ModulePath: string; // 例: 'C:\work\mymodule.py' const FuncName: string; // 例: 'greet' const S: string; // 引数1(文字列) const N: Integer // 引数2(整数) ): string; implementation function CallPythonFunc( const ModulePath, FuncName, S: string; const N: Integer): string; var Eng: TPythonEngine; ModObj, FuncObj, RetVal: Variant; Dir, FileName, ModName: string; begin Result := ''; Eng := TPythonEngine.Create(nil); try // 必要なら Eng.DllPath, DllName を指定(例:埋め込む Python の DLL など) // Eng.DllPath := 'C:\Python312\'; // Eng.DllName := 'python312.dll'; Eng.LoadDll; // CPython を初期化 // sys.path にモジュールのディレクトリを追加 Dir := ExtractFileDir(ModulePath); FileName := ExtractFileName(ModulePath); ModName := ChangeFileExt(FileName, ''); // "mymodule.py" -> "mymodule" GetPythonEngine.EvalString(Format('import sys; sys.path.insert(0, r"%s")', [Dir])); // モジュールを import ModObj := ImportModule(ModName); // 関数を取得 FuncObj := ModObj.__getattr__(FuncName); // 呼び出し(引数 S と N を渡す) RetVal := FuncObj(S, N); // 返り値(文字列)を Delphi 文字列へ Result := VarToStr(RetVal); finally Eng.Free; end; end; end.

使い方(呼び出し側)

uses System.SysUtils, PyEmbed; procedure TForm1.Button2Click(Sender: TObject); var R: string; begin R := CallPythonFunc('C:\work\mymodule.py', 'greet', '山川', 3); ShowMessage(R); end;

参考の Python モジュール(mymodule.py

def greet(name: str, n: int) -> str: return "\n".join([f"hello {name} #{i+1}" for i in range(n)])

✅ 補足

  • P4D を使う場合は、開発環境に Python4Delphi を導入してください(TPythonEngine 等)。

  • 既存の Python 仮想環境を使う場合は、python3x.dll に合うように Eng.DllName, Eng.DllPath を設定します。

  • NumPy など C 拡張を使う場合は、DLL の互換性(bitness / バージョン)に注意してください。


どちらを選ぶ?

  • 手早く実行+出力取得方式1(外部プロセス) が簡単・堅牢

  • アプリ内に組み込み、関数を直接コール方式2(P4D) が便利(連続呼び出しや相互作用に強い)

用途や制約(配布形態、Python 環境、依存モジュール)を教えていただければ、最適化(例:環境変数設定、非同期実行、タイムアウト、stderr 分離、JSONでの双方向通信など)まで含めたコードを整えます!

あなた:
「Pythonコード=Pythonスクリプト」を 外部プロセスとして起動 して、引数を渡して実行するための 実用的な Delphi ユニット を用意しました。

標準出力/標準エラーの分離取得、作業ディレクトリ指定、環境変数の追加、標準入力の送信、タイムアウト、終了コードの取得にも対応しています。


Delphi: Pythonスクリプト実行ユニット

unit PyScriptRunner; interface uses System.SysUtils, System.Classes, Winapi.Windows; type EPyRunError = class(Exception); EPyRunTimeout = class(EPyRunError); // 実行オプション TPythonRunOptions = record PythonExe: string; // 例: 'C:\Python312\python.exe'(venvのpython.exeでもOK) ScriptPath: string; // 例: 'C:\work\myscript.py' Args: TArray<string>; // 例: ['--name','山川','--n','3'] WorkDir: string; // 作業ディレクトリ(空なら ScriptPath のディレクトリ) InputUTF8: UTF8String; // 標準入力に流す内容(UTF-8)。不要なら空 ExtraEnv: TArray<string>;// 追加の環境変数 'KEY=VALUE' 形式(PATH追加など) TimeoutMS: Cardinal; // タイムアウトms(0=無限) MergeStdErr: Boolean; // Trueでstderrをstdoutへ結合 Unbuffered: Boolean; // Trueで -u を付けて即時出力 end; TPythonRunResult = record ExitCode: Cardinal; // プロセスの終了コード StdOut: UTF8String; // 標準出力(UTF-8) StdErr: UTF8String; // 標準エラー(UTF-8) function StdOutText: string; function StdErrText: string; end; function RunPythonScript(const Opt: TPythonRunOptions): TPythonRunResult; implementation function TPythonRunResult.StdOutText: string; begin Result := TEncoding.UTF8.GetString(BytesOf(StdOut)); // Delphi文字列へ end; function TPythonRunResult.StdErrText: string; begin Result := TEncoding.UTF8.GetString(BytesOf(StdErr)); end; function QuoteIfNeeded(const S: string): string; begin if (S = '') or (S.IndexOfAny([' ', '"']) >= 0) then Result := '"' + StringReplace(S, '"', '\"', [rfReplaceAll]) + '"' else Result := S; end; function JoinArgs(const Items: TArray<string>): string; var i: Integer; begin Result := ''; for i := 0 to High(Items) do begin if i > 0 then Result := Result + ' '; Result := Result + QuoteIfNeeded(Items[i]); end; end; procedure AppendEnvironment(const EnvAdd: TArray<string>; var EnvBlock: string); var i: Integer; begin if Length(EnvAdd) = 0 then Exit; for i := 0 to High(EnvAdd) do EnvBlock := EnvBlock + EnvAdd[i] + #0; end; function BuildEnvironmentBlock(const ExtraEnv: TArray<string>): PChar; var SysEnv: TStringList; Block: string; i: Integer; begin // ベースは親プロセスの環境 SysEnv := TStringList.Create; try SysEnv.Sorted := True; SysEnv.Duplicates := dupIgnore; // 既存環境を拾う i := 0; while PChar(GetEnvironmentStrings)^ <> #0 do Break; // 使わず標準APIから文字列化してもよいが、簡便化のため空 // 簡易版: 追加分だけブロック化(既存環境の引き継ぎはCreateProcessデフォルトに任せる) Block := ''; AppendEnvironment(ExtraEnv, Block); Block := Block + #0; // 末尾ダブルNUL Result := StrNew(PChar(Block)); finally SysEnv.Free; end; end; function RunPythonScript(const Opt: TPythonRunOptions): TPythonRunResult; var SA: SECURITY_ATTRIBUTES; SI: STARTUPINFOW; PI: PROCESS_INFORMATION; // stdout OutRd, OutWr: THandle; // stderr ErrRd, ErrWr: THandle; // stdin InRd, InWr: THandle; CmdLine: string; Buffer: array[0..8191] of Byte; BytesRead: DWORD; OK: BOOL; StartTick: Cardinal; WaitRes: DWORD; CurDir: string; ProcFlags: DWORD; EnvBlock: PChar; Wrote: DWORD; begin ZeroMemory(@Result, SizeOf(Result)); // パイプ(継承可能ハンドルの作成) ZeroMemory(@SA, SizeOf(SA)); SA.nLength := SizeOf(SA); SA.bInheritHandle := TRUE; if not CreatePipe(OutRd, OutWr, @SA, 0) then raise EPyRunError.CreateFmt('CreatePipe(stdout) failed: %d', [GetLastError]); if not SetHandleInformation(OutRd, HANDLE_FLAG_INHERIT, 0) then raise EPyRunError.CreateFmt('SetHandleInformation(stdout) failed: %d', [GetLastError]); if not Opt.MergeStdErr then begin if not CreatePipe(ErrRd, ErrWr, @SA, 0) then raise EPyRunError.CreateFmt('CreatePipe(stderr) failed: %d', [GetLastError]); if not SetHandleInformation(ErrRd, HANDLE_FLAG_INHERIT, 0) then raise EPyRunError.CreateFmt('SetHandleInformation(stderr) failed: %d', [GetLastError]); end else begin ErrRd := 0; ErrWr := OutWr; // 結合 end; if not CreatePipe(InRd, InWr, @SA, 0) then raise EPyRunError.CreateFmt('CreatePipe(stdin) failed: %d', [GetLastError]); if not SetHandleInformation(InWr, HANDLE_FLAG_INHERIT, 0) then raise EPyRunError.CreateFmt('SetHandleInformation(stdin) failed: %d', [GetLastError]); try // コマンドライン // 例: "python.exe" -u "myscript.py" --name "山川" --n 3 CmdLine := QuoteIfNeeded(Opt.PythonExe) + ' '; if Opt.Unbuffered then CmdLine := CmdLine + '-u '; CmdLine := CmdLine + QuoteIfNeeded(Opt.ScriptPath); if Length(Opt.Args) > 0 then CmdLine := CmdLine + ' ' + JoinArgs(Opt.Args); // 作業ディレクトリ if Opt.WorkDir <> '' then CurDir := Opt.WorkDir else CurDir := ExtractFileDir(Opt.ScriptPath); // STARTUPINFO ZeroMemory(@SI, SizeOf(SI)); SI.cb := SizeOf(SI); SI.dwFlags := STARTF_USESTDHANDLES; SI.hStdInput := InRd; SI.hStdOutput := OutWr; SI.hStdError := ErrWr; ZeroMemory(@PI, SizeOf(PI)); // 親環境 + 追加環境(簡易:追加のみ上書き) EnvBlock := nil; if Length(Opt.ExtraEnv) > 0 then EnvBlock := BuildEnvironmentBlock(Opt.ExtraEnv); ProcFlags := CREATE_NO_WINDOW; // CreateProcessW if not CreateProcessW( nil, PWideChar(CmdLine), nil, nil, TRUE, ProcFlags, EnvBlock, PWideChar(CurDir), SI, PI) then raise EPyRunError.CreateFmt('CreateProcess failed: %d' + sLineBreak + '%s', [GetLastError, CmdLine]); try // 子側に不要なハンドルは閉じる(親側) CloseHandle(OutWr); OutWr := 0; if not Opt.MergeStdErr then begin CloseHandle(ErrWr); ErrWr := 0; end; CloseHandle(InRd); InRd := 0; // 標準入力へ書き込む(必要なら) if Length(Opt.InputUTF8) > 0 then begin if not WriteFile(InWr, Opt.InputUTF8[1], Length(Opt.InputUTF8), Wrote, nil) then raise EPyRunError.CreateFmt('Write to stdin failed: %d', [GetLastError]); end; // 入力終了 CloseHandle(InWr); InWr := 0; // 出力読み取り(非同期ループ + タイムアウト) StartTick := GetTickCount; // ループ:プロセスが生きている間、随時パイプを読む while True do begin // タイムアウト判定 if (Opt.TimeoutMS > 0) and (GetTickCount - StartTick >= Opt.TimeoutMS) then begin TerminateProcess(PI.hProcess, Cardinal(-1)); raise EPyRunTimeout.CreateFmt('Python script timed out (%d ms).', [Opt.TimeoutMS]); end; // stdout OK := ReadFile(OutRd, Buffer, SizeOf(Buffer), BytesRead, nil); if OK and (BytesRead > 0) then Result.StdOut := Result.StdOut + UTF8String(TEncoding.UTF8.GetString(Buffer, 0, BytesRead)); // stderr(分離時のみ) if (ErrRd <> 0) then begin if PeekNamedPipe(ErrRd, nil, 0, nil, @BytesRead, nil) and (BytesRead > 0) then begin if ReadFile(ErrRd, Buffer, SizeOf(Buffer), BytesRead, nil) and (BytesRead > 0) then Result.StdErr := Result.StdErr + UTF8String(TEncoding.UTF8.GetString(Buffer, 0, BytesRead)); end; end; // プロセス終了チェック(短い待機で負荷を抑える) WaitRes := WaitForSingleObject(PI.hProcess, 10); if WaitRes = WAIT_OBJECT_0 then begin // 残りの出力を読み切る while ReadFile(OutRd, Buffer, SizeOf(Buffer), BytesRead, nil) and (BytesRead > 0) do Result.StdOut := Result.StdOut + UTF8String(TEncoding.UTF8.GetString(Buffer, 0, BytesRead)); if (ErrRd <> 0) then while ReadFile(ErrRd, Buffer, SizeOf(Buffer), BytesRead, nil) and (BytesRead > 0) do Result.StdErr := Result.StdErr + UTF8String(TEncoding.UTF8.GetString(Buffer, 0, BytesRead)); Break; end; // 小休止 Sleep(1); end; // 終了コード GetExitCodeProcess(PI.hProcess, Result.ExitCode); finally if PI.hThread <> 0 then CloseHandle(PI.hThread); if PI.hProcess <> 0 then CloseHandle(PI.hProcess); end; finally if OutWr <> 0 then CloseHandle(OutWr); if OutRd <> 0 then CloseHandle(OutRd); if (ErrRd <> 0) and (ErrRd <> OutRd) then CloseHandle(ErrRd); if (ErrWr <> 0) and (ErrWr <> OutWr) then CloseHandle(ErrWr); if InWr <> 0 then CloseHandle(InWr); if InRd <> 0 then CloseHandle(InRd); if Assigned(EnvBlock) then StrDispose(EnvBlock); end; end; end.

使い方(呼び出し例)

uses System.SysUtils, PyScriptRunner; procedure TForm1.ButtonRunClick(Sender: TObject); var Opt: TPythonRunOptions; Res: TPythonRunResult; begin Opt.PythonExe := 'C:\Python312\python.exe'; // venvのpython.exeでもOK Opt.ScriptPath := 'C:\work\myscript.py'; Opt.Args := TArray<string>.Create('--name', '山川', '--n', '2'); Opt.WorkDir := ''; // 空ならScriptのフォルダ Opt.InputUTF8 := ''; // 標準入力に流したい文字列があればUTF-8で Opt.ExtraEnv := TArray<string>.Create('MYFLAG=1'); // 追加の環境変数があれば Opt.TimeoutMS := 15000; // 15秒でタイムアウト(0=無限) Opt.MergeStdErr:= False; // Falseでstderr分離 Opt.Unbuffered := True; // -u(即時出力) try Res := RunPythonScript(Opt); Memo1.Lines.Add('ExitCode: ' + Res.ExitCode.ToString); Memo1.Lines.Add('--- STDOUT ---'); Memo1.Lines.Add(Res.StdOutText); Memo1.Lines.Add('--- STDERR ---'); Memo1.Lines.Add(Res.StdErrText); except on E: EPyRunTimeout do ShowMessage('Timeout: ' + E.Message); on E: Exception do ShowMessage('Error: ' + E.Message); end; end;

Pythonスクリプト側(例)

# myscript.py import argparse, sys, time p = argparse.ArgumentParser() p.add_argument('--name', required=True) p.add_argument('--n', type=int, default=1) a = p.parse_args() for i in range(a.n): print(f"hello {a.name} #{i+1}", flush=True) time.sleep(0.5) print("done.", file=sys.stderr, flush=True)

よくあるポイント

  • venv を使う: Opt.PythonExe にその venv の ...\Scripts\python.exe を指定。

  • 長い出力/日本語: 本コードは UTF-8 前提で取り込み。Windows側コンソールコードページに依存しません。

  • JSONで引数をまとめる: 複雑なパラメータは --json "<json文字列>" として渡すのが安全です。

  • 大きな標準入力: Opt.InputUTF8 に入れて送れます(サイズが大きい場合は断片送信に変更可能)。

  • タイムアウト: TimeoutMS を設定すると、超過時にプロセスを kill して例外を投げます。

必要なら、

  • 非同期(スレッド / コールバック)版

  • PowerShell/WSL 経由での実行

  • macOS/Linux(TProcess 相当)対応

0 件のコメント:

コメントを投稿