Python の相互運用性

Python API の相互運用性は、このプロジェクトの重要な要件です。 Swift は他のプログラミング言語 (およびそのランタイム) と統合するように設計されていますが、動的言語の性質上、静的言語をサポートするために必要な深い統合は必要ありません。特に Python は他のアプリケーションに埋め込まれるように設計されておりシンプルな C インターフェイス APIを備えています。私たちの作業のために、メタ埋め込みを提供できます。これにより、Swift プログラムは、Python 自体を直接埋め込んでいるかのように Python API を使用できるようになります。

これを実現するには、Swift スクリプト/プログラムは Python インタープリターをそのコードにリンクするだけです。私たちの目標は、「Python API をどのように操作するか」から、「Python API を自然でアクセスしやすく、Swift コードから簡単にアクセスできるようにするにはどうすればよいか?」という問題に変わります。これは簡単な問題ではありません。Swift と Python の間には、エラー処理へのアプローチ、Python の超動的性質、2 つの言語間の表面レベルの構文の違い、およびそれらを区別したくないという要望など、設計上の大きな違いがあります。 Swift プログラマーが期待するものを「妥協」します。私たちは利便性と人間工学にも配慮しており、SWIG のようなラッパー ジェネレーターを必要とすることは受け入れられないと考えています。

全体的なアプローチ

私たちの全体的なアプローチは、Python は厳密に型指定されているが、ほとんどの動的型付け言語と同様に、その型システムは実行時に強制されるという観察に基づいています。静的型システムをその上に改良する試みは数多くありますが (例: mypypytypeなど)、それらは不健全な型システムに依存しているため、私たちが信頼できる完全なソリューションではなく、さらに多くの機能を阻害します。 Python とそのライブラリを本当に優れたものにする設計の前提条件について説明します。

多くの人は Swift を静的に型付けされた言語とみなしているため、静的に定義された穴に Python の流体形式を押し込むことが正しい解決策であるという結論に飛びつきます。しかし、Swift が強力な静的型システムの利点と (過小評価されがちな) 動的型システムの利点を組み合わせていることを理解している人もいます。 Python の動的型システムを強制的にそうでないものにしようとするのではなく、私たちは Python の現状に応え、その動的型付けアプローチを完全に受け入れることを選択しました。

この最終結果として、非常に自然な Python エクスペリエンスを Swift コードで直接実現できるようになります。これがどのようなものであるかの例を次に示します。コメントアウトされたコードは、比較のために純粋な Python 構文を示しています。

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)

ご覧のとおり、ここでの構文は Python プログラマにとってすぐに理解できます。主な違いは、Swift では使用前に値を宣言する必要があること ( letまたはvarを使用) と、 importtypesliceなどのPython 組み込み関数を配置することを選択したことです。 Python.名前空間 (単にグローバル スコープが乱雑になるのを避けるため)。これは、Swift 言語のグローバルな設計を損なうことなく、Python を自然で親しみやすいものにしようとする意識的なバランスの結果です。

この境界線は単純な要件によって確立されます。つまり、Python の相互運用性を実現するために Python 固有のコンパイラや言語機能に依存すべきではなく、完全に Swift ライブラリとして実装される必要があります。結局のところ、Python は機械学習コミュニティにとって非常に重要ですが、他のドメインに強力な足場を持つ他の動的言語 (JavaScript、Ruby など) があり、これらの各ドメインが際限なく複雑さを増長することは望んでいません。 Swift言語に移行します。

ブリッジ層の現在の実装はPython.swiftで確認できます。これは、変更されていない Swift で動作する純粋な Swift コードです。

このアプローチの制限

Swift では Python の動的な性質を採用することを選択したため、動的言語がもたらす長所と短所の両方が得られます。具体的には、多くの Swift プログラマーは、驚くべきコード補完を期待し依存するようになり、コンパイラーがタイプミスやその他の些細なバグをコンパイル時にキャッチしてくれる快適さを高く評価するようになりました。対照的に、Python プログラマーにはこれらのアフォーダンスがありません (代わりに、通常、バグは実行時に検出されます)。また、Python の動的な性質を受け入れているため、Swift の Python API は同じように機能します。

Swift コミュニティと慎重に検討した結果、これはバランスであることが明らかになりました。Python の真実で美しいものを壊すことなく、Swift の哲学と価値体系を Python ライブラリ エコシステムにどれだけ投影できるかです。そしてそのライブラリは?最終的に、私たちは Python 中心のモデルが最善の妥協策であると結論付けました。つまり、Python は動的言語であり、静的コンパイル時に完璧なコード補完とエラー検出を行うことは決して不可能であるという事実を受け入れる必要があります。

使い方

