Совместимость Python

Совместимость API Python является важным требованием для этого проекта. Хотя Swift предназначен для интеграции с другими языками программирования (и их средами выполнения), природа динамических языков не требует глубокой интеграции, необходимой для поддержки статических языков. Python, в частности , предназначен для встраивания в другие приложения и имеет простой API-интерфейс C. Для целей нашей работы мы можем предоставить мета-встраивание, которое позволяет программам Swift использовать API-интерфейсы Python, как если бы они непосредственно встраивали сам Python.

Для этого сценарий/программа Swift просто связывает интерпретатор Python со своим кодом. Наша цель меняется с «как мы работаем с API-интерфейсами Python» на вопрос «как сделать API-интерфейсы Python естественными, доступными и простыми в использовании из кода Swift?» Это нетривиальная проблема — между Swift и Python существуют существенные различия в дизайне, включая их подходы к обработке ошибок, супердинамическую природу Python, различия в синтаксисе поверхностного уровня между двумя языками и желание не «скомпрометировать» то, чего ожидают программисты Swift. Мы также заботимся об удобстве и эргономичности и считаем недопустимым требовать такой генератор оберток, как SWIG.

Общий подход

Наш общий подход основан на наблюдении, что Python строго типизирован, но, как и большинство динамически типизированных языков, его система типов применяется во время выполнения. Хотя было предпринято множество попыток модернизировать систему статических типов поверх нее (например, mypy , pytype и другие ), они опираются на ненадежные системы типов, поэтому не являются полным решением, на которое мы можем положиться, и, кроме того, они противоречат многим предпосылок проектирования, которые делают 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 ), и что мы решили поместить встроенные функции Python, такие как import , type , slice . и т.д. под Python. пространство имен (просто чтобы не загромождать глобальную область видимости). Это результат сознательного баланса между попыткой сделать Python естественным и знакомым, не ставя при этом под угрозу глобальный дизайн языка Swift.

Эта линия устанавливается посредством простого требования: мы не должны зависеть от какого-либо специфичного для Python компилятора или функций языка для достижения взаимодействия с Python — оно должно быть полностью реализовано как библиотека Swift. В конце концов, хотя Python невероятно важен для сообщества машинного обучения, существуют и другие динамические языки (Javascript, Ruby и т. д.), которые имеют прочные позиции в других областях, и мы не хотим, чтобы каждая из этих областей вызывала бесконечное увеличение сложности. на язык Swift.

Вы можете увидеть текущую реализацию нашего мостового слоя в Python.swift . Это чистый код Swift, который работает с немодифицированным Swift.

Ограничения этого подхода

Поскольку мы решили использовать динамическую природу Python в Swift, мы получаем как плюсы, так и минусы, которые приносят с собой динамические языки. В частности, многие программисты Swift привыкли ожидать и зависеть от великолепного завершения кода и ценят удобство, когда компилятор выявляет опечатки и другие тривиальные ошибки во время компиляции. Напротив, программисты Python не имеют таких возможностей (вместо этого ошибки обычно обнаруживаются во время выполнения), и поскольку мы принимаем динамическую природу Python, API-интерфейсы Python в Swift работают таким же образом.

После тщательного обсуждения с сообществом Swift стало ясно, что это баланс: какая часть философии и системы ценностей Swift может быть спроецирована на экосистему библиотек Python... не нарушая при этом тех вещей, которые верны и прекрасны в Python. и его библиотеки? В конце концов мы пришли к выводу, что модель, ориентированная на Python, является лучшим компромиссом: мы должны принять тот факт, что Python — динамический язык, что он никогда не будет и никогда не сможет иметь идеальное завершение кода и обнаружение ошибок во время статической компиляции.

Как это работает

Мы сопоставляем систему динамических типов Python с одним статическим типом Swift с именем PythonObject и позволяем PythonObject принимать любое динамическое значение Python во время выполнения (аналогично подходу Абади и др. ). PythonObject напрямую соответствует PyObject* , используемому в привязках Python C, и может делать все, что значение 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, которая обертывает частный класс Swift PyReference , позволяя 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
  ...
}

Аналогичным образом мы можем реализовать func + (и остальные поддерживаемые операторы Python) для PythonObject в терминах существующего интерфейса среды выполнения 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 , вы получаете полный доступ к API Swift для коллекций , включая такие функции, как map , filter , sort и т. д.

