在Android應用中通過Chaquopy使用Python

2023-04-02 21:00:30

在Android應用中通過Chaquopy使用Python [譯]

通過Python指令碼和包為Android應用帶來更多的功能

翻譯自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來進行構建和使用.

Chaquopy是什麼? 它是如何讓Python在Android中執行的?

和大多數跨語言介面工作原理一樣, 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, 它可以下載為直譯器下載包. 像NumPySciPy這樣的受歡迎的包可以通過原生程式碼執行高密集的CPU計算, 我們需要事先安裝這些包. 因此, Chaquopy團隊維護了自己的儲存庫,其中包含專門為Android的ARM架構構建的本地軟體包. 這些軟體包的維護者不會為Android平臺構建他們的原生程式碼,因為使用者數量較少,因此Chaquopy團隊會針對Android平臺構建它們並通過自己的儲存庫進行釋出.

對於純粹的Python包, 不需要額外的構建並且Chaquopy可以直接執行這些.從更宏觀來看, Chaquopy包含了三個主要的元件.

  1. Chaquopy Gradle Plugin
  2. Chaquopy Runtime
  3. Package Repository

1.在Android專案中新增Chaquopy

1.1 Gradle依賴和ABI規則

在新/現有的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的大小,同時仍保持最大限度的裝置相容性.

1.2 Python版本和PIP包

下一步, 我們將設定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包列表.

通過Java/Kotlin獲取Python物件

在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


  1. 沒能完全理解這裡的native code具體代指的是什麼, 但是我覺得這裡的意思是不需要在Android應用中引入C/C++程式碼, 也就是說不需要在Android應用中引入NDK. ↩︎