Python の動的型システムをPythonObjectという名前の単一の静的 Swift 型にマップし、 PythonObject実行時に任意の動的 Python 値を取れるようにします ( Abadi et al.のアプローチと同様)。 PythonObject Python C バインディングで使用されるPyObject*に直接対応し、Python 値が Python で行うことはすべて実行できます。たとえば、これは 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".

Swift のグローバルな設計を妥協したくないため、Python のすべての動作をこのPythonObjectタイプに関係する式に制限します。これにより、通常の Swift コードが Python 値と混合、一致、インターフェース、および混在している場合でも、そのコードのセマンティクスが変更されないことが保証されます。

基本的な相互運用性

Swift 4.0 の時点で、適切なレベルの基本的な相互運用性は、既存の言語機能を通じてすでに直接達成可能でした。単にPythonObjectプライベート Swift PyReferenceクラスをラップする Swift 構造体として定義するだけで、Swift が 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
  ...
}

同様に、既存の Python ランタイム インターフェイスの観点から、 func + (およびサポートされている残りの Python 演算子) をPythonObjectに実装できます。私たちの実装は次のようになります。

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

また、 PythonObject Sequenceやその他のプロトコルに準拠させ、次のようなコードが動作できるようにします。

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

さらに、 PythonObject MutableCollectionに準拠しているため、 mapfiltersortなどの関数を含むCollections の Swift APIに完全にアクセスできます。

Swift 値との変換

Swift が Python 値を表現して操作できるようになったので、 IntArray<Float>などの Swift ネイティブ型と Python の同等の型の間で変換できることが重要になります。これは、 PythonConvertibleプロトコルによって処理されますIntなどの基本的な Swift 型はこのプロトコルに準拠し、 ArrayDictionaryなどの Swift コレクション型も条件付きで (要素が準拠する場合に) 準拠します。これにより、変換が Swift モデルに自然に適合します。

たとえば、Swift 整数が必要であることがわかっている場合、または Swift 整数を Python に変換したい場合は、次を使用できます。

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

同様に、配列などの集約タイプもまったく同じように機能します。

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

これは、Swift プログラマーが期待するモデルに正確に当てはまります。失敗する可能性のある変換は (「文字列から int」への変換と同様に) オプションの結果に投影され、Swift プログラマーが期待する安全性と予測可能性が提供されます。

最後に、Python の全機能にアクセスできるため、 Python.typePython.idPython.dir 、および Python inspectモジュールを含む、Python のすべての通常のリフレクション機能も直接利用できます。

相互運用性の課題

上記のサポートが可能となるのは、Swift の設計が型のライブラリ レベルの構文拡張性という目標を目指しており、それを認識しているためです。また、幸いなことに、Python と Swift は、式 (演算子と関数/メソッド呼び出し) の表面レベルの構文が非常によく似ています。とはいえ、Swift 4.0 の構文拡張性の制限と意図的な設計の違いにより、克服する必要のある課題がいくつかあります。

動的メンバー検索

Swift は一般に拡張可能な言語ですが、プリミティブ メンバーの検索はライブラリで拡張可能な機能ではありませんでした。具体的には、形式xyの式を指定すると、 xの型は、メンバーyアクセスされたときに何が起こるかを制御できませんでした。 xの型がyという名前のメンバーを静的に宣言していれば、この式は解決されますが、そうでない場合はコンパイラによって拒否されます。

Swift の制約内で、これを回避するバインディングを構築しました。たとえば、Python のPyObject_GetAttrStringおよびPyObject_SetAttrStringを使用してメンバー アクセスを実装するのは簡単でした。これにより、次のようなコードが許可されました。

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

ただし、これでは Python 値を操作するための自然で人間工学に基づいたインターフェイスを提供するという目標を達成できないことに、おそらく誰もが同意するでしょう。それ以上に、Swift L-Value を操作するためのアフォーダンスは提供されません。 ax += 1に相当するものを綴る方法はありません。これら 2 つの問題を合わせると、表現力に大きなギャップが生じます。

Swiftコミュニティとの議論の結果、この問題の解決策は、失敗したメンバー検索を処理するフォールバック フックをライブラリ コードに実装できるようにすることです。この機能はObjective-C を含む多くの動的言語に存在するため、静的型が未解決の検索に対するフォールバック ハンドラーを提供できるようにする SE-0195: ユーザー定義の「動的メンバー検索」型の導入を提案および実装しました。この提案は、Swift Evolution プロセスを通じてSwift コミュニティによって徹底的に議論され、最終的に受け入れられました。 Swift 4.1 から出荷されています。

この結果、相互運用性ライブラリは次のフックを実装できるようになりました。

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

これにより、上記のコードは次のように単純に表現できます。

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

...自然なax += 1構文は期待どおりに機能します。これは、目標を達成するために、言語、そのライブラリ、およびアプリケーションのフルスタックを一緒に進化させることができるという大きな利点を示しています。

