借石攻玉——如何用 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

(示例代碼來源於:quora.com/Can-we-write-,有修改)

從調用方式中可以看出,其實也是調用 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

(示例代碼來源於:mango-solutions.com/blo,有修改)

總結

Python 的開發,是以 C/C++ 為基礎的,所以針對 C/C++ 的調用最為方便。其它程序、動態鏈接庫、代碼的調用,都可以通過 EXE、DLL、C/C++ 三種渠道之一實現。Python 還是比較適合粘合各種程序與代碼的。

當然,也就是比 Julia 差一點,(逃


推薦閱讀:

TAG:Python | 动态链接库 | CC |