Interoperabilitas Python

Interoperabilitas API Python merupakan persyaratan penting untuk proyek ini. Meskipun Swift dirancang untuk berintegrasi dengan bahasa pemrograman lain (dan runtime-nya), sifat bahasa dinamis tidak memerlukan integrasi mendalam yang diperlukan untuk mendukung bahasa statis. Python khususnya dirancang untuk ditanamkan ke aplikasi lain dan memiliki antarmuka C API yang sederhana . Untuk keperluan pekerjaan kami, kami dapat menyediakan meta-embedding, yang memungkinkan program Swift menggunakan API Python seolah-olah program tersebut secara langsung menyematkan Python itu sendiri.

Untuk mencapai hal ini, skrip/program Swift cukup menghubungkan juru bahasa Python ke dalam kodenya. Sasaran kami berubah dari "bagaimana kami bekerja dengan API Python" menjadi pertanyaan "bagaimana kami membuat API Python terasa alami, mudah diakses, dan mudah dijangkau dari kode Swift?" Ini bukan masalah sepele - ada perbedaan desain yang signifikan antara Swift dan Python, termasuk pendekatan mereka terhadap penanganan kesalahan, sifat super-dinamis dari Python, perbedaan sintaksis tingkat permukaan antara kedua bahasa, dan keinginan untuk tidak melakukannya. "mengkompromikan" hal-hal yang diharapkan oleh pemrogram Swift. Kami juga peduli dengan kenyamanan dan ergonomi dan berpendapat bahwa memerlukan generator pembungkus seperti SWIG adalah hal yang tidak dapat diterima.

Pendekatan keseluruhan

Pendekatan keseluruhan kami didasarkan pada pengamatan bahwa Python diketik dengan kuat tetapi - seperti kebanyakan bahasa yang diketik secara dinamis - sistem tipenya diterapkan saat runtime. Meskipun ada banyak upaya untuk melakukan retrofit sistem tipe statis di atasnya (misalnya mypy , pytype dan lain-lain ), mereka mengandalkan sistem tipe yang tidak sehat sehingga ini bukan solusi lengkap yang dapat kita andalkan, dan lebih jauh lagi, mereka merugikan banyak orang. dari premis desain yang membuat Python dan perpustakaannya benar-benar hebat.

Banyak orang melihat Swift sebagai bahasa yang diketik secara statis dan oleh karena itu menyimpulkan bahwa solusi yang tepat adalah dengan memasukkan bentuk fluida Python ke dalam lubang yang ditentukan secara statis. Namun, yang lain menyadari bahwa Swift menggabungkan keunggulan sistem tipe statis yang kuat dengan sistem tipe dinamis (yang sering kali kurang dihargai!). Daripada mencoba memaksakan sistem tipe dinamis Python menjadi sesuatu yang bukan sistemnya, kami memilih untuk menemui Python di tempatnya dan sepenuhnya menerima pendekatan tipe dinamisnya.

Hasil akhirnya adalah kita dapat mencapai pengalaman Python yang sangat alami - langsung dalam kode Swift. Berikut adalah contoh tampilannya; kode yang dikomentari menunjukkan sintaksis Python murni untuk perbandingan:

import PythonKit

// Python:
//    import numpy as np
//    a = np.arange(15).reshape(3, 5)
//    b = np.array([6, 7, 8])
let np = Python.import("numpy")
let a = np.arange(15).reshape(3, 5)
let b = np.array([6, 7, 8])

// Python:
//    import gzip as gzip
//    import pickle as pickle
let gzip = Python.import("gzip")
let pickle = Python.import("pickle")

// Python:
//    file = gzip.open("mnist.pkl.gz", "rb")
//    (images, labels) = pickle.load(file)
//    print(images.shape)  # (50000, 784)
let file = gzip.open("mnist.pkl.gz", "rb")
let (images, labels) = pickle.load(file).tuple2
print(images.shape) // (50000, 784)

Seperti yang Anda lihat, sintaksis di sini langsung dapat dimengerti oleh pemrogram Python: perbedaan utamanya adalah Swift mengharuskan nilai dideklarasikan sebelum digunakan (dengan let atau var ) dan kami memilih untuk menggunakan fungsi bawaan Python seperti import , type , slice dll di bawah Python. namespace (hanya untuk menghindari kekacauan lingkup global). Hal ini merupakan hasil dari keseimbangan antara mencoba membuat Python terasa alami dan familier, namun tidak mengorbankan desain global bahasa Swift.

