Python Birlikte Çalışabilirliği

Python API'nin birlikte çalışabilirliği bu proje için önemli bir gerekliliktir. Swift diğer programlama dilleriyle (ve çalışma zamanlarıyla) entegre olacak şekilde tasarlanmış olsa da dinamik dillerin doğası, statik dilleri desteklemek için gereken derin entegrasyonu gerektirmez. Python özellikle diğer uygulamalara gömülmek üzere tasarlanmıştır ve basit bir C arayüzü API'sine sahiptir. Çalışmamızın amaçları doğrultusunda, Swift programlarının Python API'lerini doğrudan Python'un kendisini katıştırıyormuş gibi kullanmasına olanak tanıyan bir meta yerleştirme sağlayabiliriz.

Bunu başarmak için Swift betiği/programı Python yorumlayıcısını kendi koduna bağlar. Hedefimiz "Python API'leriyle nasıl çalışırız" sorusundan "Python API'lerinin doğal, erişilebilir ve Swift kodundan erişilmesi kolay olmasını nasıl sağlarız?" sorusuna dönüşüyor. Bu önemsiz bir sorun değil; Swift ve Python arasında, hata işleme yaklaşımları, Python'un süper dinamik doğası, iki dil arasındaki yüzey düzeyindeki sözdizimindeki farklılıklar ve bunu yapmama arzusu da dahil olmak üzere önemli tasarım farklılıkları var. Swift programcılarının beklediği şeylerden "uzlaşma". Aynı zamanda rahatlık ve ergonomiyi de önemsiyoruz ve SWIG gibi bir sarma oluşturucuya ihtiyaç duymanın kabul edilemez olduğunu düşünüyoruz.

Genel yaklaşım

Genel yaklaşımımız, Python'un güçlü bir şekilde yazıldığı ancak dinamik olarak yazılan dillerin çoğunda olduğu gibi, tür sisteminin çalışma zamanında zorlandığı gözlemine dayanmaktadır. Bunun üzerine statik tipte bir sistemi yenilemek için birçok girişimde bulunulmasına rağmen (örn. mypy , pytype ve diğerleri ), bunlar sağlam olmayan tipteki sistemlere dayanıyorlar, dolayısıyla güvenebileceğimiz tam bir çözüm değiller ve ayrıca birçok kişiye zarar veriyorlar. Python'u ve kütüphanelerini gerçekten harika kılan tasarım öncüllerinden.

Birçok kişi Swift'i statik olarak yazılmış bir dil olarak görüyor ve bu nedenle doğru çözümün Python'un akışkan formunu statik olarak tanımlanmış bir deliğe yerleştirmek olduğu sonucuna varıyor. Ancak diğerleri Swift'in güçlü bir statik tip sistemin avantajlarını (çoğunlukla yeterince takdir edilmeyen!) dinamik tip bir sistemle birleştirdiğinin farkındadır. Python'un dinamik tip sistemini olmadığı bir şey olmaya zorlamak yerine, Python'u olduğu yerde karşılamayı ve onun dinamik olarak yazılmış yaklaşımını tamamen benimsemeyi seçiyoruz.

Bunun nihai sonucu, doğrudan Swift kodunda çok doğal bir Python deneyimi elde edebilmemizdir. İşte bunun neye benzediğine dair bir örnek; yorumlanan kod, karşılaştırma için saf Python sözdizimini gösterir:

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)

Gördüğünüz gibi buradaki sözdizimi bir Python programcısı için hemen anlaşılabilir: temel farklar Swift'in kullanımdan önce değerlerin bildirilmesini gerektirmesi ( let veya var ile) ve biz import , type , slice gibi Python yerleşik fonksiyonlarını koymayı seçmemizdir vb. bir Python. ad alanı (yalnızca küresel kapsamın karmaşıklığını önlemek için). Bu, Swift dilinin küresel tasarımından ödün vermeden Python'u doğal ve tanıdık hissettirmeye çalışmak arasındaki bilinçli dengenin bir sonucudur.

