Khả năng tương tác Python

Khả năng tương tác API Python là một yêu cầu quan trọng đối với dự án này. Mặc dù Swift được thiết kế để tích hợp với các ngôn ngữ lập trình khác (và thời gian chạy của chúng), nhưng bản chất của ngôn ngữ động không yêu cầu tích hợp sâu cần thiết để hỗ trợ các ngôn ngữ tĩnh. Python nói riêng được thiết kế để nhúng vào các ứng dụng khác và có API giao diện C đơn giản . Vì mục đích công việc của chúng tôi, chúng tôi có thể cung cấp tính năng nhúng meta, cho phép các chương trình Swift sử dụng API Python như thể chúng đang nhúng trực tiếp chính Python.

Để thực hiện điều này, tập lệnh/chương trình Swift chỉ cần liên kết trình thông dịch Python với mã của nó. Mục tiêu của chúng tôi thay đổi từ "cách chúng tôi làm việc với API Python" thành câu hỏi "làm cách nào để chúng tôi làm cho API Python trở nên tự nhiên, dễ truy cập và dễ dàng tiếp cận từ mã Swift?" Đây không phải là một vấn đề tầm thường - có sự khác biệt đáng kể về thiết kế giữa Swift và Python, bao gồm các phương pháp xử lý lỗi, tính chất siêu năng động của Python, sự khác biệt về cú pháp cấp độ bề mặt giữa hai ngôn ngữ và mong muốn không “thỏa hiệp” những điều mà lập trình viên Swift mong đợi. Chúng tôi cũng quan tâm đến sự tiện lợi và công thái học và cho rằng việc yêu cầu một trình tạo trình bao bọc như SWIG là không thể chấp nhận được.

Cách tiếp cận tổng thể

Cách tiếp cận tổng thể của chúng tôi dựa trên quan sát rằng Python được gõ mạnh nhưng - giống như hầu hết các ngôn ngữ được gõ động - hệ thống kiểu của nó được thực thi trong thời gian chạy. Mặc dù đã có nhiều nỗ lực nhằm trang bị thêm một hệ thống kiểu tĩnh trên nó (ví dụ: mypy , pytypecác hệ thống khác ), nhưng chúng dựa vào các hệ thống kiểu không chắc chắn nên chúng không phải là một giải pháp đầy đủ mà chúng ta có thể dựa vào, và hơn nữa chúng còn cản trở nhiều về cơ sở thiết kế giúp Python và các thư viện của nó thực sự tuyệt vời.

Nhiều người coi Swift là một ngôn ngữ được gõ tĩnh và do đó đi đến kết luận rằng giải pháp phù hợp là đưa dạng chất lỏng của Python vào một lỗ được xác định tĩnh. Tuy nhiên, những người khác nhận ra rằng Swift kết hợp các lợi ích của hệ thống kiểu tĩnh mạnh mẽ với hệ thống kiểu động (thường bị đánh giá thấp!). Thay vì cố gắng buộc hệ thống kiểu động của Python trở thành một thứ không phải như vậy, chúng tôi chọn làm quen với Python ở đúng vị trí của nó và hoàn toàn áp dụng cách tiếp cận kiểu động của nó.

Kết quả cuối cùng của việc này là chúng ta có thể đạt được trải nghiệm Python rất tự nhiên - trực tiếp trong mã Swift. Đây là một ví dụ về giao diện này; mã nhận xét hiển thị cú pháp Python thuần túy để so sánh:

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)

Như bạn có thể thấy, cú pháp ở đây có thể hiểu ngay lập tức đối với một lập trình viên Python: điểm khác biệt chính là Swift yêu cầu các giá trị phải được khai báo trước khi sử dụng (với let hoặc var ) và chúng tôi đã chọn đặt các hàm dựng sẵn của Python như import , type , slice v.v. dưới một Python. không gian tên (đơn giản là để tránh làm lộn xộn phạm vi toàn cầu). Đây là kết quả của sự cân bằng có ý thức giữa việc cố gắng làm cho Python cảm thấy tự nhiên và quen thuộc, đồng thời không ảnh hưởng đến thiết kế chung của ngôn ngữ Swift.