Baris ini dibuat melalui persyaratan sederhana: kita tidak boleh bergantung pada kompiler khusus Python atau fitur bahasa apa pun untuk mencapai interop Python - baris ini harus sepenuhnya diimplementasikan sebagai pustaka Swift. Lagi pula, meskipun Python sangat penting bagi komunitas pembelajaran mesin, ada bahasa dinamis lainnya (Javascript, Ruby, dll) yang memiliki pijakan kuat di domain lain, dan kami tidak ingin masing-masing domain ini memaksakan kompleksitas yang tak ada habisnya. ke bahasa Swift.

Anda dapat melihat implementasi lapisan penghubung kami saat ini di Python.swift . Ini adalah kode Swift murni yang berfungsi dengan Swift yang tidak dimodifikasi.

Keterbatasan pendekatan ini

Karena kami memilih untuk menggunakan sifat dinamis Python di Swift, kami mendapatkan pro dan kontra yang dibawa oleh bahasa dinamis. Secara khusus, banyak pemrogram Swift mengharapkan dan bergantung pada penyelesaian kode yang luar biasa dan menghargai kenyamanan kompiler menangkap kesalahan ketik dan bug sepele lainnya pada waktu kompilasi. Sebaliknya, pemrogram Python tidak memiliki kemampuan ini (sebaliknya, bug biasanya terdeteksi saat runtime), dan karena kita menganut sifat dinamis Python, API Python di Swift bekerja dengan cara yang sama.

Setelah mempertimbangkan dengan cermat komunitas Swift, menjadi jelas bahwa ini adalah keseimbangan: seberapa banyak filosofi dan sistem nilai Swift dapat diproyeksikan ke ekosistem perpustakaan Python... tanpa merusak hal-hal yang benar dan indah tentang Python dan perpustakaannya? Pada akhirnya, kami menyimpulkan bahwa model yang berpusat pada Python adalah kompromi terbaik: kita harus menerima kenyataan bahwa Python adalah bahasa yang dinamis, bahwa ia tidak akan pernah dan tidak akan pernah memiliki penyelesaian kode yang sempurna dan deteksi kesalahan pada waktu kompilasi statis.

Bagaimana itu bekerja

Kami memetakan sistem tipe dinamis Python ke dalam satu tipe Swift statis bernama PythonObject , dan mengizinkan PythonObject mengambil nilai dinamis Python apa pun saat runtime (mirip dengan pendekatan Abadi dkk. ). PythonObject berhubungan langsung dengan PyObject* yang digunakan dalam binding Python C, dan dapat melakukan apa pun yang dilakukan nilai Python di Python. Misalnya, ini berfungsi seperti yang Anda harapkan di Python:

var x: PythonObject = 42  // x is an integer represented as a Python value.
print(x + 4)         // Does a Python addition, then prints 46.

x = "stringy now"    // Python values can hold strings, and dynamically change Python type!
print("super " + x)  // Does a Python addition, then prints "super stringy now".

Karena kami tidak ingin mengkompromikan desain global Swift, kami membatasi semua perilaku Python pada ekspresi yang melibatkan tipe PythonObject ini. Hal ini memastikan bahwa semantik kode Swift normal tetap tidak berubah, meskipun kode tersebut mencampur, mencocokkan, menghubungkan, dan membaurkan dengan nilai-nilai Python.

Interoperabilitas dasar

Pada Swift 4.0, tingkat interoperabilitas dasar yang wajar sudah dapat dicapai secara langsung melalui fitur bahasa yang ada: kita cukup mendefinisikan PythonObject sebagai struct Swift yang menggabungkan kelas Swift PyReference pribadi, sehingga memungkinkan Swift untuk mengambil alih tanggung jawab penghitungan referensi Python:

/// Primitive reference to a Python value.  This is always non-null and always
/// owning of the underlying value.
private final class PyReference {
  var state: UnsafeMutablePointer<PyObject>

  init(owned: UnsafeMutablePointer<PyObject>) {
    state = owned
  }

  init(borrowed: UnsafeMutablePointer<PyObject>) {
    state = borrowed
    Py_IncRef(state)
  }

  deinit {
    Py_DecRef(state)
  }
}

// This is the main type users work with.
public struct PythonObject {
  /// This is a handle to the Python object the PythonObject represents.
  fileprivate var state: PyReference
  ...
}

Demikian pula, kita dapat mengimplementasikan func + (dan operator Python lain yang didukung) pada PythonObject dalam kaitannya dengan antarmuka runtime Python yang ada. Implementasi kami terlihat seperti ini:

// Implement the + operator in terms of the standard Python __add__ method.
public static func + (lhs: PythonObject, rhs: PythonObject) -> PythonObject {
  return lhs.__add__.call(with: rhs)
}
// Implement the - operator in terms of the standard Python __sub__ method.
public static func - (lhs: PythonObject, rhs: PythonObject) -> PythonObject {
  return lhs.__sub__.call(with: rhs)
}
// Implement += and -= in terms of + and -, as usual.
public static func += (lhs: inout PythonObject, rhs: PythonObject) {
  lhs = lhs + rhs
}
public static func -= (lhs: inout PythonObject, rhs: PythonObject) {
  lhs = lhs - rhs
}
// etc...

Kami juga membuat PythonObject sesuai dengan Sequence dan protokol lainnya, sehingga memungkinkan kode seperti ini berfungsi:

func printPythonCollection(_ collection: PythonObject) {
  for elt in collection {
    print(elt)
  }
}

Selain itu, karena PythonObject sesuai dengan MutableCollection , Anda mendapatkan akses penuh ke API Swift untuk Collections , termasuk fungsi seperti map , filter , sort , dll.

Konversi ke dan dari nilai Swift

Sekarang Swift dapat mewakili dan mengoperasikan nilai-nilai Python, menjadi penting untuk dapat mengkonversi antara tipe asli Swift seperti Int dan Array<Float> dan setara dengan Python. Hal ini ditangani oleh protokol PythonConvertible - yang sesuai dengan tipe dasar Swift seperti Int , dan tipe koleksi Swift seperti Array dan Dictionary secara kondisional sesuai (ketika elemennya sesuai). Hal ini membuat konversi secara alami cocok dengan model Swift.

Misalnya, jika Anda memerlukan bilangan bulat Swift atau ingin mengonversi bilangan bulat Swift ke Python, Anda dapat menggunakan:

let pyInt = PythonObject(someSwiftInteger)     // Always succeeds.
if let swiftInt = Int(somePythonValue) {  // Succeeds if the Python value is convertible to Int.
  print(swiftInt)
}

Demikian pula, tipe agregat seperti array bekerja dengan cara yang persis sama:

// This succeeds when somePythonValue is a collection of values that are convertible to Int.
if let swiftIntArray = Array<Int>(somePythonValue) {
  print(swiftIntArray)
}

Hal ini sangat sesuai dengan model yang diharapkan oleh pemrogram Swift: konversi yang gagal diproyeksikan menjadi hasil opsional (seperti konversi "string ke int"), memberikan keamanan dan prediktabilitas yang diharapkan oleh pemrogram Swift.

Terakhir, karena Anda memiliki akses ke kekuatan penuh Python, semua kemampuan reflektif normal Python juga tersedia secara langsung, termasuk Python.type , Python.id , Python.dir , dan modul inspect Python.

Tantangan Interoperabilitas

Dukungan di atas dimungkinkan karena desain Swift bertujuan dan menghargai tujuan ekstensibilitas tipe sintaksis tingkat perpustakaan. Kami juga beruntung karena Python dan Swift memiliki sintaks tingkat permukaan yang sangat mirip untuk ekspresi (operator dan pemanggilan fungsi/metode). Meskipun demikian, ada beberapa tantangan yang kami temui karena keterbatasan ekstensibilitas sintaksis Swift 4.0 dan perbedaan desain yang disengaja yang perlu kami atasi.

Pencarian anggota dinamis

Meskipun Swift secara umum merupakan bahasa yang dapat diperluas, pencarian anggota primitif bukanlah fitur yang dapat diperluas ke perpustakaan. Secara khusus, dengan ekspresi bentuk xy , tipe x tidak dapat mengontrol apa yang terjadi ketika anggota y diakses di dalamnya. Jika tipe x secara statis mendeklarasikan anggota bernama y maka ekspresi ini akan diselesaikan, jika tidak maka akan ditolak oleh kompiler.

Dalam batasan Swift, kami membuat pengikatan yang mengatasi hal ini. Misalnya, sangat mudah untuk mengimplementasikan akses anggota dalam PyObject_GetAttrString dan PyObject_SetAttrString Python. Ini mengizinkan kode seperti:

// Python: a.x = a.x + 1
a.set(member: "x", to: a.get(member: "x") + 1)

