إمكانية التشغيل البيني بيثون

تعد قابلية التشغيل البيني لـ Python API مطلبًا مهمًا لهذا المشروع. بينما تم تصميم Swift للتكامل مع لغات البرمجة الأخرى (وأوقات تشغيلها)، فإن طبيعة اللغات الديناميكية لا تتطلب التكامل العميق اللازم لدعم اللغات الثابتة. تم تصميم Python على وجه الخصوص ليتم تضمينها في تطبيقات أخرى ولها واجهة برمجة تطبيقات بسيطة لواجهة C. ولأغراض عملنا، يمكننا توفير تضمين ميتا، والذي يسمح لبرامج Swift باستخدام واجهات برمجة تطبيقات Python كما لو أنها تقوم بدمج Python نفسها مباشرة.

ولتحقيق ذلك، يقوم البرنامج النصي/البرنامج Swift ببساطة بربط مترجم Python بالكود الخاص به. يتغير هدفنا من "كيف نعمل مع واجهات برمجة تطبيقات Python" إلى سؤال "كيف نجعل واجهات برمجة تطبيقات Python تبدو طبيعية، ويمكن الوصول إليها، ويسهل الوصول إليها من خلال كود Swift؟" هذه ليست مشكلة تافهة - هناك اختلافات كبيرة في التصميم بين Swift وPython، بما في ذلك أساليبهم في معالجة الأخطاء، والطبيعة الديناميكية الفائقة لـ Python، والاختلافات في بناء الجملة على مستوى السطح بين اللغتين، والرغبة في عدم القيام بذلك. "تسوية" الأشياء التي يتوقعها مبرمجو Swift. نحن نهتم أيضًا بالراحة وبيئة العمل ونعتقد أنه من غير المقبول المطالبة بمولد غلاف مثل SWIG.

النهج العام

يعتمد نهجنا العام على ملاحظة أن لغة بايثون مكتوبة بقوة ولكن - مثل معظم اللغات المكتوبة ديناميكيًا - يتم فرض نظام الكتابة الخاص بها في وقت التشغيل. على الرغم من وجود العديد من المحاولات لتحديث نظام كتابة ثابت فوقه (على سبيل المثال، mypy و pytype وغيرها )، إلا أنها تعتمد على أنظمة كتابة غير سليمة لذا فهي ليست حلاً كاملاً يمكننا الاعتماد عليه، علاوة على أنها تتعارض مع العديد من الأنظمة من أماكن التصميم التي تجعل بايثون ومكتباتها رائعة حقًا.

يرى العديد من الأشخاص أن لغة Swift هي لغة مكتوبة بشكل ثابت، وبالتالي يقفزون إلى استنتاج مفاده أن الحل الصحيح هو إدخال الشكل المرن لبايثون في حفرة محددة بشكل ثابت. ومع ذلك، يدرك آخرون أن Swift يجمع بين فوائد نظام كتابة ثابت قوي ونظام كتابة ديناميكي (غالبًا ما لا يحظى بالتقدير!). بدلاً من محاولة إجبار نظام الكتابة الديناميكي في بايثون على أن يكون شيئًا ليس كذلك، اخترنا أن نلتقي ببايثون حيث هي وتبني منهجها المكتوب ديناميكيًا بشكل كامل.

والنتيجة النهائية لذلك هي أنه يمكننا تحقيق تجربة بايثون طبيعية جدًا - مباشرةً في كود Swift. هنا مثال على ما يبدو عليه هذا؛ يعرض الكود الذي تم التعليق عليه صيغة بايثون النقية للمقارنة:

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)

كما ترون، فإن بناء الجملة هنا مفهوم على الفور لمبرمج بايثون: الاختلافات الرئيسية هي أن سويفت يتطلب الإعلان عن القيم قبل الاستخدام (مع let أو var ) وأننا اخترنا وضع وظائف بايثون المضمنة مثل import و type و slice الخ تحت Python. مساحة الاسم (ببساطة لتجنب ازدحام النطاق العالمي). وهذا نتيجة للتوازن الواعي بين محاولة جعل لغة بايثون تبدو طبيعية ومألوفة، مع عدم المساس بالتصميم العالمي للغة Swift.

تم إنشاء هذا الخط من خلال متطلب بسيط: لا ينبغي أن نعتمد على أي مترجم أو ميزات لغة خاصة بـ Python لتحقيق التشغيل المتداخل لـ Python - بل يجب تنفيذه بالكامل كمكتبة Swift. بعد كل شيء، على الرغم من أهمية بايثون بشكل لا يصدق لمجتمع التعلم الآلي، إلا أن هناك لغات ديناميكية أخرى (جافا سكريبت وروبي وما إلى ذلك) لها موطئ قدم قوي في مجالات أخرى، ولا نريد أن يفرض كل من هذه المجالات زحفًا لا نهاية له من التعقيد على لغة سويفت.

يمكنك رؤية التنفيذ الحالي لطبقة التجسير الخاصة بنا في Python.swift . هذا كود Swift خالص يعمل مع Swift غير المعدل.

حدود هذا النهج