Dòng này được thiết lập thông qua một yêu cầu đơn giản: chúng ta không nên phụ thuộc vào bất kỳ trình biên dịch hoặc tính năng ngôn ngữ dành riêng cho Python nào để đạt được khả năng tương tác Python - nó phải được triển khai hoàn toàn dưới dạng thư viện Swift. Xét cho cùng, mặc dù Python cực kỳ quan trọng đối với cộng đồng máy học, nhưng vẫn có những ngôn ngữ động khác (Javascript, Ruby, v.v.) có chỗ đứng vững chắc trong các lĩnh vực khác và chúng tôi không muốn mỗi lĩnh vực này tạo ra sự phức tạp vô tận. sang ngôn ngữ Swift.

Bạn có thể thấy cách triển khai hiện tại của lớp cầu nối của chúng tôi trong Python.swift . Đây là mã Swift thuần túy hoạt động với Swift chưa sửa đổi.

Hạn chế của phương pháp này

Bởi vì chúng tôi chọn nắm bắt bản chất năng động của Python trong Swift, nên chúng tôi nhận được cả ưu và nhược điểm mà các ngôn ngữ động mang lại. Cụ thể, nhiều lập trình viên Swift đã mong đợi và phụ thuộc vào khả năng hoàn thành mã tuyệt vời cũng như đánh giá cao sự thoải mái khi trình biên dịch bắt lỗi chính tả và các lỗi nhỏ khác cho họ trong thời gian biên dịch. Ngược lại, các lập trình viên Python không có những khả năng này (thay vào đó, các lỗi thường được phát hiện trong thời gian chạy) và vì chúng tôi đang tận dụng tính chất năng động của Python nên API Python trong Swift hoạt động theo cách tương tự.

Sau khi xem xét cẩn thận với cộng đồng Swift, rõ ràng đây là sự cân bằng: triết lý và hệ thống giá trị của Swift có thể được áp dụng lên hệ sinh thái thư viện Python ở mức độ nào... mà không vi phạm những điều chân thực và đẹp đẽ về Python và thư viện của nó? Cuối cùng, chúng tôi kết luận rằng mô hình lấy Python làm trung tâm là sự thỏa hiệp tốt nhất: chúng ta nên chấp nhận thực tế rằng Python là một ngôn ngữ động, rằng nó sẽ không bao giờ và không bao giờ có thể hoàn thành mã hoàn hảo và phát hiện lỗi tại thời gian biên dịch tĩnh.

Làm thế nào nó hoạt động

Chúng tôi ánh xạ hệ thống kiểu động của Python thành một kiểu Swift tĩnh duy nhất có tên PythonObject và cho phép PythonObject nhận bất kỳ giá trị Python động nào trong thời gian chạy (tương tự như cách tiếp cận của Abadi và cộng sự ). PythonObject tương ứng trực tiếp với PyObject* được sử dụng trong các liên kết Python C và có thể thực hiện bất kỳ điều gì mà giá trị Python thực hiện trong Python. Ví dụ: điều này hoạt động giống như bạn mong đợi trong 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".

Vì chúng tôi không muốn làm ảnh hưởng đến thiết kế chung của Swift nên chúng tôi hạn chế tất cả hành vi của Python ở các biểu thức liên quan đến loại PythonObject này. Điều này đảm bảo rằng ngữ nghĩa của mã Swift thông thường không thay đổi, ngay cả khi nó trộn, khớp, giao tiếp và xen kẽ với các giá trị Python.

Khả năng tương tác cơ bản

Kể từ Swift 4.0, mức độ tương tác cơ bản hợp lý đã có thể đạt được trực tiếp thông qua các tính năng ngôn ngữ hiện có: chúng tôi chỉ định nghĩa PythonObject là một cấu trúc Swift bao bọc một lớp Swift PyReference riêng tư, cho phép Swift đảm nhận trách nhiệm đếm tham chiếu 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
  ...
}

