翻譯自https://proandroiddev.com/chaquopy-using-python-in-android-apps-dd5177c9ab6b
歡迎通過我的Blog存取此文章.
Python在開發者社群中時最受歡迎的語言之一, 因為其簡單,健壯並且有著龐大的軟體生態使其可以在多個領域發揮作用. 類似NumPy和SciPy這樣的包允許你在專案中使用高等數學計算, 而這樣的計算在其它的語言中是無法簡單實現的. 那麼如果將Python引入到Android應用中又會帶來什麼樣效果呢?
Chaquopy是一個可以幫助開發者通過Java/Kotlin在Android平臺中執行Python指令碼的框架. 和其它跨語言庫不同, 它不再有NDK依賴的煩惱, 也不需要native code[1], 並且安裝也十分的簡單. 在這篇檔案中, 我們將探索Chaquopy, 並通過Kotlin來進行構建和使用.
和大多數跨語言介面工作原理一樣, Python和Android都有著C/C++的血統, 使其可以通過中介軟體來進行通訊. Android的NDK允許開發者使用通過C/C++編寫的本地庫, 來幫助Android應用獲得更好的圖形和科學計算效果.
Chaquopy使用CPython, 一個通過C來實現的Python實現. 不同於一般的誤解, Python並不是一個純粹的解釋性語言. Python的原始碼最開始會被構建為可以被CPython執行的特殊位元組碼. 當然, CPython只是Python的幾種直譯器之一, 其它的還有PyPy, IronPython, Jython等等.
Chaquopy通過Android NDK工具鏈來構建CPython, CPython在專案構建的時候通過Chaquopy Gradle外掛從Maven倉庫中心進行下載, 在這個過程中使用者並不需要下載NDK. 它還下載Chaquopy執行支援通過JNI將Java/Kotlin和Python連線起來.
同時, 我們還需要Python包管理工具pip
, 它可以下載為直譯器下載包. 像NumPy
和SciPy
這樣的受歡迎的包可以通過原生程式碼執行高密集的CPU計算, 我們需要事先安裝這些包. 因此, Chaquopy團隊維護了自己的儲存庫,其中包含專門為Android的ARM架構構建的本地軟體包. 這些軟體包的維護者不會為Android平臺構建他們的原生程式碼,因為使用者數量較少,因此Chaquopy團隊會針對Android平臺構建它們並通過自己的儲存庫進行釋出.
對於純粹的Python包, 不需要額外的構建並且Chaquopy可以直接執行這些.從更宏觀來看, Chaquopy包含了三個主要的元件.
在新/現有的Android專案中新增Chaquopy, project級的build.gradle
檔案的頂部, 我們定義專案的plugin並且新增Chaquopy的Gradle外掛.
plugins {
id 'com.android.application' version '7.4.2' apply false
id 'com.android.library' version '7.4.2' apply false
id 'org.jetbrains.kotlin.android' version '1.7.0' apply false
id 'com.chaquo.python' version '13.0.0' apply false
}
下一步, 我們在module級的build.gradle
檔案中新增Chaquopy plugin和指定ABI規則,
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'com.chaquo.python'
}
android {
...
defaultConfig {
...
ndk {
abiFilters "armeabi-v7a" //, "arm64-v8a", "x86", "x86_64"
}
}
...
}
正如官方檔案提及的, Python直譯器是使用Android NDK來構建的本機元件,NDK為指定的版本構建原生程式碼, 比如like arm,x86或x86_64. 不同的裝置支援不同的架構. 所以我們只能包含特定版本的Python直譯器, 而不是為所有架構都進行構建, 因為這會增加應用程式的大小. Android官方檔案中是這麼說的,
構建系統的預設行為是將每個ABI的二進位制檔案包括在單個APK也稱為胖 APK)內. 與僅包含單個ABI的二進位制檔案的APK相比,胖APK要大得多, 要權衡的是相容性更廣,但APK更大. 強烈建議您利用app bundle和APK拆分減小 APK的大小,同時仍保持最大限度的裝置相容性.
下一步, 我們將設定Python構建版本, 我們可以通過修改module級build.gradle
檔案來指定版本.
plugins {
id 'com.android.application'
id 'org.jetbrains.kotlin.android'
id 'com.chaquo.python'
}
android {
...
defaultConfig {
...
ndk {
abiFilters "armeabi-v7a" //, "arm64-v8a", "x86", "x86_64"
}
python {
version "3.10"
}
}
...
}
不同的Chaquopy支援不同的Python版本有著不同的最小API需要. 通過這個表可以查詢到你需要匹配的版本. 下一步, 我們指定在Python直譯器需要的包的版本.
defaultConfig {
python {
pip {
// A requirement specifier, with or without a version number:
install "scipy"
install "requests==2.24.0"
// An sdist or wheel filename, relative to the project directory:
install "MyPackage-1.2.3-py2.py3-none-any.whl"
// A directory containing a setup.py, relative to the project
// directory (must contain at least one slash):
install "./MyPackage"
// "-r"` followed by a requirements filename, relative to the
// project directory:
install "-r", "requirements.txt"
}
}
}
這是在Chaquopy中安裝包的幾種不同方法. 它可以是具有特定版本的包名, 也可以是自定義包或者requirement.txt
包列表.
在Python中,我們使用屬於Python模組的函數或者資料成員, 一個Python模組包含.py檔案. 要使用任何Python模組中的成員. 第一步是將Python原始碼放入<project>/app/src/main/python
資料夾中.
# Contents of my_module.py
import numpy as np
def get_exec_details():
return __file__
def sumOp( nums ):
return sum( nums )
def powOp( a , x ):
return a**x
def npMatrixSum( m , n ):
mat = np.ones( ( m , n ) )
mat_sum = np.sum( mat , axis=1 )
return mat_sum
class Operations:
num_ops = 2
def meanOp( self , nums ):
return sum( nums ) / len( nums )
def maxOp( self , nums ):
return max( nums )
nums_len = 10
nums_len_str = "ten"
ops = Operations()
為了使用my_module
中的成員, 我們需要使用Python.getModule
方法傳遞模組的名稱. 在這之前, 我們需要執行Python
在應用中被允許, 這可以在Application的onCreate方法中執行,
class App : Application() {
override fun onCreate() {
super.onCreate()
if( !Python.isStarted() ) {
Python.start( AndroidPlatform( this ) )
}
}
}
將App
新增到 AndroidManifest.xml
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:tools="http://schemas.android.com/tools">
<application
android:name=".App"
...
</application>
</manifest>
那麼在MainActivity
中, 我們就可以使用Python.getInstance
(否則的話會出現PyException
異常),
val py = Python.getInstance()
val module = py.getModule( "my_module" )
為了使用資料成員, 像my_module.py
中的nums_len
,
val numsLength = module[ "nums_len" ]?.toInt()
println( "Nums Length is $numsLength" )
Nums Length is 10
存取物件ops
類中的屬性
val ops = module[ "ops" ]!!
println( "Operations: $ops" )
println( "num_ops : ${ ops[ "num_ops" ] }" )
println( "mean func : ${ ops[ "meanOp" ] }" )
Operations: <my_module.Operations object at 0xb9339ce8>
num_ops : 2
mean func : <bound method Operations.mean of <my_module.Operations object at 0xb9339ce8>>
由於Python中的函數是物件, 因此允許將函數作為模組的值進行存取.然後,我們使用PyObject.call
方法來向函數傳遞引數並獲取結果(如果函數返回一個值).
val sumFunc = module[ "sumOp" ]
val sum = sumFunc?.call( intArrayOf( 12 , 25 , 32 ) )
val powFun = module[ "powOp" ]
val pow = powFun?.call( 5 , 2 )
println( "Sum: $sum" )
println( "Pow: $pow" )
Sum: 69
Pow: 25
要從ops
物件存取成員函數,
val meanFunc = ops[ "meanOp" ]
val mean = meanFunc?.call( intArrayOf( 23 , 45 , 12 , 91 ) )
println( "Mean: $mean" )
// OR
val mean = ops.callAttr( "meanOp" , intArrayOf( 23 , 45 , 12 , 91 ) )
println( "Mean: $mean" )
Mean: 42.75
這是一個範例, 其中Python函數使用numpy
並返回型別為np.ndarray
的結果
# my_module.py
import numpy as np
def npMatrixSum( m , n ):
mat = np.ones( ( m , n ) )
mat_sum = np.sum( mat , axis=1 )
return mat_sum
val npSumFunc = module[ "npMatrixSum" ]
val output = npSumFunc?.call( 2 , 3 )
// OR
val output = module.callAttr( "npMatrixSum" , 2 , 3 )
println( "Output: $output" )
println( "Output shape: ${output!![ "shape" ] }")
Output: [3. 3.]
Output shape: (2,)
希望我為您的Android開發工具箱新增了一個新工具! Chaquopy是一個非常好用的工具, 具有清晰明瞭的語法和無需費心安裝的優點.你可以在下一個Android專案中使用它.繼續學習,祝您度過愉快的一天!
完成程式碼可以存取我的GitHub
沒能完全理解這裡的native code具體代指的是什麼, 但是我覺得這裡的意思是不需要在Android應用中引入C/C++程式碼, 也就是說不需要在Android應用中引入NDK. ↩︎