Namun, kita mungkin semua sepakat bahwa ini tidak mencapai tujuan kita dalam menyediakan antarmuka yang alami dan ergonomis untuk bekerja dengan nilai-nilai Python! Selain itu, ini tidak memberikan biaya apa pun untuk bekerja dengan Swift L-Values: tidak ada cara untuk mengeja yang setara dengan ax += 1 . Kedua masalah ini secara bersama-sama merupakan kesenjangan ekspresifitas yang signifikan.

Setelah berdiskusi dengan komunitas Swift , solusi untuk masalah ini adalah mengizinkan kode perpustakaan mengimplementasikan hook fallback untuk menangani pencarian anggota yang gagal. Fitur ini ada dalam banyak bahasa dinamis termasuk Objective-C , dan dengan demikian, kami mengusulkan dan mengimplementasikan SE-0195: Memperkenalkan Tipe "Pencarian Anggota Dinamis" yang ditentukan pengguna yang memungkinkan tipe statis menyediakan pengendali cadangan untuk pencarian yang belum terselesaikan. Usulan ini dibahas panjang lebar oleh komunitas Swift melalui proses Swift Evolution, dan akhirnya diterima. Telah dikirimkan sejak Swift 4.1.

Sebagai hasilnya, perpustakaan interoperabilitas kami dapat mengimplementasikan hook berikut:

@dynamicMemberLookup
public struct PythonObject {
...
  subscript(dynamicMember member: String) -> PythonObject {
    get {
      return ... PyObject_GetAttrString(...) ...
    }
    set {
      ... PyObject_SetAttrString(...)
    }
  }
}

Yang memungkinkan kode di atas diungkapkan secara sederhana sebagai:

// Python: a.x = a.x + 1
a.x = a.x + 1

... dan sintaks natural ax += 1 berfungsi seperti yang kita harapkan. Hal ini menunjukkan manfaat besar dari kemampuan mengembangkan keseluruhan bahasa, perpustakaan, dan aplikasinya secara bersamaan untuk mencapai tujuan.

Tipe yang dapat dipanggil secara dinamis

Selain pencarian anggota, kami memiliki tantangan serupa dalam hal pemanggilan nilai. Bahasa dinamis sering kali memiliki gagasan tentang nilai "dapat dipanggil" , yang dapat mengambil tanda tangan sewenang-wenang, tetapi Swift 4.1 tidak mendukung hal seperti itu. Misalnya, mulai Swift 4.1, pustaka interoperabilitas kami dapat bekerja dengan API Python melalui antarmuka seperti ini:

// Python: a = np.arange(15).reshape(3, 5)
let a = np.arange.call(with: 15).reshape.call(with: 3, 5)

// Python: d = np.array([1, 2, 3], dtype="i2")
let d = np.array.call(with: [6, 7, 8], kwargs: [("dtype", "i2")])

Meskipun hal ini mungkin dilakukan, hal ini jelas tidak mencapai tujuan kami yaitu kenyamanan dan ergonomis.

Mengevaluasi masalah ini dengan komunitas Swift dan #2 , kami mengamati bahwa Python dan Swift mendukung argumen bernama dan tidak disebutkan namanya: argumen bernama diteruskan sebagai kamus. Pada saat yang sama, bahasa turunan Smalltalk menambah masalah tambahan: referensi metode adalah unit atom, yang menyertakan nama dasar metode beserta argumen kata kunci apa pun. Meskipun interoperabilitas dengan gaya bahasa ini tidak penting untuk Python, kami ingin memastikan bahwa Swift tidak menghalangi interop besar dengan Ruby, Squeak, dan bahasa turunan SmallTalk lainnya.

Solusi kami, yang diterapkan di Swift 5 , adalah memperkenalkan atribut @dynamicCallable baru untuk menunjukkan bahwa suatu tipe (seperti PythonObject ) dapat menangani resolusi panggilan dinamis. Fitur @dynamicCallable telah diimplementasikan dan tersedia di modul interop PythonKit.

// Python: a = np.arange(15).reshape(3, 5)
let a = np.arange(15).reshape(3, 5)

// Python: d = np.array([1, 2, 3], dtype="i2")
let d = np.array([6, 7, 8], dtype: "i2")

Menurut kami hal ini cukup menarik, dan menutup kesenjangan ekspresif dan ergonomis yang ada dalam kasus ini. Kami percaya bahwa fitur ini akan menjadi solusi yang baik untuk Ruby, Squeak, dan bahasa dinamis lainnya, serta menjadi fitur bahasa Swift yang berguna secara umum yang dapat diterapkan pada perpustakaan Swift lainnya.

Penanganan pengecualian vs penanganan kesalahan

