قابلیت همکاری پایتون

قابلیت همکاری Python API یک نیاز مهم برای این پروژه است. در حالی که سوئیفت برای ادغام با سایر زبان های برنامه نویسی (و زمان اجرا آنها) طراحی شده است، ماهیت زبان های پویا نیازی به یکپارچگی عمیق مورد نیاز برای پشتیبانی از زبان های ثابت ندارد. پایتون به طور خاص برای تعبیه شدن در سایر برنامه ها طراحی شده است و دارای یک رابط کاربری ساده C است. برای اهداف کارمان، می‌توانیم یک متا embedding ارائه کنیم که به برنامه‌های سوئیفت اجازه می‌دهد از APIهای پایتون استفاده کنند، انگار که مستقیماً خود پایتون را جاسازی می‌کنند.

برای انجام این کار، اسکریپت/برنامه سوئیفت به سادگی مفسر پایتون را به کد خود پیوند می دهد. هدف ما از «چگونه با APIهای پایتون کار می‌کنیم» به سؤالی مبنی بر اینکه «چگونه می‌توانیم رابط‌های برنامه‌نویسی برنامه‌نویسی پایتون را طبیعی، در دسترس و دسترسی آسان از طریق کد سوئیفت ایجاد کنیم؟» تغییر می‌کند؟ این یک مشکل پیش پا افتاده نیست - تفاوت‌های طراحی قابل توجهی بین سوئیفت و پایتون وجود دارد، از جمله رویکردهای آن‌ها برای مدیریت خطا، ماهیت فوق‌دینامیک پایتون، تفاوت‌ها در نحو سطح سطحی بین دو زبان، و تمایل به عدم استفاده از آن. چیزهایی را که برنامه نویسان Swift انتظار داشتند، "مطالعه" کنید. ما همچنین به راحتی و ارگونومی اهمیت می دهیم و فکر می کنیم که نیاز به ژنراتور لفاف مانند SWIG غیرقابل قبول است.

رویکرد کلی

رویکرد کلی ما مبتنی بر مشاهده است که پایتون به شدت تایپ می‌شود اما - مانند اکثر زبان‌های تایپ شده پویا - سیستم نوع آن در زمان اجرا اعمال می‌شود. در حالی که تلاش‌های زیادی برای مقاوم‌سازی یک سیستم نوع ثابت در بالای آن صورت گرفته است (مثلاً mypy ، pytype و موارد دیگر )، آنها به سیستم‌های نوع ناسالم متکی هستند، بنابراین آنها راه‌حل کاملی نیستند که بتوانیم به آن تکیه کنیم، و علاوه بر این، آنها در برابر بسیاری از آنها غلبه می‌کنند. از فضاهای طراحی که پایتون و کتابخانه های آن را واقعا عالی می کند.

بسیاری از مردم 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. فضای نام (به سادگی برای جلوگیری از به هم ریختن دامنه جهانی). این نتیجه تعادل آگاهانه بین تلاش برای ایجاد احساس طبیعی و آشنای پایتون است، در حالی که طراحی جهانی زبان سوئیفت را به خطر نمی اندازد.

این خط از طریق یک نیاز ساده ایجاد می شود: ما نباید به هیچ کامپایلر یا ویژگی زبان اختصاصی پایتون برای دستیابی به تعامل پایتون وابسته باشیم - باید به طور کامل به عنوان یک کتابخانه سوئیفت پیاده سازی شود. به هر حال، در حالی که پایتون برای جامعه یادگیری ماشینی بسیار مهم است، زبان‌های پویا دیگری (جاوا اسکریپت، روبی و غیره) وجود دارند که جای پای محکمی در حوزه‌های دیگر دارند و ما نمی‌خواهیم هر یک از این دامنه‌ها پیچیدگی بی‌پایانی را تحمیل کنند. روی زبان سوئیفت

می توانید اجرای فعلی لایه پل زدن ما را در Python.swift ببینید. این کد Swift خالص است که با Swift اصلاح نشده کار می کند.

محدودیت های این رویکرد