動的に呼び出し可能な型

メンバーの検索に加えて、値の呼び出しに関しても同様の課題があります。動的言語には多くの場合、任意の署名を取ることができる「呼び出し可能な」値の概念がありますが、Swift 4.1 はそのようなものをサポートしていません。たとえば、Swift 4.1 では、相互運用ライブラリは次のようなインターフェイスを通じて Python API と連携できます。

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

これで物事を成し遂げることは可能ですが、利便性と人間工学という私たちの目標を達成できていないことは明らかです。

Swift コミュニティと #2でこの問題を評価すると、Python と Swift が名前付き引数と名前なし引数の両方をサポートしていることがわかります。名前付き引数は辞書として渡されます。同時に、Smalltalk 派生言語にはさらなる問題が加わります。メソッド参照はアトミック単位であり、キーワード引数とともにメソッドの基本名が含まれます。このスタイルの言語との相互運用性は Python にとって重要ではありませんが、Swift が隅に追いやられて Ruby、Squeak、およびその他の SmallTalk 派生言語との優れた相互運用性を妨げないようにしておきたいと考えています。

Swift 5 で実装された私たちの解決策は、型 ( PythonObjectなど) が動的呼び出し解決を処理できることを示す新しい@dynamicCallable属性を導入することです。 @dynamicCallable機能が実装され、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")

これは非常に説得力があり、これらのケースに存在する残りの表現力と人間工学的なギャップを埋めるものであると私たちは考えています。私たちは、この機能が Ruby、Squeak、その他の動的言語にとって優れたソリューションであるだけでなく、他の Swift ライブラリにも適用できる一般的に便利な Swift 言語機能であると信じています。

例外処理とエラー処理

Python の例外処理のアプローチは C++ や他の多くの言語と似ており、どの式でもいつでも例外をスローでき、呼び出し元は例外を個別に処理するかどうかを選択できます。対照的に、Swift のエラー処理アプローチでは、「スロー可能性」をメソッドの API コントラクトの明示的な部分にし、呼び出し元にエラーがスローされる可能性があることを処理 (または少なくとも承認) するよう強制します

これは 2 つの言語間の固有のギャップであり、この違いを言語拡張で覆い隠すつもりはありません。これに対する現在の解決策は、どの関数呼び出しでもスローされる可能性があるにもかかわらず、ほとんどの呼び出しではスローされないという観察に基づいています。さらに、Swift が言語内でエラー処理を明示的にしていることを考えると、Swift での Python プログラマーは、エラーがスロー可能およびキャッチ可能であると予想される場所についても考慮するのが合理的です。これは、 PythonObjectの明示的な.throwingプロジェクションを使用して行います。以下に例を示します。

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

そしてもちろん、これは、 try?を使用する機能を含む、Swift エラー処理によって提供されるすべての通常の仕組みと統合されています。エラーを処理したいが、例外に含まれる詳細は気にしない場合。

現在の実装とステータス

上で述べたように、Python 相互運用性ライブラリの現在の実装は、GitHub のPython.swiftファイルで入手できます。実際に、これが多くのユースケースでうまく機能することがわかりました。ただし、不足しているものがいくつかあるため、開発を続けて解明する必要があります。

Python のスライスは、Swift のスライス構文よりも一般的です。現時点ではPython.slice(a, b, c)関数を通じて完全にアクセスできます。ただし、Swift の通常のa...b範囲構文で配線する必要があり、その基本的な範囲構文の拡張としてストライド演算子の実装を検討することは興味深いかもしれません。 Python クラスのサブクラス化に使用する適切なモデルを調査して決定する必要があります。現在、 PythonObjectのような構造体をタプル パターン マッチングで動作させる方法はないため、 .tuple2のような射影プロパティを使用します。これが実際上問題になる場合は、これを Swift に追加することを検討できますが、現時点では、短期的に解決する価値があるほど十分な問題ではないと考えています。

要約と結論

私たちはこの方向性に満足しており、この取り組みにはいくつかの興味深い側面があると考えています。Swift コンパイラーまたは言語に Python 固有の変更がないのは素晴らしいことです。 Python に依存しない言語機能を構成することで、Swift で書かれたライブラリを通じて Python の優れた相互運用性を実現できます。私たちは、他のコミュニティも同じ機能セットを構成して、他のコミュニティにとって重要な動的言語 (およびそのランタイム) (JavaScript、Ruby など) と直接統合できるようになると信じています。

この研究のもう 1 つの興味深い点は、Python サポートが他の TensorFlow や、Swift for TensorFlow の一部として構築している自動微分ロジックから完全に独立していることです。これは、スタンドアロンで使用できる Swift エコシステムへの一般に便利な拡張機能で、サーバー側の開発や、既存の Python API と相互運用したいその他のあらゆるものに役立ちます。