Bu çizgi basit bir gereksinimle oluşturulmuştur: Python birlikte çalışmayı sağlamak için Python'a özgü herhangi bir derleyiciye veya dil özelliğine bağlı kalmamalıyız - tamamen bir Swift kitaplığı olarak uygulanmalıdır. Sonuçta Python, makine öğrenimi topluluğu için inanılmaz derecede önemli olsa da, diğer alanlarda güçlü dayanakları olan başka dinamik diller de (Javascript, Ruby, vb.) var ve bu alanların her birinin sonsuz bir karmaşıklık yaratmasını istemiyoruz. Swift diline.

Köprüleme katmanımızın mevcut uygulamasını Python.swift'te görebilirsiniz. Bu, değiştirilmemiş Swift ile çalışan saf Swift kodudur.

Bu yaklaşımın sınırlamaları

Swift'de Python'un dinamik doğasını benimsemeyi seçtiğimiz için, dinamik dillerin beraberinde getirdiği hem artıları hem de eksileri elde ediyoruz. Özellikle, birçok Swift programcısı harika kod tamamlamayı beklemeye ve buna güvenmeye başladı ve derleyicinin derleme zamanında kendileri için yazım hatalarını ve diğer önemsiz hataları yakalamasının rahatlığını takdir etti. Bunun aksine, Python programcıları bu olanaklara sahip değildir (bunun yerine hatalar genellikle çalışma zamanında yakalanır) ve Python'un dinamik doğasını benimsediğimiz için Swift'deki Python API'leri aynı şekilde çalışır.

Swift topluluğuyla dikkatlice düşündükten sonra bunun bir denge olduğu ortaya çıktı: Swift'in felsefesinin ve değer sisteminin ne kadarı Python kütüphane ekosistemine yansıtılabilir... Python hakkında doğru ve güzel olan şeyleri bozmadan ve kütüphaneleri? Sonunda Python merkezli bir modelin en iyi uzlaşma olduğu sonucuna vardık: Python'un dinamik bir dil olduğu, hiçbir zaman mükemmel kod tamamlama ve statik derleme zamanında hata tespitine sahip olamayacağı gerçeğini benimsememiz gerekiyor.

Nasıl çalışır

Python'un dinamik tür sistemini PythonObject adlı tek bir statik Swift türüyle eşliyoruz ve PythonObject çalışma zamanında herhangi bir dinamik Python değerini almasına izin veriyoruz ( Abadi ve diğerlerinin yaklaşımına benzer şekilde). PythonObject Python C bağlamalarında kullanılan PyObject* e doğrudan karşılık gelir ve Python'da bir Python değerinin yaptığı her şeyi yapabilir. Örneğin, bu tam Python'da beklediğiniz gibi çalışır:

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".

Swift'in genel tasarımından ödün vermek istemediğimiz için Python davranışlarının tamamını bu PythonObject türünü içeren ifadelerle sınırlandırıyoruz. Bu, Python değerleriyle karıştırılsa, eşleştirilse, ara bağlantı oluşturulsa ve karıştırılsa bile normal Swift kodunun semantiğinin değişmeden kalmasını sağlar.

Temel birlikte çalışabilirlik

Swift 4.0'dan itibaren, mevcut dil özellikleri aracılığıyla makul düzeyde bir temel birlikte çalışabilirlik zaten doğrudan elde edilebiliyordu: PythonObject özel bir Swift PyReference sınıfını saran bir Swift yapısı olarak tanımlıyoruz ve Swift'in Python referans sayımı sorumluluğunu üstlenmesine olanak tanıyoruz:

/// 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
  ...
}

Benzer şekilde, func + (ve desteklenen Python operatörlerinin geri kalanını) mevcut Python çalışma zamanı arayüzü açısından PythonObject üzerinde uygulayabiliriz. Uygulamamız şuna benziyor:

// 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...

