Python 상호 운용성

Python API 상호 운용성은 이 프로젝트의 중요한 요구 사항입니다. Swift는 다른 프로그래밍 언어(및 해당 런타임)와 통합되도록 설계되었지만 동적 언어의 특성상 정적 언어를 지원하는 데 필요한 심층 통합이 필요하지 않습니다. 특히 Python은 다른 응용 프로그램에 내장되도록 설계되었으며 간단한 C 인터페이스 API를 가지고 있습니다. 우리 작업의 목적을 위해 Swift 프로그램이 Python 자체를 직접 포함하는 것처럼 Python API를 사용할 수 있도록 하는 메타 임베딩을 제공할 수 있습니다.

이를 달성하기 위해 Swift 스크립트/프로그램은 단순히 Python 인터프리터를 코드에 연결합니다. 우리의 목표는 "Python API로 어떻게 작업합니까?"에서 "Python API를 어떻게 자연스럽고 접근 가능하며 Swift 코드에서 쉽게 접근할 수 있도록 만들 수 있습니까?"라는 질문으로 변경됩니다. 이것은 사소한 문제가 아닙니다. 오류 처리에 대한 접근 방식, Python의 초동적 특성, 두 언어 간의 표면 수준 구문의 차이점, Swift 프로그래머가 기대하는 것을 "타협"합니다. 우리는 또한 편의성과 인체공학에 관심을 갖고 있으며 SWIG와 같은 래퍼 생성기를 요구하는 것은 용납될 수 없다고 생각합니다.

전반적인 접근 방식

우리의 전반적인 접근 방식은 Python이 강력한 유형이지만 대부분의 동적으로 유형이 지정된 언어와 마찬가지로 해당 유형 시스템이 런타임에 적용된다는 관찰을 기반으로 합니다. 그 위에 정적 유형 시스템 (예: mypy , pytype기타 )을 개조하려는 시도가 많이 있었지만 건전하지 않은 유형 시스템에 의존하므로 우리가 의존할 수 있는 완전한 솔루션이 아니며 더 나아가 많은 단점을 보완했습니다. Python과 그 라이브러리를 정말 훌륭하게 만드는 디자인 전제.

많은 사람들은 Swift를 정적으로 유형이 지정된 언어로 보고 Python의 유동적 형태를 정적으로 정의된 구멍에 집어넣는 것이 올바른 솔루션이라는 결론에 도달합니다. 그러나 다른 사람들은 Swift가 강력한 정적 유형 시스템의 이점을 (종종 과소평가되는!) 동적 유형 시스템과 결합한다는 것을 알고 있습니다. Python의 동적 유형 시스템을 Python의 동적 유형 시스템이 아닌 것으로 강제하려고 시도하는 대신 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 사용). import , type , slice 같은 Python 내장 함수를 넣기로 선택했습니다. Python. (단순히 전역 범위를 복잡하게 만드는 것을 피하기 위해) 이는 Python을 자연스럽고 친숙하게 느끼도록 하는 동시에 Swift 언어의 글로벌 디자인을 손상시키지 않는 것 사이의 의식적인 균형의 결과입니다.

이 선은 간단한 요구 사항을 통해 설정됩니다. 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 값을 취하도록 허용합니다( Adi 등 의 접근 방식과 유사). 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 유형과 관련된 표현식으로 제한합니다. 이는 Python 값과 혼합, 일치, 인터페이스 및 섞이는 경우에도 일반 Swift 코드의 의미가 변경되지 않은 상태로 유지되도록 보장합니다.

기본 상호 운용성

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 런타임 인터페이스 측면에서 PythonObjectfunc + (및 지원되는 나머지 Python 연산자)를 구현할 수 있습니다. 우리의 구현은 다음과 같습니다:

// 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 따르기 때문에 map , filter , sort 등과 같은 함수를 포함하여 컬렉션용 Swift API 에 대한 전체 액세스 권한을 얻습니다.

Swift 값과의 변환