Преобразования в значения Swift и обратно

Теперь, когда Swift может представлять значения Python и работать с ними, становится важным иметь возможность конвертировать собственные типы Swift, такие как Int и Array<Float> , и их эквиваленты Python. Это обрабатывается протоколом PythonConvertible , которому соответствуют базовые типы Swift, такие как Int , и условное соответствие типам коллекций Swift, таким как Array и Dictionary (когда их элементы соответствуют). Благодаря этому преобразования естественным образом вписываются в модель 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 также доступны напрямую, включая Python.type , Python.id , Python.dir и модуль 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-Values: невозможно написать эквивалент ax += 1 . Вместе эти две проблемы представляли собой значительный разрыв в выразительности.

После обсуждения с сообществом Swift решение этой проблемы состоит в том, чтобы разрешить библиотечному коду реализовать резервный хук для обработки неудачных поисков членов . Эта функция существует во многих динамических языках , включая Objective-C , и поэтому мы предложили и реализовали SE-0195: введение определяемых пользователем типов «динамического поиска членов» , который позволяет статическому типу предоставлять резервный обработчик для неразрешенных поисков. Это предложение подробно обсуждалось сообществом Swift в рамках процесса Swift Evolution и в конечном итоге было принято. Он поставляется начиная с 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, наша библиотека совместимости может работать с API Python через такой интерфейс:

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

// 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 метода и заставляет вызывающие объекты обрабатывать (или, по крайней мере, подтверждать) , что ошибка может быть выдана.

Это неотъемлемый разрыв между двумя языками, и мы не хотим сглаживать эту разницу языковым расширением. Наше нынешнее решение этой проблемы основано на наблюдении, что, хотя любой вызов функции может вызвать ошибку, большинство вызовов этого не делают. Более того, учитывая, что Swift делает обработку ошибок явной в языке, программисту Python-in-Swift разумно также подумать о том, где, по его мнению, ошибки будут выдаваться и перехватываться. Мы делаем это с помощью явной проекции .throwing на PythonObject . Вот пример:

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

И, конечно же, это интегрируется со всеми обычными механизмами обработки ошибок Swift, включая возможность использовать try? если вы хотите обработать ошибку, но вас не волнуют детали, включенные в исключение.

Текущая реализация и статус

Как упоминалось выше, наша текущая реализация библиотеки совместимости Python доступна на GitHub в файле Python.swift . На практике мы обнаружили, что это хорошо работает во многих случаях использования. Однако нам не хватает нескольких вещей, которые нам нужно продолжать развивать и выяснять:

Нарезка в Python является более общей, чем синтаксис нарезки в Swift. Прямо сейчас вы можете получить к нему полный доступ через функцию Python.slice(a, b, c) . Однако нам следует подключить обычный синтаксис диапазона a...b из Swift, и было бы интересно рассмотреть возможность реализации операторов шагания в качестве расширения этого базового синтаксиса диапазона. Нам нужно изучить и выбрать правильную модель для создания подклассов классов Python. В настоящее время нет способа заставить такую ​​структуру, как PythonObject работать с сопоставлением шаблонов кортежей, поэтому мы используем свойства проекции, такие как .tuple2 . Если это станет проблемой на практике, мы можем изучить возможность добавления этого в Swift, но в настоящее время мы не думаем, что это будет достаточно серьезной проблемой, чтобы ее стоило решать в ближайшем будущем.

Резюме и заключение

Мы хорошо относимся к этому направлению и считаем, что есть несколько интересных аспектов этой работы: здорово, что в компиляторе или языке Swift нет специфичных для Python изменений. Мы можем добиться хорошей совместимости Python с помощью библиотеки, написанной на Swift, путем создания независимых от Python функций языка. Мы считаем, что другие сообщества смогут создать тот же набор функций для прямой интеграции с динамическими языками (и их средами выполнения), которые важны для других сообществ (например, JavaScript, Ruby и т. д.).

Еще один интересный аспект этой работы заключается в том, что поддержка Python полностью независима от другой логики TensorFlow и автоматического дифференцирования, которую мы создаем как часть Swift для TensorFlow. Это в целом полезное расширение экосистемы Swift, которое может быть самостоятельным и полезным для разработки на стороне сервера или чего-либо еще, что требует взаимодействия с существующими API-интерфейсами Python.