Pendekatan Python terhadap penanganan pengecualian mirip dengan C++ dan banyak bahasa lainnya, di mana ekspresi apa pun dapat memunculkan pengecualian kapan saja, dan penelepon dapat memilih untuk menanganinya (atau tidak) secara mandiri. Sebaliknya, pendekatan penanganan kesalahan Swift menjadikan "kemampuan melempar" sebagai bagian eksplisit dari kontrak API suatu metode dan memaksa pemanggil untuk menangani (atau setidaknya mengakui) bahwa kesalahan dapat terjadi.

Ini adalah kesenjangan yang melekat antara kedua bahasa, dan kami tidak ingin menutup-nutupi perbedaan ini dengan perluasan bahasa. Solusi kami saat ini untuk hal ini didasarkan pada pengamatan bahwa meskipun pemanggilan fungsi apa pun dapat dilakukan, sebagian besar panggilan tidak dapat dilakukan. Selain itu, mengingat bahwa Swift membuat penanganan kesalahan secara eksplisit dalam bahasanya, masuk akal bagi pemrogram Python-in-Swift untuk juga memikirkan di mana mereka mengharapkan kesalahan dapat dibuang dan ditangkap. Kami melakukan ini dengan proyeksi .throwing eksplisit pada PythonObject . Berikut ini contohnya:

  // Open a file.  If this fails, the program is terminated, just like an
  // unhandled exception in Python.

  // file = open("foo.txt")
  let file = Python.open("foo.txt")
  // blob = file.read()
  let blob = file.read()

  // Open a file, a thrown "file not found" exception is turned into a Swift error.
  do {
    let file = try Python.open.throwing.dynamicallyCall("foo.txt")
    let blob = file.read()
    ...
  } catch {
    print(error)
  }

Dan tentu saja, ini terintegrasi dengan semua mekanisme normal yang disediakan oleh penanganan kesalahan Swift, termasuk kemampuan untuk menggunakan try? jika Anda ingin menangani kesalahan tetapi tidak peduli dengan detail yang disertakan dalam pengecualian.

Implementasi dan Status Saat Ini

Seperti disebutkan di atas, implementasi pustaka interoperabilitas Python kami saat ini tersedia di GitHub dalam file Python.swift . Dalam praktiknya, kami menemukan bahwa ini berfungsi dengan baik untuk banyak kasus penggunaan. Namun, ada beberapa hal yang kurang yang perlu terus kami kembangkan dan cari tahu:

Pengirisan Python lebih umum daripada sintaksis pengirisan Swift. Saat ini Anda bisa mendapatkan akses penuh melalui fungsi Python.slice(a, b, c) . Namun, kita harus memasukkan sintaks rentang a...b normal dari Swift, dan mungkin menarik untuk mempertimbangkan penerapan operator striding sebagai perpanjangan dari sintaks rentang dasar tersebut. Kita perlu menyelidiki dan menentukan model yang tepat untuk digunakan dalam subkelas kelas Python. Saat ini tidak ada cara untuk membuat struct seperti PythonObject berfungsi dengan pencocokan pola tuple, jadi kami menggunakan properti proyeksi seperti .tuple2 . Jika hal ini menjadi masalah dalam praktiknya, kami dapat menyelidiki penambahan ini ke Swift, namun saat ini menurut kami masalah tersebut tidak akan cukup untuk diselesaikan dalam waktu dekat.

Ringkasan dan Kesimpulan

Kami merasa senang dengan arah ini dan berpikir bahwa ada beberapa aspek menarik dari pekerjaan ini: sangat bagus bahwa tidak ada perubahan spesifik Python dalam kompiler atau bahasa Swift. Kami dapat mencapai interoperabilitas Python yang baik melalui perpustakaan yang ditulis dalam Swift dengan menyusun fitur bahasa yang tidak bergantung pada Python. Kami percaya bahwa komunitas lain akan dapat membuat kumpulan fitur yang sama untuk diintegrasikan secara langsung dengan bahasa dinamis (dan runtime mereka) yang penting bagi komunitas lain (misalnya JavaScript, Ruby, dll).

Aspek menarik lainnya dari pekerjaan ini adalah dukungan Python sepenuhnya independen terhadap TensorFlow lain dan logika diferensiasi otomatis yang kami buat sebagai bagian dari Swift untuk TensorFlow. Ini adalah ekstensi yang umumnya berguna untuk ekosistem Swift yang dapat berdiri sendiri, berguna untuk pengembangan sisi server atau apa pun yang ingin berinteroperasi dengan API Python yang ada.