از آنجایی که ما طبیعت پویای پایتون را در سوئیفت انتخاب می کنیم، هم مزایا و هم معایب زبان های پویا را دریافت می کنیم. به طور خاص، بسیاری از برنامه‌نویسان سوئیفت انتظار دارند و به تکمیل کد شگفت‌انگیز وابسته هستند و از راحتی اینکه کامپایلر اشتباهات تایپی و سایر باگ‌های بی‌اهمیت را برای آنها در زمان کامپایل تشخیص می‌دهد، قدردانی می‌کنند. در مقابل، برنامه نویسان پایتون این توانایی ها را ندارند (در عوض، باگ ها معمولا در زمان اجرا شناسایی می شوند)، و از آنجایی که ماهیت پویای پایتون را پذیرفته ایم، API های پایتون در سوئیفت به همین صورت عمل می کنند.

پس از بررسی دقیق با جامعه سوئیفت، مشخص شد که این یک تعادل است: چه مقدار از فلسفه و سیستم ارزشی سوئیفت را می توان در اکوسیستم کتابخانه پایتون پیش بینی کرد... بدون شکستن چیزهایی که در مورد پایتون درست و زیبا هستند. و کتابخانه هایش؟ در پایان، به این نتیجه رسیدیم که یک مدل پایتون محور بهترین سازش است: باید این واقعیت را بپذیریم که پایتون یک زبان پویا است، که هرگز و هرگز نمی تواند تکمیل کد و تشخیص خطا در زمان کامپایل ایستا داشته باشد.

چگونه کار می کند

ما سیستم نوع پویای پایتون را به یک نوع سوئیفت استاتیک به نام PythonObject نگاشت می‌کنیم و به PythonObject اجازه می‌دهیم تا هر مقدار پایتون پویا را در زمان اجرا دریافت کند (مشابه با رویکرد آبادی و همکاران ). 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".

از آنجایی که نمی‌خواهیم طراحی جهانی سوئیفت را به خطر بیندازیم، تمام رفتار پایتون را به عباراتی که شامل این نوع PythonObject هستند محدود می‌کنیم. این تضمین می‌کند که معنای کد معمولی سوئیفت بدون تغییر باقی می‌ماند، حتی اگر در حال ترکیب، تطبیق، واسط و آمیختگی با مقادیر پایتون باشد.

قابلیت همکاری اولیه

در Swift 4.0، سطح معقولی از قابلیت همکاری اولیه مستقیماً از طریق ویژگی‌های زبان موجود قابل دستیابی بود: ما به سادگی PythonObject به عنوان یک ساختار Swift تعریف می‌کنیم که یک کلاس PyReference Swift خصوصی را می‌پیچد و به Swift اجازه می‌دهد مسئولیت شمارش مراجع پایتون را بر عهده بگیرد:

/// 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 + (و بقیه عملگرهای پایتون پشتیبانی‌شده) را بر روی PythonObject از نظر رابط زمان اجرا پایتون موجود پیاده‌سازی کنیم. پیاده سازی ما به این صورت است:

// 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 و غیره دارید.

تبدیل به و از مقادیر سوئیفت

اکنون که سوئیفت می‌تواند مقادیر پایتون را نشان دهد و بر روی آنها کار کند، مهم است که بتوانیم بین انواع بومی سوئیفت مانند Int و Array<Float> و معادل‌های پایتون تبدیل کنیم. این توسط پروتکل PythonConvertible مدیریت می شود - که انواع Swift اصلی مانند Int با آن مطابقت دارند و انواع مجموعه سوئیفت مانند Array و Dictionary به طور مشروط با آن مطابقت دارند (زمانی که عناصر آنها مطابقت دارند). این باعث می شود که تبدیل ها به طور طبیعی در مدل سوئیفت قرار بگیرند.

به عنوان مثال، اگر می دانید که به یک عدد صحیح سوئیفت نیاز دارید یا می خواهید یک عدد صحیح سوئیفت را به پایتون تبدیل کنید، می توانید از موارد زیر استفاده کنید:

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") که ایمنی و قابلیت پیش‌بینی مورد انتظار برنامه‌نویسان Swift را فراهم می‌کند.

در نهایت، چون شما به تمام قدرت پایتون دسترسی دارید، تمام قابلیت‌های بازتابی عادی پایتون نیز مستقیماً در دسترس هستند، از جمله Python.type ، Python.id ، Python.dir و ماژول inspect Python.

چالش های قابلیت همکاری