Tương tự, chúng ta có thể triển khai func + (và phần còn lại của các toán tử Python được hỗ trợ) trên PythonObject theo giao diện thời gian chạy Python hiện có. Việc thực hiện của chúng tôi trông như thế này:

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

Chúng tôi cũng làm cho PythonObject tuân theo Sequence và các giao thức khác, cho phép mã như thế này hoạt động:

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

Hơn nữa, vì PythonObject tuân thủ MutableCollection , nên bạn có toàn quyền truy cập vào API Swift cho Collections , bao gồm các chức năng như map , filter , sort , v.v.

Chuyển đổi sang và từ các giá trị Swift

Giờ đây Swift có thể biểu diễn và vận hành trên các giá trị Python, điều quan trọng là có thể chuyển đổi giữa các kiểu gốc Swift như IntArray<Float> và các kiểu tương đương Python. Điều này được xử lý bởi giao thức PythonConvertible - mà các loại Swift cơ bản như Int tuân thủ và các loại bộ sưu tập Swift như ArrayDictionary tuân thủ có điều kiện (khi các phần tử của chúng tuân thủ). Điều này làm cho các chuyển đổi phù hợp một cách tự nhiên với mô hình Swift.

Ví dụ: nếu bạn biết mình cần số nguyên Swift hoặc bạn muốn chuyển đổi số nguyên Swift sang Python, bạn có thể sử dụng:

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

Tương tự, các kiểu tổng hợp như mảng hoạt động theo cùng một cách:

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

Điều này hoàn toàn phù hợp với mô hình mà lập trình viên Swift mong đợi: các chuyển đổi không thành công được chiếu thành các kết quả tùy chọn (giống như chuyển đổi "chuỗi thành int"), mang lại sự an toàn và khả năng dự đoán mà các lập trình viên Swift mong đợi.

Cuối cùng, vì bạn có quyền truy cập vào toàn bộ sức mạnh của Python nên tất cả các khả năng phản chiếu thông thường của Python cũng có sẵn trực tiếp, bao gồm Python.type , Python.id , Python.dir và mô-đun inspect Python.

Những thách thức về khả năng tương tác

Sự hỗ trợ ở trên là có thể vì thiết kế của Swift hướng tới và đánh giá cao mục tiêu về khả năng mở rộng cú pháp ở cấp độ thư viện của các loại. Chúng tôi cũng may mắn khi Python và Swift chia sẻ cú pháp cấp độ bề mặt rất giống nhau cho các biểu thức (toán tử và lệnh gọi hàm/phương thức). Điều đó cho thấy, có một số thách thức mà chúng tôi gặp phải do các giới hạn về khả năng mở rộng cú pháp của Swift 4.0 và những khác biệt về thiết kế có chủ ý mà chúng tôi cần phải khắc phục.

Tra cứu thành viên động

Mặc dù Swift là ngôn ngữ có thể mở rộng nói chung, việc tra cứu thành viên nguyên thủy không phải là một tính năng có thể mở rộng thư viện. Cụ thể, với một biểu thức có dạng xy , loại x không thể kiểm soát những gì xảy ra khi thành viên y được truy cập trên đó. Nếu loại x đã khai báo tĩnh một thành viên có tên y thì biểu thức này sẽ được giải quyết, nếu không nó sẽ bị trình biên dịch từ chối.

Trong giới hạn của Swift, chúng tôi đã xây dựng một liên kết giải quyết được vấn đề này. Ví dụ: việc triển khai quyền truy cập thành viên theo PyObject_GetAttrStringPyObject_SetAttrString của Python là rất đơn giản. Mã được phép này như:

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

Tuy nhiên, có lẽ tất cả chúng ta đều đồng ý rằng điều này không đạt được mục tiêu cung cấp giao diện tự nhiên và tiện dụng để làm việc với các giá trị Python! Ngoài ra, nó không cung cấp bất kỳ khả năng nào để làm việc với Swift L-Values: không có cách nào để đánh vần tương đương với ax += 1 . Hai vấn đề này cùng nhau là một khoảng cách biểu cảm đáng kể.