Ayrıca PythonObject Sequence ve diğer protokollere uygun olmasını sağlayarak aşağıdaki gibi kodların çalışmasına olanak sağlıyoruz:

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

Ayrıca PythonObject MutableCollection ile uyumlu olduğundan, map , filter , sort vb. işlevler de dahil olmak üzere Koleksiyonlar için Swift API'lerine tam erişim elde edersiniz.

Swift değerlerine ve Swift değerlerinden dönüşümler

Artık Swift, Python değerlerini temsil edebildiği ve bunlar üzerinde çalışabildiği için, Int ve Array<Float> gibi Swift yerel türleri ile Python eşdeğerleri arasında dönüşüm yapabilmek önemli hale geliyor. Bu, Int gibi temel Swift türlerinin ve Array ve Dictionary gibi Swift koleksiyon türlerinin koşullu olarak uyduğu (öğeleri uygun olduğunda) PythonConvertible protokolü tarafından gerçekleştirilir. Bu, dönüşümlerin Swift modeline doğal bir şekilde uymasını sağlar.

Örneğin, bir Swift tamsayısına ihtiyacınız olduğunu biliyorsanız veya bir Swift tamsayısını Python'a dönüştürmek istiyorsanız şunu kullanabilirsiniz:

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

Benzer şekilde, diziler gibi toplama türleri de tamamen aynı şekilde çalışır:

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

Bu, bir Swift programcısının bekleyeceği modele tam olarak uyuyor: başarısız dönüşümler isteğe bağlı sonuçlara yansıtılıyor ("string'den int'ye" dönüşümler gibi), Swift programcılarının beklediği güvenliği ve öngörülebilirliği sağlıyor.

Son olarak, Python'un tüm gücüne erişiminiz olduğundan, Python.type , Python.id , Python.dir ve Python inspect modülü dahil olmak üzere Python'un tüm normal yansıtma yeteneklerine de doğrudan erişebilirsiniz.

Birlikte Çalışabilirlik Zorlukları

Yukarıdaki destek mümkündür çünkü Swift'in tasarımı, türlerin kitaplık düzeyinde sözdizimsel genişletilebilirliği hedefini amaçlıyor ve bu hedefi takdir ediyor. Ayrıca Python ve Swift'in ifadeler (operatörler ve işlev/yöntem çağrıları) için çok benzer bir yüzey düzeyinde sözdizimini paylaştıkları için de şanslıyız. Bununla birlikte, Swift 4.0'ın sözdizimi genişletilebilirliğinin sınırları ve üstesinden gelmemiz gereken kasıtlı tasarım farklılıkları nedeniyle karşılaştığımız birkaç zorluk var.

Dinamik üye arama

Swift genel olarak genişletilebilir bir dil olmasına rağmen, ilkel üye arama, kitaplıkta genişletilebilir bir özellik değildi. Spesifik olarak, xy formunun bir ifadesi verildiğinde, x türü, bir y üyesine erişildiğinde ne olacağını kontrol edemiyordu. Eğer x türü statik olarak y adında bir üye bildirmiş olsaydı bu ifade çözümlenirdi, aksi takdirde derleyici tarafından reddedilirdi.

Swift'in kısıtlamaları dahilinde bu sorunu çözen bir bağlama oluşturduk . Örneğin, üye erişimlerini Python'un PyObject_GetAttrString ve PyObject_SetAttrString terimleriyle uygulamak basitti. Bu, aşağıdaki gibi kodlara izin verdi:

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

Ancak bunun Python değerleriyle çalışırken doğal ve ergonomik bir arayüz sağlama hedefimize ulaşmadığı konusunda muhtemelen hepimiz hemfikiriz! Bunun ötesinde, Swift L-Değerleri ile çalışmak için herhangi bir uygunluk sağlamaz: ax += 1 eşdeğerini yazmanın bir yolu yoktur. Bu iki sorun birlikte önemli bir ifade boşluğu oluşturuyordu.