پشتیبانی بالا امکان پذیر است زیرا طراحی سوئیفت هدف توسعه نحوی انواع در سطح کتابخانه را هدف قرار داده و از آن استقبال می کند. ما همچنین خوش شانس هستیم که پایتون و سوئیفت یک نحو بسیار مشابه سطح سطحی برای عبارات (عملگرها و فراخوانی تابع/روش) دارند. گفتنی است، به دلیل محدودیت‌های توسعه‌پذیری نحو Swift 4.0 و تفاوت‌های طراحی عمدی، با چند چالش مواجه شدیم که باید بر آن‌ها غلبه کنیم.

جستجوی اعضای پویا

اگرچه Swift یک زبان به طور کلی قابل توسعه است، جستجوی اعضای اولیه یک ویژگی قابل توسعه کتابخانه نیست. به طور خاص، با توجه به شکل xy ، نوع x قادر به کنترل آنچه در هنگام دسترسی به عضو y روی آن رخ می‌دهد، نبود. اگر نوع x به صورت ایستا عضوی به نام y را اعلام می کرد، این عبارت حل می شد، در غیر این صورت توسط کامپایلر رد می شد.

در چارچوب محدودیت های سوئیفت، ما یک اتصال ایجاد کردیم که حول این موضوع کار می کرد. برای مثال، پیاده‌سازی دسترسی‌های اعضا از نظر 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 وجود ندارد. این دو مشکل با هم یک شکاف بیانی قابل توجه بودند.

پس از بحث با انجمن سوئیفت ، راه‌حل این مشکل این است که به کد کتابخانه اجازه داده شود تا یک هوک بازگشتی را برای رسیدگی به جستجوهای ناموفق اعضا پیاده‌سازی کند . این ویژگی در بسیاری از زبان‌های پویا از جمله Objective-C وجود دارد، و به این ترتیب، ما SE-0195 را پیشنهاد و پیاده‌سازی کردیم: انواع «جستجوی اعضای پویا» تعریف‌شده توسط کاربر را معرفی کنید که به یک نوع ثابت اجازه می‌دهد تا یک کنترل‌کننده بازگشتی برای جستجوهای حل‌نشده ارائه کند. این پیشنهاد به طور طولانی توسط جامعه سوئیفت از طریق فرآیند تکامل سوئیفت مورد بحث قرار گرفت و در نهایت پذیرفته شد. از زمان سوئیفت 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 درست همانطور که انتظار داریم کار می کند. این نشان دهنده مزیت بزرگ توانایی تکامل پشته کامل یک زبان، کتابخانه ها و برنامه های کاربردی آن برای دستیابی به یک هدف است.

انواع قابل فراخوانی پویا

علاوه بر جستجوی اعضا، در مورد فراخوانی مقادیر نیز چالش مشابهی داریم. زبان‌های پویا اغلب دارای مفهوم مقادیر «قابل تماس» هستند که می‌توانند امضای دلخواه داشته باشند، اما سوئیفت 4.1 از چنین چیزی پشتیبانی نمی‌کند. برای مثال، از سوییفت 4.1، کتابخانه قابلیت همکاری ما می‌تواند با رابط‌های برنامه‌نویسی 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")])

در حالی که انجام کارها با این امکان پذیر است، اما به وضوح به هدف ما برای راحتی و ارگونومی دست نمی‌یابیم.

با ارزیابی این مشکل با جامعه سوئیفت و شماره 2 ، مشاهده می‌کنیم که پایتون و سوئیفت هم از آرگومان‌های نام‌دار و هم بی‌نام پشتیبانی می‌کنند: آرگومان‌های نام‌گذاری شده به‌عنوان یک فرهنگ لغت ارسال می‌شوند. در همان زمان، زبان های مشتق شده از Smalltalk یک چروک اضافی اضافه می کنند: مراجع روش واحد اتمی هستند که شامل نام پایه روش به همراه هر آرگومان کلمه کلیدی است. در حالی که قابلیت همکاری با این سبک از زبان برای پایتون مهم نیست، ما می‌خواهیم مطمئن شویم که سوئیفت در گوشه‌ای قرار نگیرد که مانع تعامل عالی با Ruby، Squeak و سایر زبان‌های مشتق شده از SmallTalk شود.

