原文:Python互操作性

    如设计概述文档所述,Python API互操作性是本项目的一个重要需求。虽然Swift被设计为与其他编程语言(及其运行时)集成,但动态语言的本质并不需要支持静态语言所需的深度集成。特别是Python被设计为嵌入到其他应用程序中,并且有一个简单的C接口API。在我们的工作中,我们可以提供一个元嵌入,它允许Swift程序使用pythonapi,就像它们直接嵌入Python本身一样。
    为了实现这一点,Swift脚本/程序只需将Python解释器链接到其代码中。我们的目标从“我们如何使用pythonapi”变为“我们如何让pythonapi感觉自然、可访问,以及如何从Swift代码中容易获得?”这不是一个小问题——Swift和Python在设计上有很大的不同,包括它们处理错误的方法,Python的超级动态特性,两种语言在表层语法上的差异,以及不想“妥协”Swift程序员所期望的东西。我们还关心方便性和工效学,并认为这是不可接受的需要包装生成器,如SWIG。
    本文中的TL;DR是我们对这个方向感觉良好,并且认为这项工作有一些有趣的方面:通过编写与Python无关的语言特性,我们能够与Swift编写的库实现良好的Python互操作性,这是非常好的。这允许其他社区组成相同的功能集,以便直接与对其他社区(如Javascript、Ruby等)很重要的其他动态语言集成。这项工作独立于Swift对TensorFlow的自动求导和图形程序提取特性,也很有意义。

    整体方案

    我们的总体方法基于这样的观察:Python是强类型的,但与大多数动态类型语言一样,它的类型系统是在运行时强制执行的。虽然有很多人试图在其上改造静态类型系统(例如mypy、pytype和其他类型),但它们依赖于不健全的类型系统,因此它们不是我们可以依赖的完整解决方案,而且它们违背了许多使Python及其库真正伟大的设计前提。
    许多人认为Swift是一种静态类型的语言,因此得出结论:正确的解决方案是将Python的流体形式塞进一个静态定义的洞中。然而,其他人意识到Swift结合了强大的静态类型系统的优点和一个(经常被低估的!)动态类型系统。我们没有试图强迫Python的动态类型系统成为它不存在的东西,而是选择在Python存在的地方遇到它,并完全接受它的动态类型方法。
    这样做的最终结果是,我们可以获得非常自然的Python体验—直接使用Swift代码。下面是一个这样的示例;注释掉的代码显示了纯Python语法以进行比较:
    屏幕快照 2020-04-14 上午10.37.41.png

    如您所见,这里的语法对于Python程序员来说是可以立即理解的:主要区别在于Swift要求在使用之前声明值(使用let或var),并且我们选择将Python内置函数(如import、type、slice等)放在Python下。命名空间(只是为了避免混淆全局范围)。这是在努力让Python感觉自然和熟悉,同时又不损害Swift语言的全局设计之间有意识地平衡的结果。
    这一行是通过一个简单的需求建立起来的:我们不应该依赖任何特定于Python的编译器或语言特性来实现Python互操作——它应该完全实现为一个Swift库。毕竟,虽然Python对机器学习社区极其重要,但也有其他动态语言(Javascript、Ruby等)在其他领域有很强的立足点,我们不希望这些领域中的每一个都给Swift语言带来无尽的复杂性。
    您可以在Python.swift中看到我们桥接层的当前实现。这是纯Swift代码,与未修改的Swift 4.1一起工作。

    这种方法的局限性
    因为我们选择在Swift中接受Python的动态特性,所以我们得到了动态语言带来的优点和缺点。特别是,许多Swift程序员已经开始期待并依赖于惊人的代码完成,并欣赏在编译时让编译器捕捉错误和其他小错误的舒适性。相反,Python程序员没有这些功能(相反,bug通常在运行时被捕获),而且由于我们接受Python的动态特性,因此在Swift中pythonapi的工作方式是相同的。

    经过与Swift社区的仔细考虑,很明显这是一种平衡:Swift的哲学和价值体系有多少可以投射到Python库生态系统上。。。不破坏那些关于Python及其库的真实和美好的东西?最后,我们得出结论,以Python为中心的模型是最好的折衷方案:我们应该接受这样一个事实:Python是一种动态语言,它永远不会也永远不会在静态编译时有完美的代码完成和错误检测。
    必须注意的是,Python确实拥有现有的生产力工具,这些工具可以发现一些bug并提供良好的工具特性,如代码完成。这些工具通常基于不合理的试探法,但仍然非常有用。我们希望这些工具所使用的启发式方法能够集成到Swift源代码工具和IDE生态系统中,但是我们需要有人来帮助构建这个系统。如果您有兴趣,请联系我们

    工作原理
    我们将Python的动态类型系统映射到一个名为PythonObject的静态Swift类型中,并允许PythonObject在运行时接受任何动态Python值(类似于Abadi等人的方法)。PythonObject与Python C绑定中使用的PyObject*直接对应,并且可以执行Python值在Python中执行的任何操作。例如,这就像您在Python中所期望的那样:

    屏幕快照 2020-04-14 上午10.42.04.png
    因为我们不想破坏Swift的全局设计,所以我们将所有Python行为都限制在涉及这个PythonObject类型的表达式中。这确保了普通Swift代码的语义保持不变,即使它与Python值混合、匹配、接口和混合。

    基本互操作性
    从Swift 4.0开始,通过现有的语言特性已经可以直接实现合理的基本互操作性:我们简单地将PythonObject定义为一个Swift结构,它包装了一个私有的Swift PyReference类,允许Swift接管Python引用计数的责任:
    屏幕快照 2020-04-14 上午10.42.56.png

    类似地,我们可以根据现有的Python运行时接口在PythonObject上实现func+(以及其他受支持的Python操作符)。我们的实现如下:
    屏幕快照 2020-04-14 上午10.43.41.png
    我们还使PythonObject符合序列和其他协议,允许这样的代码工作:
    屏幕快照 2020-04-14 上午10.44.25.png
    此外,由于PythonObject符合MutableCollection,因此您可以完全访问集合的Swift api,包括map、filter、sort等函数。

    Swift值之间的转换
    既然Swift可以表示和操作Python值,那么在Int和Array等Swift原生类型和Python等价类型之间进行转换就变得非常重要。这是由PythonConvertible协议处理的,基本的Swift类型(如Int)符合该协议,而Swift集合类型(如Array和Dictionary)符合条件(当它们的元素符合时)。这使得转换自然地适合Swift模型。
    例如,如果您知道需要Swift整数,或者希望将Swift整数转换为Python,则可以使用:
    屏幕快照 2020-04-14 上午10.45.42.png
    类似地,像数组这样的聚合类型的工作方式完全相同:
    屏幕快照 2020-04-14 上午10.46.34.png
    这完全符合Swift程序员所期望的模型:可失败的转换被投射到可选结果中(就像“string to int”转换一样),提供Swift程序员所期望的安全性和可预测性。

    最后,因为您可以访问Python的全部功能,所以Python的所有常规反射功能也都可以直接使用,包括Python.type、Python.id、Python.dir和Python inspect模块。

    互操作性挑战
    上面的支持是可能的,因为Swift的设计旨在并理解类型的库级语法扩展性的目标。我们也很幸运,Python和Swift对于表达式(操作符和函数/方法调用)共享非常相似的表层语法。也就是说,由于Swift 4.0语法扩展性的限制和有意设计的差异,我们需要克服一些挑战。

    动态成员查找
    尽管Swift 4.0是一种通用的可扩展语言,但原始成员查找并不是库的可扩展特性。具体地说,给定一个x.y形式的表达式,x类型无法控制在访问成员y时发生的事情。如果x的类型静态地声明了一个名为y的成员,那么这个表达式将被解析,否则编译器将拒绝它。
    在Swift 4.0的约束下,我们构建了一个绑定来解决这个问题。例如,根据Python的PyObject_GetAttrString和PyObject_SetAttrString实现成员访问非常简单。允许的代码如下:
    屏幕快照 2020-04-14 上午10.47.58.png

    然而,我们可能都同意,这并不能实现我们的目标,即为使用Python值提供一个自然且符合人体工程学的界面!除此之外,它不提供使用Swift L值的任何功能:无法拼写a.x+=1的等价值。这两个问题在表现力上有很大的差距。
    在与Swift社区讨论之后,这个问题的解决方案是允许库代码实现回退挂钩来处理失败的成员查找。此特性存在于许多动态语言中,包括Objective-C,因此,我们提出并实现了SE-0195:引入用户定义的“动态成员查找”类型,该类型允许静态类型为未解析的查找提供回退处理程序。斯威夫特社区通过斯威夫特进化过程对这一建议进行了详细讨论,并最终被接受。从Swift 4.1开始它就一直在运输。
    因此,我们的互操作性库能够实现以下挂钩:
    屏幕快照 2020-04-14 上午10.49.08.png

    这使得上述代码可以简单地表示为:
    屏幕快照 2020-04-14 上午10.49.46.png

    … 自然的a.x+=1语法的工作原理和我们期望的一样。这显示了一个巨大的好处,即能够将语言、其库和应用程序的整个堆栈一起进化,以实现一个目标。

    动态可调用类型
    除了成员查找之外,在调用值时,我们还有一个类似的挑战。动态语言通常有“可调用”值的概念,它可以接受任意签名,但是Swift 4.1不支持这样的东西。例如,从Swift 4.1开始,我们的互操作性库能够通过这样的接口使用pythonapi:
    //Python:a=np.arange(15).reforme(3,5)
    设a=np.arange.call(with:15).resforme.call(with:3,5)
    //Python:d=np.array([1,2,3],dtype=“i2”)
    设d=np.array.call(使用:[6,7,8],kwargs:[(“dtype”,“i2”)])
    虽然可以做到这一点,但显然并没有实现我们的方便和符合人体工程学的目标。
    使用Swift社区#2评估这个问题,我们发现Python和Swift同时支持命名参数和未命名参数:命名参数作为字典传入。同时,Smalltalk派生语言增加了一个额外的问题:方法引用是原子单元,它包括方法的基名称和任何关键字参数。虽然与这种风格的语言的互操作性对Python并不重要,但我们希望确保Swift不会被描绘成妨碍与Ruby、Squeak和其他SmallTalk派生语言进行良好互操作的角落。
    我们目前的提议已经讨论过,但是还没有实现(需要Swift社区的最终批准),就是引入一个新的@dynamicCallable属性来表示一个类型(比如PythonObject)可以处理动态调用解析。@dynamicCallable特性已经实现并在Python互操作模块中可用。
    屏幕快照 2020-04-14 上午10.51.38.png

    我们认为这是非常有说服力的,并且确实缩小了在这些情况下存在的剩余表现力和人体工程学差距。我们相信这个特性对于Ruby、Squeak和其他动态语言来说是一个很好的解决方案,同时也是一个普遍有用的Swift语言特性,可以应用于其他Swift库。

    异常处理与错误处理
    Python的异常处理方法类似于C++和许多其他语言,其中任何表达式都可以在任何时候抛出异常,而调用方可以选择独立地处理它们(或不)。相反,Swift的错误处理方法使“可抛出性”成为方法API契约的一个显式部分,并强制调用方处理(或至少承认)可以抛出错误。
    这是两种语言之间固有的差异,我们不想用语言扩展来掩盖这种差异。我们目前对此的解决方案基于这样的观察:即使任何函数调用都可能抛出,但大多数调用都不会。此外,鉴于Swift在语言中明确了错误处理,因此Swift程序员中的Python也应该考虑错误在哪里可以抛出和捕获。我们用一个明确的.throwing来做PythonObject。下面是一个例子:

    屏幕快照 2020-04-14 上午10.53.14.png
    当然,这与快速错误处理提供的所有正常机制集成在一起,包括使用try的能力?如果您想处理错误,但不关心异常中包含的详细信息。

    目前的执行情况和现状
    如上所述,Python互操作性库的当前实现可在Python.swift文件的GitHub上获得。在实践中,我们发现它在许多用例中都能很好地工作。然而,我们需要继续开发和解决的一些问题是:
    Python切片比Swift的切片语法更通用。现在您可以通过Python.slice(a,b,c)函数完全访问它。然而,我们应该从Swift中连接到标准的a…b范围语法中,考虑实现striding操作符作为对基本范围语法的扩展可能会很有趣。我们需要研究并确定用于Python类的子类化的正确模型。目前没有办法使PythonObject这样的结构与元组模式匹配一起工作,因此我们使用.tuple2这样的投影属性。如果这在实践中成为一个问题,我们可以调查在Swift中添加这个,但是我们目前认为这个问题不足以在短期内得到解决。

    总结和结论
    我们对这个方向感到满意,并认为这项工作有几个有趣的方面:Swift编译器或语言中没有Python特定的更改,这很好。通过编写与Python无关的语言特性,我们能够通过用Swift编写的库实现良好的Python互操作性。我们相信其他社区将能够组成相同的功能集,直接与对其他社区(如JavaScript、Ruby等)重要的动态语言(及其运行时)集成。
    这项工作的另一个有趣的方面是,Python支持完全独立于我们作为Swift for TensorFlow的一部分构建的其他TensorFlow和自动微分逻辑。这是对Swift生态系统的一个通常有用的扩展,它可以独立运行,对于服务器端开发或任何其他希望与现有pythonapi交互的东西都很有用。

    最后,在Swift for TensorFlow的上下文中指出一个重要的警告是很重要的:虽然可以直接调用任意Python API,但是为您自动构建TensorFlow图的代码分区分析无法理解动态Python API调用。虽然通过Python互操作层直接为TensorFlow(sessions、Keras等)使用api在技术上是可行的,但我们在Swift中为TensorFlow构建的编译器分析和转换并不会给它带来好处。相反,我们需要发明自己的高级api,并从Keras和其他现有api中汲取灵感。有关详细信息,请参阅图程序抽象文档