이제 Swift는 Python 값을 표현하고 작동할 수 있으므로 IntArray<Float> 와 같은 Swift 기본 유형과 Python에 상응하는 유형 간에 변환할 수 있는 것이 중요해졌습니다. 이는 Int 와 같은 기본 Swift 유형이 준수하고 ArrayDictionary 와 같은 Swift 컬렉션 유형이 조건부로 준수되는 PythonConvertible 프로토콜에 의해 처리됩니다(해당 요소가 준수되는 경우). 이렇게 하면 변환이 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 프로그래머가 기대하는 모델에 정확히 들어맞습니다. 실패할 수 있는 변환은 선택적인 결과("문자열에서 정수로" 변환과 마찬가지로)로 투영되어 Swift 프로그래머가 기대하는 안전성과 예측 가능성을 제공합니다.

마지막으로 Python의 모든 기능에 액세스할 수 있으므로 Python.type , Python.id , Python.dir 및 Python inspect 모듈을 포함하여 Python의 모든 일반적인 반사 기능도 직접 사용할 수 있습니다.

상호 운용성 문제

위의 지원은 Swift의 디자인이 유형의 라이브러리 수준 구문 확장성 목표를 목표로 하고 이를 높이 평가하기 때문에 가능합니다. 운 좋게도 Python과 Swift가 표현식(연산자 및 함수/메서드 호출)에 대해 매우 유사한 표면 수준 구문을 공유합니다. 즉, Swift 4.0의 구문 확장성 한계와 극복해야 할 의도적인 디자인 차이로 인해 우리가 직면한 몇 가지 과제가 있습니다.

동적 회원 조회

Swift는 일반적으로 확장 가능한 언어이지만 기본 멤버 조회는 라이브러리 확장 기능이 아닙니다. 특히 xy 형식의 표현식이 주어지면 x 유형은 멤버 y 에 액세스할 때 발생하는 일을 제어할 수 없습니다. x 유형이 y 라는 멤버를 정적으로 선언한 경우 이 표현식은 해결되고, 그렇지 않으면 컴파일러에서 거부됩니다.

Swift의 제약 내에서 우리는 이 문제를 해결하는 바인딩을 구축했습니다 . 예를 들어, Python의 PyObject_GetAttrStringPyObject_SetAttrString 측면에서 멤버 액세스를 구현하는 것은 간단했습니다. 이는 다음과 같은 코드를 허용했습니다.

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

그러나 이것이 Python 값 작업에 자연스럽고 인체공학적인 인터페이스를 제공하려는 목표를 달성하지 못한다는 점에 우리 모두 동의할 수 있습니다! 그 외에도 Swift L-값 작업에 대한 어포던스를 제공하지 않습니다. ax += 1 에 해당하는 철자를 입력할 방법이 없습니다. 이 두 가지 문제는 함께 상당한 표현력 격차를 가져왔습니다.

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에서 구현된 우리의 솔루션은 새로운 @dynamicCallable 속성을 도입하여 유형(예: PythonObject )이 동적 호출 해결을 처리할 수 있음을 나타내는 것입니다. @dynamicCallable 기능이 PythonKit interop 모듈에 구현되어 제공되었습니다.

// 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의 오류 처리 접근 방식은 "throwability"를 메소드 API 계약의 명시적인 부분으로 만들고 호출자가 오류가 발생할 수 있음을 처리(또는 최소한 승인) 하도록 강제합니다.

이는 두 언어 사이에 본질적인 차이가 있으며, 우리는 언어 확장을 통해 이 차이점을 다루고 싶지 않습니다. 이에 대한 현재 솔루션은 모든 함수 호출이 발생할 있지만 대부분의 호출은 발생하지 않는다는 관찰을 기반으로 합니다. 게다가, Swift가 언어에서 오류 처리를 명시적으로 만든다는 점을 고려하면, Python-in-Swift 프로그래머가 오류가 발생하고 포착될 것으로 예상되는 위치에 대해서도 생각하는 것이 합리적입니다. 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 등)에 중요한 동적 언어(및 해당 런타임)와 직접 통합할 수 있을 것이라고 믿습니다.

이 작업의 또 다른 흥미로운 측면은 Python 지원이 다른 TensorFlow 및 우리가 Swift for TensorFlow의 일부로 구축 중인 자동 차별화 논리와 완전히 독립적이라는 것입니다. 이는 독립형으로 사용할 수 있는 Swift 생태계에 대한 일반적으로 유용한 확장으로, 서버 측 개발이나 기존 Python API와 상호 운용하려는 기타 모든 것에 유용합니다.