יכולת פעולה הדדית של פייתון

יכולת פעולה הדדית של Python API היא דרישה חשובה לפרויקט זה. בעוד ש- Swift תוכננה להשתלב עם שפות תכנות אחרות (וזמני הריצה שלהן), טבען של שפות דינמיות אינו מצריך את האינטגרציה העמוקה הדרושה לתמיכה בשפות סטטיות. Python במיוחד נועד להיות מוטמע ביישומים אחרים ויש לו ממשק C פשוט API . למטרות העבודה שלנו, אנו יכולים לספק מטה הטמעה, המאפשרת לתוכניות Swift להשתמש בממשקי API של Python כאילו הן מטמיעות ישירות את Python עצמה.

כדי להשיג זאת, התסריט/התוכנית של Swift פשוט מקשרת את מתורגמן Python לקוד שלו. המטרה שלנו משתנה מ"איך אנחנו עובדים עם ממשקי API של Python" לשאלה של "איך אנחנו גורמים לממשקי API של Python להרגיש טבעיים, נגישים וקלים להגיע אליהם מקוד Swift?" זו לא בעיה טריוויאלית - ישנם הבדלי עיצוב משמעותיים בין סוויפט לפייתון, כולל הגישות שלהם לטיפול בשגיאות, האופי הסופר-דינמי של פייתון, ההבדלים בתחביר ברמת פני השטח בין שתי השפות, והרצון שלא "להתפשר" על הדברים שמתכנתי Swift למדו לצפות להם. אכפת לנו גם מנוחות וארגונומיה וחושבים שזה לא מקובל לדרוש מחולל עטיפה כמו SWIG.

גישה כוללת

הגישה הכוללת שלנו מבוססת על התצפית ש-Python מוקלדת בצורה חזקה אבל - כמו רוב השפות המוקלדות בצורה דינמית - מערכת הסוג שלה נאכפת בזמן ריצה. אמנם היו ניסיונות רבים להרכיב מערכת סטטית על גביה (למשל mypy , pytype ואחרות ), הם מסתמכים על מערכות מסוג לא תקינות כך שהן אינן פתרון מלא שאנחנו יכולים לסמוך עליו, ויתרה מכך הם חותכים נגד רבים של הנחות העיצוב שהופכות את Python והספריות שלה באמת נהדרות.

אנשים רבים רואים בסוויפט שפה מודפסת סטטית ולכן קופצים למסקנה שהפתרון הנכון הוא להנחיל את הצורה הנוזלית של פייתון לחור מוגדר סטטית. עם זאת, אחרים מבינים שסוויפט משלבת את היתרונות של מערכת חזקה מסוג סטטי עם מערכת דינמית (לעיתים קרובות לא מוערכת!). במקום לנסות לאלץ את מערכת הטיפוס הדינמי של Python להיות משהו שהיא לא, אנו בוחרים לפגוש את Python היכן שהיא נמצאת ולאמץ באופן מלא את הגישה המוקלדת הדינמית שלה.

התוצאה הסופית של זה היא שנוכל להשיג חווית Python טבעית מאוד - ישירות בקוד 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)

כפי שאתה יכול לראות, התחביר כאן מובן מיד למתכנת Python: ההבדלים העיקריים הם ש-Swift דורשת הצהרה על ערכים לפני השימוש (עם let או var ) ושבחרנו לשים פונקציות מובנות של Python כמו import , type , slice וכו' תחת Python. מרחב השמות (פשוט כדי למנוע עומס בהיקף הגלובלי). זוהי תוצאה של איזון מודע בין הניסיון לגרום לפיתון להרגיש טבעי ומוכר, תוך אי פגיעה בעיצוב הגלובלי של שפת הסוויפט.