راه حل ما، که در سوئیفت 5 پیاده سازی شد ، معرفی یک ویژگی @dynamicCallable جدید است که نشان می دهد یک نوع (مانند PythonObject ) می تواند وضوح تماس پویا را مدیریت کند. ویژگی @dynamicCallable در ماژول interop 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++ و بسیاری از زبان‌های دیگر است، که در آن هر عبارتی می‌تواند در هر زمان استثنا ایجاد کند، و تماس‌گیرندگان می‌توانند آن‌ها را به طور مستقل مدیریت کنند (یا نه). در مقابل، رویکرد مدیریت خطای سوئیفت، «قابلیت پرتاب» را به بخشی صریح از قرارداد API یک روش تبدیل می‌کند و تماس‌گیرندگان را مجبور می‌کند تا خطا را بررسی کنند (یا حداقل تصدیق کنند) .

این یک شکاف ذاتی بین دو زبان است و ما نمی‌خواهیم این تفاوت را با یک پسوند زبانی بیان کنیم. راه‌حل فعلی ما برای این امر مبتنی بر این مشاهدات است که حتی اگر هر فراخوانی تابعی می‌تواند پرتاب کند، اکثر تماس‌ها این کار را نمی‌کنند. علاوه بر این، با توجه به اینکه سوئیفت مدیریت خطا را در زبان صریح می کند، برای یک برنامه نویس پایتون در سوئیفت منطقی است که به این فکر کند که آنها انتظار دارند خطاها در کجا قابل پرتاب و کشف باشند. ما این کار را با یک طرح .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)
  }

و البته، این با تمام مکانیک های معمولی ارائه شده توسط مدیریت خطای سوئیفت، از جمله توانایی استفاده از try? اگر می خواهید خطا را مدیریت کنید اما به جزئیات موجود در استثنا اهمیتی نمی دهید.

پیاده سازی و وضعیت فعلی

همانطور که در بالا ذکر شد، پیاده سازی فعلی ما از کتابخانه قابلیت همکاری پایتون در GitHub در فایل Python.swift موجود است. در عمل، ما متوجه شده ایم که برای بسیاری از موارد استفاده به خوبی کار می کند. با این حال، چند چیز وجود ندارد که ما باید به توسعه و کشف آنها ادامه دهیم:

برش پایتون کلی تر از نحو برش Swift است. در حال حاضر می توانید از طریق تابع Python.slice(a, b, c) به آن دسترسی کامل داشته باشید. با این حال، ما باید در نحو معمولی محدوده a...b از سویفت سیم‌کشی کنیم، و شاید جالب باشد که عملگرهای گام‌به‌گام را به‌عنوان پسوندی برای آن نحو محدوده پایه در نظر بگیریم. ما باید مدل مناسبی را که برای زیر کلاس‌بندی کلاس‌های پایتون استفاده می‌کنیم، بررسی کنیم. در حال حاضر هیچ راهی برای ساخت ساختاری مانند PythonObject با تطبیق الگوی تاپلی وجود ندارد، بنابراین ما از ویژگی های طرح ریزی مانند .tuple2 استفاده می کنیم. اگر این در عمل به یک مشکل تبدیل شود، می‌توانیم اضافه کردن آن به سوئیفت را بررسی کنیم، اما در حال حاضر فکر نمی‌کنیم مشکلی برای حل آن در کوتاه مدت کافی باشد.

خلاصه و نتیجه گیری

ما نسبت به این جهت احساس خوبی داریم و فکر می کنیم که چندین جنبه جالب این کار وجود دارد: خیلی خوب است که هیچ تغییر خاصی در پایتون در کامپایلر یا زبان سوئیفت وجود ندارد. ما می‌توانیم از طریق کتابخانه‌ای که در سوئیفت نوشته شده است، با ایجاد ویژگی‌های زبان مستقل از پایتون، به قابلیت همکاری پایتون خوبی برسیم. ما معتقدیم که سایر انجمن‌ها می‌توانند مجموعه ویژگی‌های مشابهی را برای ادغام مستقیم با زبان‌های پویا (و زمان‌های اجرا) که برای سایر جوامع مهم هستند (مانند جاوا اسکریپت، روبی و غیره) بسازند.

یکی دیگر از جنبه های جالب این کار این است که پشتیبانی پایتون کاملاً مستقل از دیگر منطق های TensorFlow و تمایز خودکار است که ما به عنوان بخشی از Swift برای TensorFlow می سازیم. این یک افزونه به طور کلی مفید برای اکوسیستم سوئیفت است که می تواند به تنهایی برای توسعه سمت سرور یا هر چیز دیگری که بخواهد با API های پایتون موجود همکاری کند مفید است.