借石攻玉——如何用 Python 調用其他程序和代碼
Python 常被稱為是膠水語言,主要原因就是其對於系統命令、各種語言的代碼和程序都具有良好的調用能力。這裡列舉了一些在 Windows 系統中,常見的介面與調用。在 Linux 中,實現起來都非常簡單直接,符合直覺。在 Windows 中,程序、代碼、動態鏈接庫的調用相對比較複雜,因此 Python 的這種能力也格外地有用。
用 Python 調用 Python 包
這節算是個湊數的熱身,但也是實際使用 Python 開發時,每日必不可少的工作。
from __future__ import (absolute_import, division, print_function,n unicode_literals)nimport python_package_example1nimport python_package_example2 as exp2nfrom python_package_example3 import sub31, sub32, sub33nfrom python_package_example4.sub import code1, code2n
推薦盡量使用絕對路徑調用,盡量起一個簡短明確的別名。
示例
這裡舉一個在開源軟體中實際應用的例子,是 matplotlib 中 imread 函數調用 pillow 圖像讀取代碼的實例。
試著引入一個包來讀圖像,不成功就返回空。
def pilread(fname):n """try to load the image with PIL or return None"""n try:n from PIL import Imagen except ImportError:n return Nonen with Image.open(fname) as image:n return pil_to_array(image)n
參見 GitHub 源碼。
邪惡的小技巧
- 引入一個包,再刪除(matplotlib)
# Get the version from the _version.py versioneer file. For a git checkout,n# this is computed based on the number of commits since the last tag.nfrom ._version import get_versionsn__version__ = str(get_versions()[version])ndel get_versionsn
參見 GitHub 源碼。
- 手動版本檢驗,出錯手動報錯(matplotlib)
_python27 = (sys.version_info.major == 2 and sys.version_info.minor >= 7)n_python34 = (sys.version_info.major == 3 and sys.version_info.minor >= 4)nif not (_python27 or _python34):n raise ImportError("Matplotlib requires Python 2.7 or 3.4 or later")n
參見 GitHub 源碼。
用 Python 調用 exe
話說有這麼一個程序 cos_exe 可以求一個角度的餘弦。你想調用這個程序,怎麼做?
可以使用 subprosess 包(無需安裝)。
文檔參見:17.5. subprocess - Subprocess management - Python 3.5.4 documentation
# test_cos_exe.pynnimport subprocessnimport unittestnnnclass TestCosExe(unittest.TestCase):n """docstring for TestCosExe"""nn def test_result(self):n subprocess.check_output([n ....tcc.exe,n str(cos_exe.c)])n result = subprocess.check_output([n str(cos_exe.exe),n str(45)])n self.assertEqual(result, b0.70711)nnif __name__ == __main__:n unittest.main()n
// cos_exe.cnn#include <stdio.h>n#include <math.h>nnninline float to_radians(float radians) {n return radians * (M_PI / 180.0);n}nnint main(int argc, char const *argv[]) {n float angle;n if (argc == 2) {n /* code */n sscanf(argv[1], "%f", &angle);n }n else {n printf("Wrong argument. Should be: "%s 45"n", argv[0]);n scanf("%f", &angle);n printf("%0.5f", cos(to_radians(angle)));n sleep(2000);n return -1;n }nn printf("%0.5f", cos(to_radians(angle)));nn return 0;n}n
subprocess 可以調用一個 exe 程序,然後把標準輸出和錯誤輸出都接收回來,以字元串變數的形式存儲下來。
如果程序複雜,也可以保存成文件,然後讀取文件中的數據。也可以直接存成圖像、聲音等多媒體文件。但要注意這種使用方式會給程序帶來很多額外的依賴關係,對程序的穩定性與健壯性損害較大。
示例
同樣也舉一個 matplotlib 中的例子。matplotlib 中對於 latex 的支持就是通過系統調用實現的。
在查看到當前目錄下不存在同名 dvi 文件後,matplotlib 調用系統中的 latex 編譯 tex 源文件。如果調用中出現錯誤,則代理輸出。
if (DEBUG or not os.path.exists(dvifile) orn not os.path.exists(baselinefile)):n texfile = self.make_tex_preview(tex, fontsize)n command = [str("latex"), "-interaction=nonstopmode",n os.path.basename(texfile)]n _log.debug(command)n try:n report = subprocess.check_output(command,n cwd=self.texcache,n stderr=subprocess.STDOUT)n except subprocess.CalledProcessError as exc:n raise RuntimeError(n (LaTeX was not able to process the following n string:n%snnn Here is the full report generated by LaTeX:n%s n nn % (repr(tex.encode(unicode_escape)),n exc.output.decode("utf-8"))))n _log.debug(report)n
參見 GitHub 源碼。
用 Python 調用 dll
Python 調用簡單的 C 語言編譯的 DLL,怎麼做?
可以簡單地使用 ctypes 包(無需安裝)。
import ctypesnuser32 = ctypes.windll.LoadLibrary(user32.dll) # load dllnassert(user32.MessageBoxA(0, bCtypes is cool!, bCtypes, 0))n# call message box functionn
(示例代碼來源久遠,已不可考,首次使用在該文中:Some Notes on Python and PyGame)
相當簡單直接。其實只需要用 dependency walker 找到 dll 文件中的函數名,就可以使用 ctypes 包直接調用。(http://dependencywalker.com/)
這裡其實揭示了 dll 文件的本質,就是一組二進位代碼,函數名就是代碼的入口定位符號。
用 ctypes 調用 dll 需要查看 dll 本身的手冊,以便了解相關函數的功能和參數。ctypes 本身只是一層介面,不提供相關功能。
示例
OpenCV 裡面,使用 ctypes 調用系統調用,來獲得當前進程的 exe 文件名和起始地址。
def getRunningProcessExePathByName_win32(name):n from ctypes import windll, POINTER, pointer, Structure, sizeofn from ctypes import c_long , c_int , c_uint , c_char , c_ubyte , c_char_p , c_void_pnn class PROCESSENTRY32(Structure):n _fields_ = [ ( dwSize , c_uint ) ,n ( cntUsage , c_uint) ,n ( th32ProcessID , c_uint) ,n ( th32DefaultHeapID , c_uint) ,n ( th32ModuleID , c_uint) ,n ( cntThreads , c_uint) ,n ( th32ParentProcessID , c_uint) ,n ( pcPriClassBase , c_long) ,n ( dwFlags , c_uint) ,n ( szExeFile , c_char * 260 ) ,n ( th32MemoryBase , c_long) ,n ( th32AccessKey , c_long ) ]nn class MODULEENTRY32(Structure):n _fields_ = [ ( dwSize , c_long ) ,n ( th32ModuleID , c_long ),n ( th32ProcessID , c_long ),n ( GlblcntUsage , c_long ),n ( ProccntUsage , c_long ) ,n ( modBaseAddr , c_long ) ,n ( modBaseSize , c_long ) ,n ( hModule , c_void_p ) ,n ( szModule , c_char * 256 ),n ( szExePath , c_char * 260 ) ]nn TH32CS_SNAPPROCESS = 2n TH32CS_SNAPMODULE = 0x00000008nn ## CreateToolhelp32Snapshotn CreateToolhelp32Snapshot= windll.kernel32.CreateToolhelp32Snapshotn CreateToolhelp32Snapshot.reltype = c_longn CreateToolhelp32Snapshot.argtypes = [ c_int , c_int ]n ## Process32Firstn Process32First = windll.kernel32.Process32Firstn Process32First.argtypes = [ c_void_p , POINTER( PROCESSENTRY32 ) ]n Process32First.rettype = c_intn ## Process32Nextn Process32Next = windll.kernel32.Process32Nextn Process32Next.argtypes = [ c_void_p , POINTER(PROCESSENTRY32) ]n Process32Next.rettype = c_intn ## CloseHandlen CloseHandle = windll.kernel32.CloseHandlen CloseHandle.argtypes = [ c_void_p ]n CloseHandle.rettype = c_intn ## Module32Firstn Module32First = windll.kernel32.Module32Firstn Module32First.argtypes = [ c_void_p , POINTER(MODULEENTRY32) ]n Module32First.rettype = c_intnn hProcessSnap = c_void_p(0)n hProcessSnap = CreateToolhelp32Snapshot( TH32CS_SNAPPROCESS , 0 )nn pe32 = PROCESSENTRY32()n pe32.dwSize = sizeof( PROCESSENTRY32 )n ret = Process32First( hProcessSnap , pointer( pe32 ) )n path = Nonenn while ret :n if name + ".exe" == pe32.szExeFile:n hModuleSnap = c_void_p(0)n me32 = MODULEENTRY32()n me32.dwSize = sizeof( MODULEENTRY32 )n hModuleSnap = CreateToolhelp32Snapshot( TH32CS_SNAPMODULE, pe32.th32ProcessID )nn ret = Module32First( hModuleSnap, pointer(me32) )n path = me32.szExePathn CloseHandle( hModuleSnap )n if path:n breakn ret = Process32Next( hProcessSnap, pointer(pe32) )n CloseHandle( hProcessSnap )n return pathn
參見 GitHub 源碼。
用 Python 調用 C/C++ 代碼
推薦使用 Cython 生成並調用 pyd。
其實 Python 基本實現就是 C 寫的。`#include <Python.h>` 之後,其實就可以直接寫 Python 的擴展了。例如 OpenCV 就是這麼實現的,參見 GitHub 源碼。但這需要對 Python 內部底層模型比較詳細的了解,相對比較麻煩,工程量也比較大。而 Cython 的打包方式,相對簡單直接得多。
cos_pyd.pyx
# distutils: language = cn# distutils: sources =n# distutils: include_dirs =n# distutils: extra_compile_args =n# distutils: extra_link_args =n# distutils: libraries =nncdef extern from math.h:ndouble M_PInncdef extern from cos_exe.c:nint cos_c(double alpha, double * cos_of_alpha)nndef cos_pyd(double alpha):ncdef double cos_of_alphancos_c(alpha, &cos_of_alpha)nreturn cos_of_alphan
pyx 是整個調用工程的組織文件,其中面向 distutils 聲明了整個項目的組織參數。包含了所需的頭文件與源代碼文件。更是寫清了對於 Python 這個 Python 包的組織。
// source.cn#include<math>nnint cos_c(double alpha, double * cos_of_alpha){n double cos_of_alpha = 0;n cos_of_alpha = cos(alpha);n return 0;n}n
這個 source.c 是示例的一部分,簡單寫清了輸入輸出,完成一個簡單的功能。也可以拓展為多輸入、多輸出。
# cos_build_and_app.pynfrom distutils.core import setupnfrom Cython.Build import cythonizennsetup(ext_modules=cythonize(cos_pyd.pyx),nscript_args=build_ext -i -t ..split( ))nnimport cos_pyd as cnnif __name__ == __main__:nresult = c.cos_pyd(45)nprint(The cos of 45 is {0:.5f}..format(result))n
這個 py 文件,實現了 pyd 工程的編譯和調用兩個步驟。
在 import 語句以前的上半部分,完成了一個 pyd 包的編譯過程。編譯後要保存好 pyd 文件,以後在使用的時候,需要把 pyd 放在 python 路徑下。
從 import 語句開始,就是一個典型 python 包的使用方法了。通過使用 pyd 就可以直接實現各種功能。
也可以將 pyd 當作一種打包方式,打包發布供二次開發的代碼。
複雜工程的調用
再複雜的工程,其實都可以用 C/C++ 封裝起來,做成一個工程,然後用 Cython 編譯成 pyd,都可以做成 python 包,被調用。簡單的,其實可以直接用 C 打包,用 ctypes 調用。稍複雜的,再用 Cython。
用 Python 調用 Java 代碼
簡單的,可以使用 Jpype 包。
安裝:conda install -c conda-forge jpype1
考慮到網路問題,可以從這裡下載 whl:Unofficial Windows Binaries for Python Extension Packages,然後使用 pip install *.whl 安裝。
使用
from jpype import *nnstartJVM("C:/Program Files/Java/jdk1.8.0_144/jre/bin/server/jvm.dll", "-ea")nn# Java code heren# Create a java.lang.String objectnjavaPackage = JPackage("java.lang")njavaClass = javaPackage.StringnjavaObject = javaClass("Hello, Jpype")nn# print the objectnjava.lang.System.out.println(javaObject)n# call String.length() and Integer.toString() methodsnjava.lang.System.out.println("This strings length: " +njavaPackage.Integer.toString(javaObject.length()))nnshutdownJVM()n
(示例代碼來源於:https://www.quora.com/Can-we-write-C-and-Java-in-Python,有修改)
從調用方式中可以看出,其實也是調用 jvm 的 dll。
複雜的,可以嘗試使用 pyjnius。
用 Python 調用 MATLAB 代碼
有原生工具,但本質上就是調用 matlab engine 的 dll。
安裝
cd "matlabrootexternenginespython"npython setup.py installn
調用
import matlab.engineneng = matlab.engine.start_matlab()ntf = eng.isprime(37)nprint(tf)n# Truen
具體請參考文檔。
用 Python 調用 R 代碼
跟調用 exe 一樣,只不過這個 exe 是 Rscript。
# run_max.pynimport subprocessnn# 聲明要調用的命令和參數ncommand = Rscriptnpath2script = path/to your script/max.Rnn# 把參數(這裡是數據)放在一個列表裡nargs = [str(11), str(3), str(9), str(42)]nn# 生成要執行的命令ncmd = [command, path2script] + argsnn# check_output 執行命令,保存結果nreturn_str = subprocess.check_output(cmd, universal_newlines=True)nnprint(The maximum of the numbers is:, return_str)n
(示例代碼來源於:https://www.mango-solutions.com/blog/integrating-python-and-r-part-ii-executing-r-from-python-and-vice-versa,有修改)
總結
Python 的開發,是以 C/C++ 為基礎的,所以針對 C/C++ 的調用最為方便。其它程序、動態鏈接庫、代碼的調用,都可以通過 EXE、DLL、C/C++ 三種渠道之一實現。Python 還是比較適合粘合各種程序與代碼的。
當然,也就是比 Julia 差一點,(逃
推薦閱讀: