Python與Javascript相互調用超詳細講解(2022年1月最新)(一)基本原理 Part 1 - 通過子進程和進程間通信(IPC)

milliele 2022-01-15 03:08:10 阅读数:963

python javascript 相互 最新 基本原理

首先要明白的是,javascript和python都是解釋型語言,它們的運行是需要具體的runtime的。

  • Python: 我們最常安裝的Python其實是cpython,就是基於C來運行的。除此之外還有像pypy這樣的自己寫了解釋器的,transcrypt這種轉成js之後再利用js的runtime的。基本上,不使用cpython作為python的runtime的最大問題就是通過pypi安裝的那些外來包,甚至有一些cpython自己的原生包(像collections這種)都用不了。
  • JavaScript: 常見的運行引擎有google的V8,Mozilla的SpiderMonkey等等,這些引擎會把JavaScript代碼轉換成機器碼執行。基於這些基礎的運行引擎,我們可以開發支持JS的瀏覽器(比如Chrome的JS運行引擎就是V8);也可以開發功能更多的JS運行環境,比如Node.js,相當於我們不需要一個瀏覽器,也可以跑JS代碼。有了Node.js,JS包管理也變得方便許多,如果我們想把開發好的Node.js包再給瀏覽器用,就需要把基於Node.js的源代碼編譯成瀏覽器支持的JS代碼。

在本文敘述中,假定:

  • 主語言: 最終的主程序所用的語言
  • 副語言: 不是主語言的另一種語言

例如,python調用js,python就是主語言,js是副語言

TL; DR

適用於:

  1. python和javascript的runtime(基本特指cpython[不是cython!]和Node.js)都裝好了
  2. 副語言用了一些複雜的包(例如python用了numpy、javascript用了一點Node.js的C++擴展等)
  3. 對運行效率有要求的話:
    • python與javascript之間的交互不能太多,傳遞的對象不要太大、太複雜,最好都是可序列化的對象
    • javascript占的比重不過小。否則,python調js的話,啟動Node.js子進程比實際跑程序還慢;js調python的話,因為js跑得快,要花很多時間在等python上。
  4. 因為IPC大概率會用線程同步輸入輸出,主語言少整啥多進程多、線程之類的並發編程

有庫!有庫!有庫!

python調javascript

  • JSPyBridgepip install javascript
    • 優點:
      1. 作者還在維護,回issue和更新蠻快的。
      2. 支持比較新的python和node版本,安裝簡單
      3. 基本支持互調用,包括綁定或者傳回調函數之類的。
    • 缺點:沒有合理的銷毀機制,import javascript即視作連接JS端,會初始化所有要用的線程多線程。如果python主程序想重啟對JS的連接,或者主程序用了多進程,想在每個進程都連接一次JS,都很難做到,會容易出錯。
  • PyExecJSpip install PyExecJS,比較老的技術文章都推的這個包
    • 優點: 支持除了Node.js以外的runtime,例如PhantomJS之類的
    • 缺點: End of Life,作者停止維護了

javascript調python

(因為與我的項目需求不太符合,所以了解不太多)

原理

首先,該方法的前提是兩種語言都要有安裝好的runtime,且能通過命令行調用runtime運行文件或一串字符脚本。例如,裝好cpython後我們可以通過python a.py來運行python程序,裝好Node.js之後我們可以通過node a.js或者node -e "some script"等來運行JS程序。

當然,最簡單的情况下,如果我們只需要調用一次副語言,也沒有啥交互(或者最多只有一次交互),那直接找個方法調用CLI就OK了。把給副語言的輸入用stdin或者命令行參數傳遞,讀取命令的輸出當作副語言的輸出。
例如,python可以用subprocess.Popensubprocess.callsubprocess.check_output或者os.system之類的,Node.js可以用child_process裏的方法,exec或者fork之類的。需要注意的是,如果需要引用其他包,Node.js需要注意在node_modules所在的目錄下運行指令,python需要注意設置好PYTHONPATH環境變量。

# Need to set the working directory to the directory where `node_modules` resides if necessary
>>> import subprocess
>>> a, b = 1, 2
>>> print(subprocess.check_output(["node", "-e", f"console.log({a}+{b})"]))
b'3\n'
>>> print(subprocess.check_output(["node", "-e", f"console.log({a}+{b})"]).decode('utf-8'))
3
// Need to set PYTHONPATH in advance if necessary
const a = 1;
const b = 2;
const { execSync } = require("child_process");
console.log(execSync(`python -c "print(${a}+${b})"`));
//<Buffer 33 0a>
console.log(execSync(`python -c "print(${a}+${b})"`).toString());
//3
//

如果有複雜的交互,要傳遞複雜的對象,有的倒還可以序列化,有的根本不能序列化,咋辦?
這基本要利用進程間通信(IPC),通常情况下是用管道(Pipe)。在stdinstdoutstderr三者之中至少挑一個建立管道。
假設我用stdin從python向js傳數據,用stderr接收數據,模式大約會是這樣的:
(以下偽代碼僅為示意,沒有嚴格測試過,實際使用建議直接用庫)

  1. 新建一個副語言(假設為JS)文件python-bridge.js:該文件不斷讀取stdin並根據發來的信息不同,進行不同處理;同時如果需要打印信息或者傳遞object給主語言,將它們適當序列化後寫入stdout或者stderr
    process.stdin.on('data', data => {
    data.split('\n').forEach(line => {
    // Deal with each line
    // write message
    process.stdout.write(message + "\n");
    // deliver object, "$j2p" can be any prefix predefined and agreed upon with the Python side
    // just to tell python side that this is an object needs parsing
    process.stderr.write("$j2p sendObj "+JSON.stringify(obj)+"\n);
    });
    }
    process.on('exit', () => {
    console.debug('** Node exiting');
    });
    
  2. 在python中,用Popen异步打開一個子進程,並將子進程的之中的至少一個,用管道連接。大概類似於:
    cmd = ["node", "--trace-uncaught", f"{os.path.dirname(__file__)}/python-bridge.js"]
    kwargs = dict(
    stdin=subprocess.PIPE,
    stdout=sys.stdout,
    stderr=subprocess.PIPE,
    )
    if os.name == 'nt':
    kwargs['creationflags'] = subprocess.CREATE_NO_WINDOW
    subproc = subprocess.Popen(cmd, **kwargs)
    
  3. 在需要調用JS,或者需要給JS傳遞數據的時候,往subproc寫入序列化好的信息,寫入後需要flush,不然可能會先寫入緩沖區:
    subproc.stdin.write(f"$p2j call funcName {json.dumps([arg1, arg2])}".encode())
    subproc.stdin.flush() # write immediately, not writing to the buffer of the stream
    
  4. 對管道化的stdout/stderr,新建一個線程,專門負責讀取傳來的數據並進行處理。是對象的重新轉換成對象,是普通信息的直接打印回主進程的stderr或者stdout
    def read_stderr():
    while subproc.poll() is None:
    # when the subprocess is still alive, keep reading
    line = self.subproc.stderr.readline().decode('utf-8')
    if line.startswith('$j2p'):
    # receive special information
    _, cmd, line = line.split(' ', maxsplit=2)
    if cmd == 'sendObj':
    # For example, received an object
    obj = json.loads(line)
    else:
    # otherwise, write to stderr as it is
    sys.stderr.write(line)
    stderr_thread = threading.Thread(target=read_stderr, args=(), daemon=True)
    stderr_thread.start()
    
    這裏由於我們的stdout沒有建立管道,所以node那邊往stdout裏打印的東西會直接打印到python的sys.stdout裏,不用自己處理。
  5. 由於線程是异步進行的,什麼時候知道一個函數返回的對象到了呢?答案是用線程同步手段,信號量(Semaphore)、條件(Condition),事件(Event)等等,都可以。以python的條件為例:
    func_name_cv = threading.Condition()
    # use a flag and a result object in case some function has no result
    func_name_result_returned = False
    func_name_result = None
    def func_name_wrapper(arg1, arg2):
    # send arguments
    subproc.stdin.write(f"$p2j call funcName {json.dumps([arg1, arg2])}".encode())
    subproc.stdin.flush()
    # wait for the result
    with func_name_cv:
    if not func_name_result_returned:
    func_name_cv.wait(timeout=10000)
    # when result finally returned, reset the flag
    func_name_result_returned = False
    return func_name_result
    
    同時,需要在讀stderr的線程read_stderr裏解除對這個返回值的阻塞。需要注意的是,如果JS端因為意外而退出了,subproc也會死掉,這時候也要記得取消主線程中的阻塞
    def read_stderr():
    while subproc.poll() is None:
    # when the subprocess is still alive, keep reading
    # Deal with a line
    line = self.subproc.stderr.readline().decode('utf-8')
    if line.startswith('$j2p'):
    # receive special information
    _, cmd, line = line.split(' ', maxsplit=2)
    if cmd == 'sendObj':
    # acquire lock here to ensure the editing of func_name_result is mutex
    with func_name_cv:
    # For example, received an object
    func_name_result = json.loads(line)
    func_name_result_returned = True
    # unblock func_name_wrapper when receiving the result
    func_name_cv.notify()
    else:
    # otherwise, write to stderr as it is
    sys.stderr.write(line)
    # If subproc is terminated (mainly due to error), still need to unblock func_name_wrapper
    func_name_cv.notify()
    
    當然這是比較簡單的版本,由於對JS的調用基本都是線性的,所以可以知道只要得到一個object的返回,那就一定是func_name_wrapper對應的結果。如果函數多起來的話,情况會更複雜。
  6. 如果想取消對JS的連接,首先應該先關閉子進程,然後等待讀stdout/stderr的線程自己自然退出,最後一定不要忘記關閉管道。並且這三步的順序不能換,如果先關了管道,讀線程會因為stdout/stderr已經關了而出錯。
    subproc.terminate()
    stderr_thread.join()
    subproc.stdin.close()
    subproc.stderr.close()
    

如果是通過這種原理javascript調用python,方法也差不多,javascript方是Node.js的話,用的是child_process裏的指令。

優點

  1. 只需要正常裝好兩方的runtime就能實現交互,運行環境相對比較好配。
  2. 只要python方和javascript方在各自的runtime裏正常運行沒問題,那麼連上之後運行也基本不會有問題。(除非涉及並發)
  3. 對兩種語言的所有可用的擴展包基本都能支持。

缺點

  1. 當python與JavaScript交互頻繁,且交互的信息都很大的時候,可能會很影響程序效率。因為僅僅通過最多3個管道混合處理普通要打印的信息、python與js交互的對象、函數調用等,通信開銷很大。
  2. 要另起一個子進程運行副語言的runtime,會花一定時間和空間開銷。
版权声明:本文为[milliele]所创,转载请带上原文链接,感谢。 https://javamana.com/2022/01/202201150250134373.html