家好,我是DD,已經(jīng)是封閉在家的第51天了!
最近一直在更新Java新特性(https://www.didispace.com/java-features/)和IDEA Tips(https://www.didispace.com/idea-tips/)兩個(gè)原創(chuàng)專欄,其他方向內(nèi)容的動(dòng)態(tài)關(guān)注少了。昨天天晚上刷推的時(shí)候,瞄到了這個(gè)神奇的東西,覺(jué)得挺cool的,拿出來(lái)分享下:
相信你看到圖,不用我說(shuō),你也猜到是啥了吧?html里可以跑python代碼了!
看到好多Python公眾號(hào)已經(jīng)開(kāi)始猛吹未來(lái)了,但乍看怎么覺(jué)得有點(diǎn)像JSP?或者一些模版引擎?是進(jìn)步還是倒退呢?與其瞎想,不如仔細(xì)看看這個(gè)東東的能力吧!
根據(jù)官方介紹,這個(gè)名為PyScript的框架,其核心目標(biāo)是為開(kāi)發(fā)者提供在標(biāo)準(zhǔn)HTML中嵌入Python代碼的能力,使用 Python調(diào)用JavaScript函數(shù)庫(kù),并以此實(shí)現(xiàn)利用Python創(chuàng)建Web應(yīng)用的功能。
看到介紹里提到了調(diào)用JavaScript函數(shù)庫(kù)的能力,看來(lái)跟JSP或者模版引擎還是有區(qū)別的。
PyScript 快速體驗(yàn)
官方給了一個(gè)例子,可以幫助我們觀的感受這個(gè)開(kāi)發(fā)框架的能力,不妨跟著DD看看,它能做啥吧!
第一個(gè)案例,hello world
代碼很簡(jiǎn)單,就下面這幾行。你只需要?jiǎng)?chuàng)建一個(gè)html文件,然后復(fù)制進(jìn)去就可以了。
<html>
<head>
<link rel="stylesheet" href="https://pyscript.net/alpha/pyscript.css" />
<script defer src="https://pyscript.net/alpha/pyscript.js"></script>
</head>
<body>
<py-script>
print('Hello, World!')
</py-script>
</body>
</html>
|
保存好之后,在瀏覽器里打開(kāi)就能看到這樣的頁(yè)面了:
回頭再看看這個(gè)html里的內(nèi)容,三個(gè)核心內(nèi)容:
- 引入pyscript的樣式文件:<link rel="stylesheet" href="https://pyscript.net/alpha/pyscript.css" />
- 引入pyscript的腳本文件:<script defer src="https://pyscript.net/alpha/pyscript.js"></script>
- <py-script>標(biāo)簽中寫具體的python代碼來(lái)輸出Hello World
如果你懶得自己敲代碼的話,本文的兩個(gè)案例代碼我打包放在公眾號(hào)了,需要的朋友可以關(guān)注公眾號(hào)“程序猿DD”,回復(fù):pyscript 獲取。
第二個(gè)案例,數(shù)據(jù)定義 + 數(shù)據(jù)展示
先創(chuàng)建一個(gè)data.py文件,然后加入前面的代碼。功能很簡(jiǎn)單,就是隨機(jī)生成(x,y)的坐標(biāo)
import numpy as np
def make_x_and_y(n):
x = np.random.randn(n)
y = np.random.randn(n)
return x, y
|
再創(chuàng)建一個(gè)html文件,加入下面的代碼
<html>
<head>
<link rel="stylesheet" href="https://pyscript.net/alpha/pyscript.css" />
<script defer src="https://pyscript.net/alpha/pyscript.js"></script>
<py-env>
- numpy
- matplotlib
- paths:
- /data.py
</py-env>
</head>
<body>
<h1>Let's plot random numbers</h1>
<div id="plot"></div>
<py-script output="plot">
import matplotlib.pyplot as plt
from data import make_x_and_y
x, y = make_x_and_y(n=1000)
fig, ax = plt.subplots()
ax.scatter(x, y)
fig
</py-script>
</body>
</html>
|
這里就稍微復(fù)雜一些了,除了hello world中的幾個(gè)要點(diǎn)外,這里還有這幾個(gè)要關(guān)注的地方:
- <py-env>標(biāo)簽:這里聲明要引入的包和要引入的文件(上面創(chuàng)建的data.py)
- <py-script output="plot">:這里定義了要在<div id="plot"></div>中輸出的內(nèi)容,可以看到這里的邏輯都是用python寫的
這個(gè)頁(yè)面的執(zhí)行效果是這樣的:
是不是很神奇呢?整個(gè)過(guò)程中都沒(méi)有大家熟悉的cs、js內(nèi)容,就完成了這樣一個(gè)圖的頁(yè)面實(shí)現(xiàn)。
小結(jié)
最后,談?wù)勗谡麄€(gè)嘗試過(guò)程中,給我的幾個(gè)感受:
- 開(kāi)發(fā)體驗(yàn)上高度統(tǒng)一,對(duì)于python開(kāi)發(fā)者來(lái)說(shuō),開(kāi)發(fā)Web應(yīng)用的門檻可以更低了
- 感覺(jué)性能上似乎有所不足,幾個(gè)復(fù)雜的案例執(zhí)行有點(diǎn)慢,開(kāi)始以為是部分國(guó)外cdn的緣故,后來(lái)移到本地后,還是慢。這部分可能還需要進(jìn)一步優(yōu)化。
這個(gè)開(kāi)發(fā)框架目前還只是alpha版本,未來(lái)一定還會(huì)有更多特性與優(yōu)化出來(lái),總體上我覺(jué)得這個(gè)框架還是非常cool的,尤其對(duì)于剛學(xué)會(huì)Python,或者只會(huì)Python,但又想快速開(kāi)發(fā)Web應(yīng)用的小伙伴來(lái)說(shuō),可能將會(huì)是個(gè)不錯(cuò)的選擇,那你覺(jué)得這個(gè)框架如何?未來(lái)會(huì)不會(huì)火?留言區(qū)聊聊吧!
avaScript 和 Python 是世界上最流行和最常用的兩種語(yǔ)言。JavaScript 是前端和后端 Web 開(kāi)發(fā)不可或缺的一部分。Python 更適合后端和快速應(yīng)用程序開(kāi)發(fā)。
兩者各有所長(zhǎng),可以通過(guò)從 Python 代碼中運(yùn)行 JavaScript 代碼來(lái)強(qiáng)強(qiáng)聯(lián)合,達(dá)到兩全其美。
JavaScript
JavaScript 是一種用于 Web 開(kāi)發(fā)的腳本語(yǔ)言。可以使用 JavaScript 向網(wǎng)頁(yè)添加行為和功能。是一種解釋性語(yǔ)言,這意味著代碼實(shí)時(shí)運(yùn)行,而無(wú)需編譯器將其轉(zhuǎn)換為機(jī)器代碼。
JavaScrip 語(yǔ)法特征:
代碼塊:JavaScript 使用大括號(hào) {} 定義;
變量:使用 var 關(guān)鍵字定義變量。語(yǔ)法: var variable_name = value;
常量:使用 const 關(guān)鍵字定義常量。語(yǔ)法:constant_name = value;
輸入/輸出:使用 window.prompt() 在 JavaScript 中獲取輸入,并使用 console.log() 在控制臺(tái)上顯示輸出。
Python
Python 是一種高級(jí)編程語(yǔ)言,在后端開(kāi)發(fā)、人工智能和數(shù)據(jù)科學(xué)中得到了廣泛應(yīng)用。Python 也是一種釋型語(yǔ)言。
Python 語(yǔ)法特征:
代碼塊:Python 使用縮進(jìn)來(lái)定義;
變量: variable_name = value;
常量:Python 并不真正支持常量,但約定成俗規(guī)定大寫變量視為常量,例如 CONSTANT_NAME;
輸入/輸出:使用 input() 獲取輸入,并使用 print()顯示輸出。
js2Py 模塊
Js2Py 是一個(gè) JavaScript 到 Python 的翻譯組件,要使用此模塊,請(qǐng)打開(kāi)終端并執(zhí)行安裝:
pip install js2py
Js2Py 自動(dòng)將任何有效的 JavaScript 轉(zhuǎn)換為 Python,而無(wú)需使用任何依賴項(xiàng)。可以將大部分 JavaScript 代碼轉(zhuǎn)換為 Python 語(yǔ)言。
在 Python 中運(yùn)行 JavaScript 代碼的示例
1.我們從經(jīng)典的“Hello World”開(kāi)始。
import js2py
js2py.eval_js('console.log("Hello World!")')
將 JavaScript 代碼作為參數(shù)傳遞給 eval_js() 以對(duì)其進(jìn)行轉(zhuǎn)換。在 Python 的輸出終端上,顯示“Hello World!”。
2.兩個(gè)數(shù)字相加
在 Python 中使用 JavaScript 執(zhí)行兩個(gè)數(shù)字相加的方法。
import js2py
js_add = '''function add(a, b){return a + b;}'''
add = js2py.eval_js(js_add)
print(add(3, 7))
使用 JavaScript 聲明一個(gè)函數(shù),并將其存儲(chǔ)在字符串中。使用函數(shù) eval_js() 以將其轉(zhuǎn)換為 Python 等效函數(shù)。通過(guò)函數(shù)調(diào)用兩個(gè)數(shù)字作為參數(shù)來(lái)顯示結(jié)果。
3.將整個(gè) JavaScript 文件轉(zhuǎn)換為 Python 文件
有兩種簡(jiǎn)單的方法可以將JavaScript文件轉(zhuǎn)換為Python文件。
(1) 使用run_file()函數(shù)直接執(zhí)行 JavaScript 文件。
import js2py
eval_result, example = js2py.run_file('example.js')
(2)轉(zhuǎn)換整個(gè)JavaScript文件為Python文件。
import js2py
js2py.translate_file('example.js', 'example.py')
Python 在web 開(kāi)發(fā)方面主要用于后端編碼,但也可以探索一些工具在前端嘗試它。
?
文章創(chuàng)作不易,如果您喜歡這篇文章,請(qǐng)關(guān)注、點(diǎn)贊并分享給朋友。如有意見(jiàn)和建議,請(qǐng)?jiān)谠u(píng)論中反饋!
?
先要明白的是,javascript和python都是解釋型語(yǔ)言,它們的運(yùn)行是需要具體的runtime的。
- Python: 我們最常安裝的Python其實(shí)是cpython,就是基于C來(lái)運(yùn)行的。除此之外還有像pypy這樣的自己寫了解釋器的,transcrypt這種轉(zhuǎn)成js之后再利用js的runtime的。基本上,不使用cpython作為python的runtime的最大問(wèn)題就是通過(guò)pypi安裝的那些外來(lái)包,甚至有一些cpython自己的原生包(像 collections 這種)都用不了。
- JavaScript: 常見(jiàn)的運(yùn)行引擎有g(shù)oogle的V8,Mozilla的SpiderMonkey等等,這些引擎會(huì)把JavaScript代碼轉(zhuǎn)換成機(jī)器碼執(zhí)行。基于這些基礎(chǔ)的運(yùn)行引擎,我們可以開(kāi)發(fā)支持JS的瀏覽器(比如Chrome的JS運(yùn)行引擎就是V8);也可以開(kāi)發(fā)功能更多的JS運(yùn)行環(huán)境,比如Node.js,相當(dāng)于我們不需要一個(gè)瀏覽器,也可以跑JS代碼。有了Node.js,JS包管理也變得方便許多,如果我們想把開(kāi)發(fā)好的Node.js包再給瀏覽器用,就需要把基于Node.js的源代碼編譯成瀏覽器支持的JS代碼。
在本文敘述中,假定:
- 主語(yǔ)言: 最終的主程序所用的語(yǔ)言
- 副語(yǔ)言: 不是主語(yǔ)言的另一種語(yǔ)言
例如,python調(diào)用js,python就是主語(yǔ)言,js是副語(yǔ)言
TL; DR
適用于:
- python和javascript的runtime(基本特指cpython[不是cython!]和Node.js)都裝好了
- 副語(yǔ)言用了一些復(fù)雜的包(例如python用了numpy、javascript用了一點(diǎn)Node.js的C++擴(kuò)展等)
- 對(duì)運(yùn)行效率有要求的話:
- python與javascript之間的交互不能太多,傳遞的對(duì)象不要太大、太復(fù)雜,最好都是可序列化的對(duì)象
- javascript占的比重不過(guò)小。否則,python調(diào)js的話,啟動(dòng)Node.js子進(jìn)程比實(shí)際跑程序還慢;js調(diào)python的話,因?yàn)閖s跑得快,要花很多時(shí)間在等python上。
- 因?yàn)镮PC大概率會(huì)用線程同步輸入輸出,主語(yǔ)言少整啥多進(jìn)程多、線程之類的并發(fā)編程
有庫(kù)!有庫(kù)!有庫(kù)!
python調(diào)javascript
- JSPyBridge : pip install javascript優(yōu)點(diǎn):作者還在維護(hù),回issue和更新蠻快的。支持比較新的python和node版本,安裝簡(jiǎn)單基本支持互調(diào)用,包括綁定或者傳回調(diào)函數(shù)之類的。缺點(diǎn) :沒(méi)有合理的銷毀機(jī)制, import javascript 即視作連接JS端,會(huì)初始化所有要用的線程多線程。如果python主程序想重啟對(duì)JS的連接,或者主程序用了多進(jìn)程,想在每個(gè)進(jìn)程都連接一次JS,都很難做到,會(huì)容易出錯(cuò)。
- PyExecJS : pip install PyExecJS ,比較老的技術(shù)文章都推的這個(gè)包優(yōu)點(diǎn): 支持除了Node.js以外的runtime,例如PhantomJS之類的缺點(diǎn): End of Life,作者停止維護(hù)了
javascript調(diào)python
(因?yàn)榕c我的項(xiàng)目需求不太符合,所以了解的不太多)
- JSPyBridge : npm i pythonia
- node-python-bridge : npm install python-bridge
- python-shell : npm install python-shell
原理
首先,該方法的前提是兩種語(yǔ)言都要有安裝好的runtime,且能通過(guò)命令行調(diào)用runtime運(yùn)行文件或一串字符腳本。例如,裝好cpython后我們可以通過(guò) python a.py 來(lái)運(yùn)行python程序,裝好Node.js之后我們可以通過(guò) node a.js 或者 node -e "some script" 等來(lái)運(yùn)行JS程序。
當(dāng)然,最簡(jiǎn)單的情況下,如果我們只需要調(diào)用一次副語(yǔ)言,也沒(méi)有啥交互(或者最多只有一次交互),那直接找個(gè)方法調(diào)用CLI就OK了。把給副語(yǔ)言的輸入用stdin或者命令行參數(shù)傳遞,讀取命令的輸出當(dāng)作副語(yǔ)言的輸出。
例如,python可以用 subprocess.Popen , subprocess.call , subprocess.check_output 或者 os.system 之類的,Node.js可以用 child_process 里的方法, exec 或者 fork 之類的。 需要注意的是,如果需要引用其他包,Node.js需要注意在 node_modules 所在的目錄下運(yùn)行指令,python需要注意設(shè)置好PYTHONPATH環(huán)境變量。
# 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
//
如果有復(fù)雜的交互,要傳遞復(fù)雜的對(duì)象,有的倒還可以序列化,有的根本不能序列化,咋辦?
這基本要利用 進(jìn)程間通信(IPC) ,通常情況下是用 管道(Pipe) 。在 stdin , stdout 和 stderr 三者之中至少挑一個(gè)建立管道。
假設(shè)我用 stdin 從python向js傳數(shù)據(jù),用 stderr 接收數(shù)據(jù),模式大約會(huì)是這樣的:
(以下偽代碼僅為示意,沒(méi)有嚴(yán)格測(cè)試過(guò),實(shí)際使用建議直接用庫(kù))
- 新建一個(gè)副語(yǔ)言(假設(shè)為JS)文件 python-bridge.js :該文件不斷讀取 stdin 并根據(jù)發(fā)來(lái)的信息不同,進(jìn)行不同的處理;同時(shí)如果需要打印信息或者傳遞object給主語(yǔ)言,將它們適當(dāng)序列化后寫入 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'); });
- 在python中,用Popen異步打開(kāi)一個(gè)子進(jìn)程,并將子進(jìn)程的之中的至少一個(gè),用管道連接。大概類似于: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)
- 在需要調(diào)用JS,或者需要給JS傳遞數(shù)據(jù)的時(shí)候,往 subproc 寫入序列化好的信息,寫入后需要 flush ,不然可能會(huì)先寫入緩沖區(qū):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
- 對(duì)管道化的 stdout / stderr ,新建一個(gè)線程,專門負(fù)責(zé)讀取傳來(lái)的數(shù)據(jù)并進(jìn)行處理。是對(duì)象的重新轉(zhuǎn)換成對(duì)象,是普通信息的直接打印回主進(jìn)程的 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 沒(méi)有建立管道,所以node那邊往 stdout 里打印的東西會(huì)直接打印到python的 sys.stdout 里,不用自己處理。
- 由于線程是異步進(jìn)行的,什么時(shí)候知道一個(gè)函數(shù)返回的對(duì)象到了呢?答案是用線程同步手段,信號(hào)量(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同時(shí),需要在讀stderr的線程 read_stderr 里解除對(duì)這個(gè)返回值的阻塞。需要注意的是,如果JS端因?yàn)橐馔舛顺隽耍?subproc 也會(huì)死掉, 這時(shí)候也要記得取消主線程中的阻塞 。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()當(dāng)然這是比較簡(jiǎn)單的版本,由于對(duì)JS的調(diào)用基本都是線性的,所以可以知道只要得到一個(gè)object的返回,那就一定是 func_name_wrapper 對(duì)應(yīng)的結(jié)果。如果函數(shù)多起來(lái)的話,情況會(huì)更復(fù)雜。
- 如果想 取消對(duì)JS的連接 ,首先應(yīng)該先關(guān)閉子進(jìn)程,然后等待讀 stdout / stderr 的線程自己自然退出,最后 一定不要忘記關(guān)閉管道 。并且 這三步的順序不能換 ,如果先關(guān)了管道,讀線程會(huì)因?yàn)?stdout / stderr 已經(jīng)關(guān)了而出錯(cuò)。subproc.terminate() stderr_thread.join() subproc.stdin.close() subproc.stderr.close()
如果是通過(guò)這種原理javascript調(diào)用python,方法也差不多,javascript方是Node.js的話,用的是 child_process 里的指令。
優(yōu)點(diǎn)
- 只需要正常裝好兩方的runtime就能實(shí)現(xiàn)交互,運(yùn)行環(huán)境相對(duì)比較好配。
- 只要python方和javascript方在各自的runtime里正常運(yùn)行沒(méi)問(wèn)題,那么連上之后運(yùn)行也基本不會(huì)有問(wèn)題。(除非涉及并發(fā))
- 對(duì)兩種語(yǔ)言的所有可用的擴(kuò)展包基本都能支持。
缺點(diǎn)
- 當(dāng)python與JavaScript交互頻繁,且交互的信息都很大的時(shí)候,可能會(huì)很影響程序效率。因?yàn)閮H僅通過(guò)最多3個(gè)管道混合處理普通要打印的信息、python與js交互的對(duì)象、函數(shù)調(diào)用等,通信開(kāi)銷很大。
- 要另起一個(gè)子進(jìn)程運(yùn)行副語(yǔ)言的runtime,會(huì)花一定時(shí)間和空間開(kāi)銷。