קו זה נוצר באמצעות דרישה פשוטה: אנחנו לא צריכים להיות תלויים בשום מהדר או תכונות שפה ספציפיות לפייתון כדי להשיג אינטררופ של Python - זה צריך להיות מיושם לחלוטין כספריית Swift. אחרי הכל, בעוד ש-Python חשובה להפליא לקהילת למידת המכונה, יש שפות דינמיות אחרות (Javascript, Ruby וכו') שיש להן אחיזה חזקה בתחומים אחרים, ואנחנו לא רוצים שכל אחד מהתחומים האלה יטיל שחילה אינסופית של מורכבות. על שפת סוויפט.

אתה יכול לראות את היישום הנוכחי של שכבת הגישור שלנו ב- Python.swift . זהו קוד Swift טהור שעובד עם Swift ללא שינוי.

מגבלות של גישה זו

מכיוון שאנו בוחרים לאמץ את האופי הדינמי של Python ב-Swift, אנו מקבלים גם את היתרונות וגם את החסרונות ששפות דינמיות מביאות איתן. באופן ספציפי, מתכנתים רבים של Swift למדו לצפות ותלויים בהשלמת קוד מדהימה ומעריכים את הנוחות בכך שהמהדר יתפוס עבורם שגיאות הקלדה ובאגים טריוויאליים אחרים בזמן ההידור. לעומת זאת, למתכנתי Python אין את האפשרויות הללו (במקום זאת, באגים נתפסים בדרך כלל בזמן ריצה), ומכיוון שאנו מאמצים את האופי הדינמי של Python, ממשקי API של Python ב-Swift עובדים באותה צורה.

לאחר בדיקה מדוקדקת עם קהילת סוויפט, התברר כי מדובר באיזון: כמה מהפילוסופיה ומערכת הערכים של הסוויפט ניתן להקרין על מערכת האקולוגית של ספריית פייתון... מבלי לשבור את הדברים הנכונים והיפים על פייתון והספריות שלו? בסופו של דבר, הגענו למסקנה שמודל ממוקד פייתון הוא הפשרה הטובה ביותר: עלינו לאמץ את העובדה שפייתון היא שפה דינמית, שלעולם לא תהיה ולעולם לא תהיה לה השלמת קוד מושלמת וזיהוי שגיאות בזמן הידור סטטי.

איך זה עובד

אנו ממפים את מערכת הטיפוס הדינמי של Python לסוג Swift סטטי בודד בשם PythonObject , ומאפשרים PythonObject לקבל כל ערך Python דינמי בזמן ריצה (בדומה לגישה של Abadi וחב' ). 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 , ומאפשר לסוויפט לקחת על עצמו את האחריות לספירת הפניות לפיתון:

/// 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 לפייתון, אתה יכול להשתמש ב:

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

זה מתאים בדיוק למודל שמתכנת סוויפט היה מצפה לו: המרות שניתנות לכשל מוקרנות לתוצאות אופציונליות (בדיוק כמו שהמרות "מחרוזת ל-int") מספקות את הבטיחות והחיזוי להן מצפים מתכנתי סוויפט.

לבסוף, מכיוון שיש לך גישה למלוא העוצמה של Python, כל היכולות הרפלקטיביות הרגילות של Python זמינות גם כן ישירות, כולל Python.type , Python.id , Python.dir ומודול inspect של Python.

אתגרי יכולת פעולה הדדית

התמיכה לעיל אפשרית מכיוון שהעיצוב של Swift מכוון ומעריך את המטרה של הרחבה תחבירית של סוגים ברמת הספרייה. אנו גם ברי מזל ש-Python ו-Swift חולקים תחביר דומה מאוד ברמת פני השטח לביטויים (אופרטורים וקריאות לפונקציות/שיטה). עם זאת, ישנם כמה אתגרים שנתקלנו בהם עקב מגבלות הרחבה של התחביר של Swift 4.0 והבדלי עיצוב מכוונים שעלינו להתגבר עליהם.

חיפוש חברים דינמי

למרות שסוויפט היא שפה הניתנת להרחבה בדרך כלל, חיפוש חברים פרימיטיבי לא היה תכונה הניתנת להרחבה בספרייה. באופן ספציפי, בהינתן ביטוי של צורה xy , סוג x לא היה מסוגל לשלוט במה שקרה כאשר ניגשו לאיבר y בו. אם הסוג של x היה מצהיר באופן סטטי על איבר בשם y אז הביטוי הזה היה נפתר, אחרת הוא יידחה על ידי המהדר.

במסגרת האילוצים של סוויפט, בנינו כריכה שעבדה סביב זה. לדוגמה, זה היה פשוט ליישם גישה לחברים במונחים של PyObject_GetAttrString ו- PyObject_SetAttrString של Python. זה אפשר קוד כמו:

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

עם זאת, כנראה שכולנו יכולים להסכים שזה לא משיג את המטרה שלנו לספק ממשק טבעי וארגונומי לעבודה עם ערכי Python! מעבר לכך, זה לא מספק שום תקציב לעבודה עם Swift L-Values: אין דרך לאיית את המקבילה של ax += 1 . יחד שתי הבעיות הללו היו פער משמעותי בביטוי.

לאחר דיון עם קהילת Swift , הפתרון לבעיה זו הוא לאפשר לקוד הספרייה ליישם הוק fallback לטיפול בחיפושי חברים כושלים. תכונה זו קיימת בשפות דינמיות רבות, כולל Objective-C , וככזו, הצענו ויישמנו את SE-0195: הצג את סוגי "בדיקת החברים הדינמיים" המוגדרים על ידי המשתמש המאפשרים לסוג סטטי לספק מטפל חוזר לחיפושים לא פתורים. הצעה זו נדונה בהרחבה על ידי קהילת סוויפט במהלך תהליך האבולוציה של סוויפט, ובסופו של דבר התקבלה. הוא נשלח מאז Swift 4.1.

כתוצאה מכך, ספריית יכולת הפעולה ההדדית שלנו מסוגלת ליישם את ה-hook הבא:

@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 עובד בדיוק כמו שאנחנו מצפים. זה מראה את היתרון העצום של היכולת לפתח את כל הערימה של שפה, ספריות ויישומים שלה יחד כדי להשיג מטרה.

סוגים הניתנים להתקשרות דינמית

בנוסף לחיפוש חברים, יש לנו אתגר דומה בכל הנוגע לקריאת ערכים. לשפות דינמיות יש לעתים קרובות את הרעיון של ערכים "ניתנים להתקשרות" , שיכולים לקבל חתימה שרירותית, אבל לסוויפט 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 של שיטה ומאלצת את המתקשרים לטפל (או לפחות להכיר) בכך שניתן לזרוק שגיאה.

זהו פער מובנה בין שתי השפות, ואיננו רוצים להתייחס להבדל זה עם הרחבת שפה. הפתרון הנוכחי שלנו לזה מתבסס על התצפית שלמרות שכל קריאת פונקציה יכולה להוביל, רוב השיחות לא. יתרה מזאת, בהתחשב בכך שסוויפט הופך את הטיפול בשגיאות למפורש בשפה, סביר שמתכנת 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.slice(a, b, c) . עם זאת, עלינו לחבר את התחביר הרגיל של טווח a...b מ-Swift, ואולי יהיה מעניין לשקול יישום אופרטורי צעדים כהרחבה לתחביר הטווח הבסיסי הזה. עלינו לחקור ולהסתפק במודל הנכון לשימוש עבור תת-סיווג של מחלקות Python. כרגע אין דרך לגרום למבנה כמו PythonObject לעבוד עם התאמת דפוסי tuple, אז אנו משתמשים במאפייני הקרנה כמו .tuple2 . אם זה יהפוך לבעיה בפועל, נוכל לחקור את הוספה של זה לסוויפט, אבל כרגע אנחנו לא חושבים שזו תהיה בעיה מספיק כדי שיהיה שווה לפתור בטווח הקרוב.

סיכום ומסקנה

אנחנו מרגישים טוב עם הכיוון הזה וחושבים שיש כמה היבטים מעניינים בעבודה הזו: זה נהדר שאין שינויים ספציפיים לפייתון במהדר או בשפה של Swift. אנו מסוגלים להשיג יכולת פעולה הדדית טובה של Python באמצעות ספרייה שנכתבה בסוויפט על ידי חיבור תכונות שפה עצמאיות של Python. אנו מאמינים שקהילות אחרות יוכלו להרכיב את אותה ערכת תכונות כדי להשתלב ישירות עם השפות הדינמיות (וזמני הריצה שלהן) שחשובות לקהילות אחרות (למשל JavaScript, Ruby וכו').

היבט מעניין נוסף של עבודה זו הוא שתמיכת Python אינה תלויה לחלוטין ב-TensorFlow האחר ובלוגיקת הבידול האוטומטית שאנו בונים כחלק מ-Swift for TensorFlow. זוהי הרחבה שימושית בדרך כלל למערכת האקולוגית של Swift שיכולה לעמוד בפני עצמה, שימושית לפיתוח צד שרת או כל דבר אחר שרוצה לפעול באופן הדדי עם ממשקי API קיימים של Python.