Sau khi thảo luận với cộng đồng Swift , giải pháp cho vấn đề này là cho phép mã thư viện triển khai hook dự phòng để xử lý việc tra cứu thành viên không thành công. Tính năng này tồn tại trong nhiều ngôn ngữ động bao gồm Objective-C và do đó, chúng tôi đã đề xuất và triển khai SE-0195: Giới thiệu Loại "Tra cứu thành viên động" do người dùng xác định , cho phép loại tĩnh cung cấp trình xử lý dự phòng cho các tra cứu chưa được giải quyết. Đề xuất này đã được cộng đồng Swift thảo luận rất lâu thông qua quy trình Swift Evolution và cuối cùng đã được chấp nhận. Nó đã được vận chuyển kể từ Swift 4.1.

Do đó, thư viện khả năng tương tác của chúng tôi có thể triển khai hook sau:

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

Điều này cho phép đoạn mã trên được thể hiện đơn giản như sau:

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

... và cú pháp ax += 1 tự nhiên hoạt động giống như chúng ta mong đợi. Điều này cho thấy lợi ích to lớn của việc có thể phát triển toàn bộ ngôn ngữ, thư viện và ứng dụng của nó cùng nhau để đạt được mục tiêu.

Các loại có thể gọi động

Ngoài việc tra cứu thành viên, chúng tôi còn có một thách thức tương tự khi gọi các giá trị. Các ngôn ngữ động thường có khái niệm về giá trị "có thể gọi được" , có thể lấy chữ ký tùy ý, nhưng Swift 4.1 không hỗ trợ điều đó. Ví dụ: kể từ Swift 4.1, thư viện khả năng tương tác của chúng tôi có thể hoạt động với API Python thông qua giao diện như sau:

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

Mặc dù có thể hoàn thành công việc với điều này nhưng rõ ràng nó không đạt được mục tiêu của chúng tôi về sự tiện lợi và công thái học.

Đánh giá vấn đề này với cộng đồng Swift và #2 , chúng tôi nhận thấy rằng Python và Swift hỗ trợ cả đối số được đặt tên và không được đặt tên: các đối số được đặt tên được chuyển vào dưới dạng từ điển. Đồng thời, các ngôn ngữ có nguồn gốc từ Smalltalk bổ sung thêm một điểm đặc biệt: tham chiếu phương thức là đơn vị nguyên tử, bao gồm tên cơ sở của phương thức cùng với bất kỳ đối số từ khóa nào. Mặc dù khả năng tương tác với phong cách ngôn ngữ này không quan trọng đối với Python, nhưng chúng tôi muốn đảm bảo rằng Swift không bị dồn vào một góc cản trở khả năng tương tác tuyệt vời với Ruby, Squeak và các ngôn ngữ có nguồn gốc từ SmallTalk khác.

Giải pháp của chúng tôi, được triển khai trong Swift 5 , là giới thiệu thuộc tính @dynamicCallable mới để chỉ ra rằng một loại (như PythonObject ) có thể xử lý độ phân giải cuộc gọi động. Tính năng @dynamicCallable đã được triển khai và cung cấp trong mô-đun tương tác 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")

Chúng tôi cho rằng điều này khá hấp dẫn và thu hẹp khoảng cách về tính biểu cảm và công thái học còn lại tồn tại trong những trường hợp này. Chúng tôi tin rằng tính năng này sẽ là một giải pháp tốt cho Ruby, Squeak và các ngôn ngữ động khác, cũng như là một tính năng ngôn ngữ Swift nói chung hữu ích có thể áp dụng cho các thư viện Swift khác.

Xử lý ngoại lệ và xử lý lỗi

Cách tiếp cận xử lý ngoại lệ của Python tương tự như C++ và nhiều ngôn ngữ khác, trong đó bất kỳ biểu thức nào cũng có thể đưa ra ngoại lệ bất kỳ lúc nào và người gọi có thể chọn xử lý chúng (hoặc không) một cách độc lập. Ngược lại, cách tiếp cận xử lý lỗi của Swift làm cho "khả năng ném" trở thành một phần rõ ràng trong hợp đồng API của phương thức và buộc người gọi phải xử lý (hoặc ít nhất là thừa nhận) rằng lỗi có thể được đưa ra.