Swift topluluğuyla tartıştıktan sonra bu sorunun çözümü, kütüphane kodunun başarısız üye aramalarını ele almak için bir geri dönüş kancası uygulamasına izin vermektir. Bu özellik, Objective-C de dahil olmak üzere birçok dinamik dilde mevcuttur ve bu nedenle, SE-0195'i önerdik ve uyguladık: Statik bir türün çözülmemiş aramalar için bir geri dönüş işleyicisi sağlamasına olanak tanıyan Kullanıcı Tanımlı "Dinamik Üye Arama" Türlerini Tanıtın . Bu öneri Swift topluluğu tarafından Swift Evolution süreci aracılığıyla uzun uzadıya tartışıldı ve sonuçta kabul edildi. Swift 4.1'den beri gönderiliyor.

Bunun sonucunda birlikte çalışabilirlik kütüphanemiz aşağıdaki kancayı uygulayabilmektedir:

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

Bu, yukarıdaki kodun basitçe şu şekilde ifade edilmesini sağlar:

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

... ve doğal ax += 1 sözdizimi tam da beklediğimiz gibi çalışıyor. Bu, bir hedefe ulaşmak için bir dilin tüm yığınını, kitaplıklarını ve uygulamalarını birlikte geliştirebilmenin büyük faydasını gösterir.

Dinamik olarak çağrılabilir türler

Üye aramanın yanı sıra değerlerin çağrılması konusunda da benzer bir zorlukla karşı karşıyayız. Dinamik diller genellikle keyfi bir imza alabilen "çağrılabilir" değerler kavramına sahiptir, ancak Swift 4.1'in böyle bir şeyi desteklemesi yoktur. Örneğin, Swift 4.1'den itibaren birlikte çalışabilirlik kitaplığımız aşağıdaki gibi bir arayüz aracılığıyla Python API'leriyle çalışabilmektedir:

// 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")])

Bununla işleri halletmek mümkün olsa da kolaylık ve ergonomi hedefimize ulaşmadığımız açıkça görülüyor.

Bu sorunu Swift topluluğu ve #2 ile değerlendirdiğimizde, Python ve Swift'in hem adlandırılmış hem de adlandırılmamış argümanları desteklediğini gözlemliyoruz: adlandırılmış argümanlar bir sözlük olarak iletilir. Aynı zamanda, Smalltalk'tan türetilmiş diller ek bir kırışıklık daha ekler: yöntem referansları, herhangi bir anahtar kelime argümanıyla birlikte yöntemin temel adını içeren atomik birimdir. Bu dil tarzıyla birlikte çalışabilirlik Python için önemli olmasa da Swift'in Ruby, Squeak ve diğer SmallTalk'tan türetilmiş dillerle mükemmel birlikte çalışmayı engelleyen bir köşeye çekilmediğinden emin olmak istiyoruz.

Swift 5'te uygulanan çözümümüz, bir türün ( PythonObject gibi) dinamik çağrı çözümlemesini işleyebileceğini belirtmek için yeni bir @dynamicCallable niteliği sunmaktır. @dynamicCallable özelliği PythonKit birlikte çalışma modülünde uygulandı ve kullanıma sunuldu.

// 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")

Bunun oldukça ikna edici olduğunu ve bu vakalar için kalan ifade gücü ve ergonomik boşluğu kapattığını düşünüyoruz. Bu özelliğin Ruby, Squeak ve diğer dinamik diller için iyi bir çözüm olmasının yanı sıra diğer Swift kütüphanelerine de uygulanabilecek genel olarak kullanışlı bir Swift dil özelliği olacağına inanıyoruz.

İstisna işleme ve hata işleme

Python'un istisna işleme yaklaşımı, herhangi bir ifadenin herhangi bir zamanda bir istisna atabileceği ve arayanların bunları bağımsız olarak ele almayı (ya da almamayı) seçebileceği C++ ve diğer birçok dile benzer. Buna karşılık, Swift'in hata işleme yaklaşımı , "atılabilirliği" bir yöntemin API sözleşmesinin açık bir parçası haline getirir ve arayanları bir hatanın atılabileceğini ele almaya (veya en azından kabul etmeye) zorlar .