لأننا اخترنا احتضان الطبيعة الديناميكية لبيثون في Swift، فإننا نحصل على الإيجابيات والسلبيات التي تجلبها اللغات الديناميكية معها. على وجه التحديد، أصبح العديد من مبرمجي Swift يتوقعون ويعتمدون على إكمال التعليمات البرمجية المذهل ويقدرون الراحة التي يوفرها لهم المترجم الذي يلتقط الأخطاء المطبعية وغيرها من الأخطاء التافهة في وقت الترجمة. في المقابل، لا يمتلك مبرمجو بايثون هذه الإمكانيات (بدلاً من ذلك، عادةً ما يتم اكتشاف الأخطاء في وقت التشغيل)، ولأننا نحتضن طبيعة بايثون الديناميكية، فإن واجهات برمجة تطبيقات بايثون في سويفت تعمل بنفس الطريقة.

بعد دراسة متأنية مع مجتمع Swift، أصبح من الواضح أن هذا يمثل توازنًا: ما مقدار الفلسفة ونظام القيم الخاص بـ Swift الذي يمكن إسقاطه على النظام البيئي لمكتبة Python... دون كسر تلك الأشياء الحقيقية والجميلة حول Python ومكتباتها؟ في النهاية، خلصنا إلى أن النموذج المرتكز على بايثون هو أفضل حل وسط: يجب أن نتقبل حقيقة أن بايثون هي لغة ديناميكية، وأنها لن ولن تتمكن أبدًا من إكمال التعليمات البرمجية بشكل مثالي واكتشاف الأخطاء في وقت الترجمة الثابتة.

كيف تعمل

نقوم بتخطيط نظام الكتابة الديناميكي لـ Python في نوع Swift واحد ثابت يسمى PythonObject ، ونسمح لـ PythonObject بأخذ أي قيمة ديناميكية لـ Python في وقت التشغيل (على غرار نهج Abadi et al. ). يتوافق PythonObject مباشرة مع PyObject* المستخدم في روابط Python C، ويمكنه فعل أي شيء تفعله قيمة 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 ، فإنك تحصل على حق الوصول الكامل إلى Swift APIs for Collections ، بما في ذلك وظائف مثل map filter sort وما إلى ذلك.

التحويلات من وإلى قيم سويفت

الآن بعد أن أصبح بإمكان 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: يتم عرض التحويلات الفاشلة إلى نتائج اختيارية (تمامًا مثل تحويلات "السلسلة إلى int")، مما يوفر الأمان والقدرة على التنبؤ التي يتوقعها مبرمجو 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's PyObject_GetAttrString و PyObject_SetAttrString . هذا الكود المسموح به مثل:

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

ومع ذلك، ربما يمكننا أن نتفق جميعًا على أن هذا لا يحقق هدفنا المتمثل في توفير واجهة طبيعية ومريحة للعمل مع قيم بايثون! أبعد من ذلك، فهو لا يوفر أي إمكانية للعمل مع 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، أصبحت مكتبة التشغيل التفاعلي لدينا قادرة على العمل مع Python APIs من خلال واجهة مثل هذه:

// 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 الأخرى.

معالجة الاستثناءات مقابل معالجة الأخطاء

يشبه أسلوب بايثون في التعامل مع الاستثناءات أسلوب 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 . من الناحية العملية، وجدنا أنه يعمل بشكل جيد للعديد من حالات الاستخدام. ومع ذلك، هناك بعض الأشياء المفقودة التي نحتاج إلى مواصلة تطويرها واكتشافها:

يعد تقطيع بايثون أكثر عمومية من بناء جملة التقطيع الخاص بـ Swift. يمكنك الآن الوصول إليها بشكل كامل من خلال وظيفة Python.slice(a, b, c) . ومع ذلك، يجب علينا توصيل صيغة النطاق a...b العادية من Swift، وقد يكون من المثير للاهتمام التفكير في تنفيذ عوامل التشغيل كامتداد لبناء جملة النطاق الأساسي. نحن بحاجة إلى التحقق من النموذج الصحيح وتسويته لاستخدامه في التصنيف الفرعي لفئات بايثون. لا توجد حاليًا طريقة لجعل بنية مثل PythonObject تعمل مع مطابقة نمط المجموعة، لذلك نستخدم خصائص الإسقاط مثل .tuple2 . إذا أصبحت هذه مشكلة في الممارسة العملية، فيمكننا التحقق من إضافتها إلى Swift، لكننا حاليًا لا نعتقد أنها ستكون مشكلة كافية تستحق الحل على المدى القريب.

الملخص و الاستنتاج

نشعر بالرضا تجاه هذا الاتجاه ونعتقد أن هناك العديد من الجوانب المثيرة للاهتمام في هذا العمل: إنه لأمر رائع أنه لا توجد تغييرات محددة في Python في مترجم Swift أو اللغة. نحن قادرون على تحقيق قابلية التشغيل البيني الجيدة لـ Python من خلال مكتبة مكتوبة بلغة Swift من خلال إنشاء ميزات لغة مستقلة عن Python. نحن نعتقد أن المجتمعات الأخرى ستكون قادرة على إنشاء نفس مجموعة الميزات للتكامل مباشرة مع اللغات الديناميكية (وأوقات التشغيل الخاصة بها) التي تعتبر مهمة للمجتمعات الأخرى (مثل JavaScript وRuby وما إلى ذلك).

جانب آخر مثير للاهتمام في هذا العمل هو أن دعم Python مستقل تمامًا عن TensorFlow الآخر ومنطق التمايز التلقائي الذي نبنيه كجزء من Swift for TensorFlow. يعد هذا امتدادًا مفيدًا بشكل عام لنظام Swift البيئي الذي يمكن أن يكون مستقلاً ومفيدًا للتطوير من جانب الخادم أو أي شيء آخر يريد التفاعل مع واجهات برمجة تطبيقات Python الحالية.