Đây là khoảng cách cố hữu giữa hai ngôn ngữ và chúng tôi không muốn che đậy sự khác biệt này bằng phần mở rộng ngôn ngữ. Giải pháp hiện tại của chúng tôi cho vấn đề này được xây dựng dựa trên quan sát rằng mặc dù bất kỳ lệnh gọi hàm nào cũng có thể thực hiện được nhưng hầu hết các cuộc gọi đều không thực hiện được. Hơn nữa, do Swift xử lý lỗi rõ ràng trong ngôn ngữ, nên việc lập trình viên Python-in-Swift cũng nghĩ về việc họ mong đợi lỗi sẽ ở đâu và có thể bắt được ở đâu. Chúng tôi thực hiện việc này bằng phép chiếu .throwing rõ ràng trên PythonObject . Đây là một ví dụ:

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

Và tất nhiên, điều này tích hợp với tất cả các cơ chế thông thường do xử lý lỗi Swift cung cấp, bao gồm khả năng sử dụng try? nếu bạn muốn xử lý lỗi nhưng không quan tâm đến các chi tiết có trong ngoại lệ.

Tình trạng và triển khai hiện tại

Như đã đề cập ở trên, việc triển khai thư viện khả năng tương tác Python hiện tại của chúng tôi có sẵn trên GitHub trong tệp Python.swift . Trong thực tế, chúng tôi nhận thấy rằng nó hoạt động tốt trong nhiều trường hợp sử dụng. Tuy nhiên, có một số điều còn thiếu mà chúng tôi cần tiếp tục phát triển và tìm ra:

Việc cắt lát Python tổng quát hơn cú pháp cắt lát của Swift. Ngay bây giờ bạn có thể có toàn quyền truy cập vào nó thông qua hàm Python.slice(a, b, c) . Tuy nhiên, chúng ta nên kết nối cú pháp phạm vi a...b bình thường từ Swift và có thể thú vị khi xem xét việc triển khai các toán tử sải bước như một phần mở rộng cho cú pháp phạm vi cơ bản đó. Chúng ta cần điều tra và giải quyết mô hình phù hợp để sử dụng cho việc phân lớp con của các lớp Python. Hiện tại không có cách nào để làm cho một cấu trúc như PythonObject hoạt động với việc khớp mẫu tuple, vì vậy chúng tôi sử dụng các thuộc tính chiếu như .tuple2 . Nếu điều này trở thành một vấn đề trong thực tế, chúng tôi có thể điều tra việc thêm nó vào Swift, nhưng hiện tại chúng tôi không nghĩ rằng nó sẽ là một vấn đề đủ đáng để giải quyết trong thời gian tới.

Tóm tắt và kết luận

Chúng tôi cảm thấy hài lòng về hướng đi này và nghĩ rằng có một số khía cạnh thú vị của công việc này: thật tuyệt khi không có thay đổi cụ thể nào về Python trong trình biên dịch hoặc ngôn ngữ Swift. Chúng tôi có thể đạt được khả năng tương tác Python tốt thông qua thư viện được viết bằng Swift bằng cách soạn thảo các tính năng ngôn ngữ độc lập với Python. Chúng tôi tin rằng các cộng đồng khác sẽ có thể soạn thảo bộ tính năng tương tự để tích hợp trực tiếp với các ngôn ngữ động (và thời gian chạy của chúng) vốn quan trọng đối với các cộng đồng khác (ví dụ: JavaScript, Ruby, v.v.).

Một khía cạnh thú vị khác của công việc này là việc hỗ trợ Python hoàn toàn độc lập với TensorFlow khác và logic phân biệt tự động mà chúng tôi đang xây dựng như một phần của Swift cho TensorFlow. Đây là một tiện ích mở rộng thường hữu ích cho hệ sinh thái Swift, có thể độc lập, hữu ích cho việc phát triển phía máy chủ hoặc bất kỳ thứ gì khác muốn tương tác với các API Python hiện có.