Bu, iki dil arasında doğal bir boşluktur ve biz bu farkı bir dil uzantısıyla kapatmak istemiyoruz. Bu soruna yönelik mevcut çözümümüz, herhangi bir işlev çağrısının gerçekleşebilmesine rağmen çoğu çağrının gerçekleşmediği gözlemine dayanmaktadır. Dahası, Swift'in dilde hata işlemeyi açıkça ifade ettiği göz önüne alındığında, Swift'te Python programcısının hataların nerede atılabilir ve yakalanabilir olmasını beklediklerini de düşünmesi mantıklıdır. Bunu PythonObject üzerinde açık bir .throwing projeksiyonu ile yapıyoruz. İşte bir örnek:

  // 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)
  }

Ve elbette bu, try? kullanma yeteneği de dahil olmak üzere Swift hata işleme tarafından sağlanan tüm normal mekaniklerle bütünleşir. hatayı ele almak istiyorsanız ancak istisnanın içerdiği ayrıntıları umursamıyorsanız.

Mevcut Uygulama ve Durum

Yukarıda belirtildiği gibi, Python birlikte çalışabilirlik kitaplığının mevcut uygulaması GitHub'da Python.swift dosyasında mevcuttur. Uygulamada bunun birçok kullanım durumunda iyi çalıştığını gördük. Ancak geliştirmeye devam etmemiz ve çözmemiz gereken bazı eksikler var:

Python dilimleme, Swift'in dilimleme sözdiziminden daha geneldir. Şu anda Python.slice(a, b, c) işlevi aracılığıyla ona tam erişim sağlayabilirsiniz. Bununla birlikte, Swift'in normal a...b aralık sözdizimini bağlamalıyız ve adımlama operatörlerini bu temel aralık sözdiziminin bir uzantısı olarak uygulamayı düşünmek ilginç olabilir. Python sınıflarının alt sınıflandırılmasında kullanılacak doğru modeli araştırıp kararlaştırmamız gerekiyor. Şu anda PythonObject gibi bir yapının demet modeli eşleştirmeyle çalışmasını sağlamanın bir yolu yoktur, bu nedenle .tuple2 gibi projeksiyon özelliklerini kullanırız. Eğer bu pratikte bir sorun haline gelirse bunu Swift'e eklemeyi araştırabiliriz ancak şu anda bunun yakın vadede çözülmeye değer bir sorun olacağını düşünmüyoruz.

Özet ve sonuç

Bu yönde kendimizi iyi hissediyoruz ve bu çalışmanın birkaç ilginç yönünün olduğunu düşünüyoruz: Swift derleyicisinde veya dilinde Python'a özgü herhangi bir değişikliğin olmaması harika. Python'dan bağımsız dil özellikleri oluşturarak Swift'de yazılmış bir kütüphane aracılığıyla iyi bir Python birlikte çalışabilirliği elde edebiliyoruz. Diğer toplulukların, diğer topluluklar için önemli olan dinamik dillerle (ve bunların çalışma zamanlarıyla) (örneğin, JavaScript, Ruby, vb.) doğrudan entegrasyon için aynı özellik setini oluşturabileceklerine inanıyoruz.

Bu çalışmanın bir başka ilginç yönü de Python desteğinin diğer TensorFlow'dan ve Swift for TensorFlow'un bir parçası olarak oluşturduğumuz otomatik farklılaşma mantığından tamamen bağımsız olmasıdır. Bu, Swift ekosisteminin tek başına durabilen, sunucu tarafı geliştirmesi veya mevcut Python API'leriyle birlikte çalışmak isteyen herhangi bir şey için yararlı olan genel olarak yararlı bir uzantısıdır.