文章 cheng yu · 1 小时 前 19m read

反馈:每天使用嵌入式 Python 已超过 2 年

我每天都在使用嵌入式 Python,至今已有两年多了。 也许现在是时候分享一下这段经历的感想了。

为什么要写这篇心得?因为,我想,我和这里的大多数人一样,都是一名ObjectScript开发者,而且我认为这些心得能给社区带来帮助,让大家更好地了解在IRIS中选择嵌入式Python进行开发所涉及的利弊,同时也避免一些陷阱。

image

引言

我自2010年起从事开发工作,自2013年起开始使用ObjectScript。

因此,我拥有大约10年的ObjectScript开发经验。

自2021年IRIS发布嵌入式Python以来,我给自己设定了一个挑战:

  • 学习Python
  • 尽可能用Python完成所有工作

当我踏上这段旅程时,我对Python一无所知。因此,我从基础开始学起,至今仍在每天学习。

从 Python 开始

Python 的优点在于它很容易上手。如果你已经掌握了 ObjectScript,学习起来会更加轻松。

**为什么呢?**因为它们有很多共同之处。

ObjectScript Python
无类型 无类型
脚本语言 脚本语言
面向对象 面向对象
解释型 解释型
轻松集成 C 语言 易于与C语言集成

因此,如果你懂 ObjectScript,那么你对 Python 已经有了相当多的了解。

不过,两者之间还是存在一些差异,其中有些差异并不容易理解。

Python 并非 ObjectScript

为了简明起见,我将重点介绍 ObjectScript 与 Python 之间的主要区别。

对我来说,主要有以下 3 个区别:

  • Pep8
  • 模块
  • Dunders

Pep8

Pep8到底是什么鬼?

这是一套用于编写 Python 代码的规则。

pep8.org

其中部分规则如下:

  • 命名约定
  • 变量名
    • 蛇形命名法
  • 类名
    • 驼峰式
  • 缩进
  • 行长
  • 等

这为什么很重要?

因为这就是编写 Python 代码的方式。如果你不遵守这些规则,你将很难阅读别人的代码,而别人也会很难阅读你的代码。

作为 ObjectScript 开发者,我们也有一些规则需要遵守,但这些规则不像 Pep8 那样严格。

我是通过艰辛的经历才学会 Pep8 的。

说来话长,我是 InterSystems 的一名销售工程师,经常要做演示。有一天,我正在向一位客户演示嵌入式 Python,这位客户本身就是一名 Python 开发者,结果他一看到我的代码,对话就戛然而止。 他告诉我,我的代码完全不符合 Python 风格(他说的没错),我写 Python 代码的方式就像在写 ObjectScript 一样。正因如此,他告诉我,他不再对嵌入式 Python 感兴趣了。我大吃一惊,于是决定以正确的方式学习 Python。

所以,如果你想学习 Python,请先学习 PEP 8。

模块

模块是ObjectScript中没有的概念。

通常,在面向对象的语言中,会有类和包。在 Python 中,则有类、包和模块。

什么是模块?

它是一个扩展名为 .py 的文件,也是组织代码的一种方式。

没明白吗?我刚开始也不明白。所以,我们来举个例子吧。

通常,当你在 ObjectScript 中想要创建一个类时,会创建一个 .cls 文件,并将类定义放入其中。如果你想创建另一个类,就再创建一个 .cls 文件。如果你想创建一个包,就创建一个文件夹,并将 .cls 文件放入其中。

在 Python 中也是如此,但 Python 支持在一个文件中包含多个类。这个文件被称为模块。 顺便提一下,在一个文件中包含多个类是 Pythonic 的做法。

因此,请提前规划好如何组织代码,以及如何命名模块,以免像我一样,最终出现大量模块名称与类名称相同的状况。

一个反面例子:

MyClass.py

class MyClass:
    def __init__(self):
        pass

    def my_method(self):
        pass

要实例化这个类,你需要这样做:

import MyClass.MyClass # weird right ?

my_class = MyClass()

很奇怪吧?

Dunders

**Dunders 是 Python 中的特殊方法。**它们之所以被称为 dunder,是因为它们以两个下划线开头和结尾。

它们类似于 ObjectScript 中的 % 方法。

它们用于:

  • 构造函数
  • 运算符重载
  • 对象表示
  • 等。

示例:

class MyClass:
    def __init__(self):
        pass

    def __repr__(self):
        return "MyClass"

    def __add__(self, other):
        return self + other

这里有 3 个双下划线方法:

  • __init__:构造函数
  • __repr__:对象表示
  • __add__:运算符重载

双下划线方法在 Python 中随处可见。这是该语言的重要组成部分,但别担心,你很快就能掌握它们。

结论

Python 不是 ObjectScript,你必须学习它。但这并不难,你会很快掌握的。 只需记住,你需要学习 Pep8,以及如何利用模块和双下划线方法来组织代码。

学习 Python 的优质网站:


嵌入式 Python

既然你已经对 Python 有了一定的了解,接下来我们就来谈谈嵌入式 Python。

什么是嵌入式 Python?

嵌入式 Python是一种在 IRIS 中执行 Python 代码的方式。这是IRIS 2021.2 及更高版本的一项新功能**。**这意味着你的 Python 代码将与 IRIS 在同一个进程中执行。 此外,每个 ObjectScript 类都是一个 Python 类,方法和属性也是如此,反之亦然。🥳 这太棒了!

如何使用嵌入式 Python?

使用嵌入式 Python 主要有 3 种方式:

  • 在 ObjectScript 中使用语言标签
    • Method Foo() As %String [ Language = python ]
  • 使用 ##class(%SYS.Python).Import() 函数
  • 使用 Python 解释器
    • python3 -c "import iris; print(iris.system.Version.GetVersion())"

但如果你想认真研究嵌入式 Python,就必须避免使用语言标签。

image

为什么?

  • 因为这不符合 Python 风格
  • 因为这也不符合ObjectScript的风格
  • 因为你没有调试器
  • 因为你没有代码检查工具
  • 因为你没有代码格式化工具
  • 因为你没有测试框架
  • 因为你没有包管理器
  • 因为你在同一个文件中混用了两种语言
  • 因为在处理崩溃时,你没有堆栈跟踪
  • 因为你无法使用虚拟环境或 conda 环境
  • ……

别误会,这种做法确实可行,如果想快速测试某些内容,它可能很有用,但依我之见,这并不是一种好的编程习惯。

那么,在这两年嵌入式 Python 的实践中,我学到了什么?又该如何正确使用它呢?

我是如何使用嵌入式 Python 的

对我来说,有两种选择:

  • 将 Python 库作为 ObjectScript 类直接使用
    • 并使用 ##class(%SYS.Python).Import() 函数
  • 采用“Python优先”的方法

将 Python 库和代码视为 ObjectScript 类来使用

您仍然希望在 ObjectScript 代码中使用 Python,但又不希望使用语言标签。那么该怎么办呢?

“只需”将 Python 库和代码当作 ObjectScript 类来使用即可。

让我们来看一个例子:

你想在 ObjectScript 代码中使用 requests 库(这是一个用于发送 HTTP 请求的库)。

使用语言标签

ClassMethod Get() As %Status [ Language = python ]
{
	import requests

	url = "https://httpbin.org/get"
	# make a get request
	response = requests.get(url)
	# get the json data from the response
	data = response.json()
	# iterate over the data and print key-value pairs
	for key, value in data.items():
		print(key, ":", value)
}

为什么我认为这不是个好主意?

因为你在同一个文件中混用了两种语言,而且没有调试器、代码检查工具、代码格式化工具等。 如果这段代码崩溃了,你将很难进行调试。 你无法获得堆栈跟踪信息,也无法确定错误的来源。 而且你无法使用自动补全功能。

没有语言标签

ClassMethod Get() As %Status
{
	set status = $$$OK
    set url = "https://httpbin.org/get"
    // Import Python module "requests" as an ObjectScript class
    set request = ##class(%SYS.Python).Import("requests")
    // Call the get method of the request class
    set response = request.get(url)
    // Call the json method of the response class
	set data = response.json()
    // Here data is a Python dictionary
    // To iterate over a Python dictionary, you have to use the dunder method and items()
	// Import built-in Python module
	set builtins = ##class(%SYS.Python).Import("builtins")
    // Here we are using len from the builtins module to get the length of the dictionary
    For i = 0:1:builtins.len(data)-1 {
        // Now we convert the items of the dictionary to a list, and we get the key and the value using the dunder method __getitem__
		Write builtins.list(data.items())."__getitem__"(i)."__getitem__"(0),": ",builtins.list(data.items())."__getitem__"(i)."__getitem__"(1),!
	}
	quit status
}

为什么我认为这是个好主意?

因为你正在像使用 ObjectScript 一样使用 Python。你将 requests 库作为 ObjectScript 类导入,并将其作为 ObjectScript 类使用。 所有逻辑都在 ObjectScript 中,而你将 Python 作为库来使用。 即使在维护方面,代码也更易于阅读和理解,任何 ObjectScript 开发人员都能看懂这段代码。 缺点是,你必须了解如何使用 duners 的方法,以及如何将 Python 当作 ObjectScript 来使用。

结论

相信我,这样你最终会得到更健壮的代码,并且能够轻松地进行调试。 起初,这似乎很难,但你会发现,学习 Python 的好处比你想象的来得更快。

采用“先用 Python”的方法

这是我使用嵌入式 Python 时最喜欢的方式。

我已经使用这种方法开发了许多工具,对此非常满意。

以下是一些示例:

那么,什么是“Python优先”的方法呢?

只有一条规则:Python 代码必须放在 .py 文件中,ObjectScript 代码必须放在 .cls 文件中

如何实现这一点?

其核心思路是创建 ObjectScript 封装类来调用 Python 代码。


让我们以 iris-fhir-python-strategy 为例:

示例:iris-fhir-python-strategy

首先,我们需要了解 IRIS FHIR 服务器的工作原理。

每个 IRIS FHIR 服务器都实现了 Strategy。

一个 Strategy 由两个类组成:

超类 子类参数
HS.FHIRServer.API.InteractionsStrategy `StrategyKey` — 指定 InteractionsStrategy 的唯一标识符。 `InteractionsClass` — 指定您的 Interactions 子类的名称。
HS.FHIRServer.API.RepoManager `StrategyClass` — 指定您的 InteractionsStrategy 子类的名称。 `StrategyKey` — 指定 InteractionsStrategy 的唯一标识符。必须与 InteractionsStrategy 子类中的 StrategyKey 参数一致。

这两个类都是 Abstract 类。

  • HS.FHIRServer.API.InteractionsStrategy 是一个 Abstract 类,必须实现该类才能自定义 FHIR 服务器的行为。
  • HS.FHIRServer.API.RepoManager 是一个 Abstract 类,必须实现该类才能自定义 FHIR 服务器的存储方式。

备注

在本示例中,我们将仅关注 HS.FHIRServer.API.InteractionsStrategy 类,即使 HS.FHIRServer.API.RepoManager 类也被实现且是自定义 FHIR 服务器的必备条件。 HS.FHIRServer.API.RepoManager 类由 HS.FHIRServer.Storage.Json.RepoManager 类实现,后者是 FHIR 服务器的默认实现。

代码位置

所有源代码均可在此代码库中找到:iris-fhir-python-strategysrc 文件夹包含以下子文件夹:

  • python:包含 Python 代码
  • cls:包含用于调用 Python 代码的 ObjectScript 代码

如何实现策略

在此概念验证中,我们仅关注如何用 Python 实现 Strategy,而不涉及如何实现 RepoManager。

要实现 Strategy,你需要创建至少两个类:

  • 一个继承自 HS.FHIRServer.API.InteractionsStrategy 类的类
  • 一个从 HS.FHIRServer.API.Interactions 类继承而来的类

InteractionsStrategy 的实现

HS.FHIRServer.API.InteractionsStrategy 类旨在通过重写以下方法来定制 FHIR 服务器的行为:

  • GetMetadataResource:调用此方法可获取 FHIR 服务器的元数据
    • 这是我们在本次概念验证中将要重写的唯一方法

HS.FHIRServer.API.InteractionsStrategy 也有两个参数:

  • StrategyKey:InteractionsStrategy 的唯一标识符
  • InteractionsClass:您的 Interactions 子类的名称

Interactions 的实现

HS.FHIRServer.API.Interactions 该类旨在通过重写以下方法来定制 FHIR 服务器的行为:

  • OnBeforeRequest:在请求发送至服务器之前被调用
  • OnAfterRequest:在请求发送至服务器后调用
  • PostProcessRead:在读取操作完成后调用
  • PostProcessSearch:在搜索操作完成后调用
  • Read : 用于读取资源时调用
  • Add : 用于添加资源时调用
  • Update : 用于更新资源时调用
  • Delete : 用于删除资源
  • 等等……

我们在 src/cls/FHIR/Python/Interactions.cls 类中实现了 HS.FHIRServer.API.Interactions 类。

 

剧透

```cls Class FHIR.Python.Interactions Extends (HS.FHIRServer.Storage.Json.Interactions, FHIR.Python.Helper) {

Parameter OAuth2TokenHandlerClass As %String = "FHIR.Python.OAuth2Token";

Method %OnNew(pStrategy As HS.FHIRServer.Storage.Json.InteractionsStrategy) As %Status { // %OnNew is called when the object is created. // The pStrategy parameter is the strategy object that created this object. // The default implementation does nothing // Frist set the python path from an env var set ..PythonPath = $system.Util.GetEnviron("INTERACTION_PATH") // Then set the python class name from the env var set ..PythonClassname = $system.Util.GetEnviron("INTERACTION_CLASS") // Then set the python module name from the env var set ..PythonModule = $system.Util.GetEnviron("INTERACTION_MODULE")

if (..PythonPath = "") || (..PythonClassname = "") || (..PythonModule = "") { //quit ##super(pStrategy) set ..PythonPath = "/irisdev/app/src/python/" set ..PythonClassname = "CustomInteraction" set ..PythonModule = "custom" }

// Then set the python class do ..SetPythonPath(..PythonPath) set ..PythonClass = ##class(FHIR.Python.Interactions).GetPythonInstance(..PythonModule, ..PythonClassname)

quit ##super(pStrategy)

}

方法 OnBeforeRequest( pFHIRServiceAsHS.FHIRServer.API.Service, pFHIRRequestAsHS.FHIRServer.API.Data.Request, pTimeoutAs %Integer) { // OnBeforeRequest 在处理每个请求之前被调用。 if $ISOBJECT(..PythonClass) { setbody =##class(%SYS.Python).None() ifpFHIRRequest.Json '=""{ setjsonLib =##class(%SYS.Python).Import("json") setbody = jsonLib.loads(pFHIRRequest.Json.%ToJSON()) } do ..PythonClass."on_before_request"(pFHIRService, pFHIRRequest, body, pTimeout) } }

方法 OnAfterRequest( pFHIRServiceAsHS.FHIRServer.API.Service, pFHIRRequestAsHS.FHIRServer.API.Data.Request, pFHIRResponseAsHS.FHIRServer.API.Data.Response) { // 每次请求处理完成后都会调用 OnAfterRequest。 if $ISOBJECT(..PythonClass) { setbody =##class(%SYS.Python).None() ifpFHIRResponse.Json '=""{ setjsonLib =##class(%SYS.Python).Import("json") setbody = jsonLib.loads(pFHIRResponse.Json.%ToJSON()) } do ..PythonClass."on_after_request"(pFHIRService, pFHIRRequest, pFHIRResponse, body) } }

方法 PostProcessRead(pResourceObjectAs %DynamicObject)As %Boolean { // 从数据库读取资源后将调用 PostProcessRead。 // 返回 1 表示该资源应包含在响应中。 // 返回 0 表示该资源应从响应中排除。 if $ISOBJECT(..PythonClass) { ifpResourceObject '=""{ setjsonLib =##class(%SYS.Python).Import("json") setbody = jsonLib.loads(pResourceObject.%ToJSON()) } return ..PythonClass."post_process_read"(body) } quit 1 }

方法 PostProcessSearch( pRSAsHS.FHIRServer.Util.SearchResult, pResourceTypeAs %String)As %Status { // 搜索执行完成后将调用 PostProcessSearch。 // 返回 $$$OK 表示搜索成功。 // 返回错误代码表示搜索失败。 if $ISOBJECT(..PythonClass) { return ..PythonClass."post_process_search"(pRS, pResourceType) } quit $$$OK }

方法Read( pResourceTypeAs %String, pResourceIdAs %String, pVersionIdAs %String="")As %DynamicObject { return ##super(pResourceType, pResourceId, pVersionId) }

方法 Add( pResourceObjAs %DynamicObject, pResourceIdToAssignAs %String="", pHttpMethod ="POST")As %String { return ##super(pResourceObj, pResourceIdToAssign, pHttpMethod) }

/// 返回“已删除”版本的 VersionId 方法 Delete( pResourceTypeAs %String, pResourceIdAs %String)As %String { return ##super(pResourceType, pResourceId) }

方法 Update(pResourceObjAs %DynamicObject)As %String { return ##super(pResourceObj) }

}

```

`FHIR.Python.Interactions` 类继承自 `HS.FHIRServer.Storage.Json.Interactions` 类和 `FHIR.Python.Helper` 类。

HS.FHIRServer.Storage.Json.Interactions类是FHIR服务器的默认实现。

FHIR.Python.Helper类旨在帮助从ObjectScript调用Python代码。

FHIR.Python.Interactions 类重写了以下方法:

  • %OnNew:在对象创建时被调用

    • 我们使用此方法根据环境变量设置 Python 路径、Python 类名和 Python 模块名
    • 如果环境变量未设置,则使用默认值
    • 我们还会设置 Python 类
    • 我们调用父类的 %OnNew 方法
Method %OnNew(pStrategy As HS.FHIRServer.Storage.Json.InteractionsStrategy) As %Status
{
	// First set the python path from an env var
	set ..PythonPath = $system.Util.GetEnviron("INTERACTION_PATH")
	// Then set the python class name from the env var
	set ..PythonClassname = $system.Util.GetEnviron("INTERACTION_CLASS")
	// Then set the python module name from the env var
	set ..PythonModule = $system.Util.GetEnviron("INTERACTION_MODULE")

	if (..PythonPath = "") || (..PythonClassname = "") || (..PythonModule = "") {
		// use default values
		set ..PythonPath = "/irisdev/app/src/python/"
		set ..PythonClassname = "CustomInteraction"
		set ..PythonModule = "custom"
	}

	// Then set the python class
	do ..SetPythonPath(..PythonPath)
	set ..PythonClass = ..GetPythonInstance(..PythonModule, ..PythonClassname)

	quit ##super(pStrategy)
}
  • OnBeforeRequest:在请求发送至服务器之前被调用

    • 我们调用 Python 类的 on_before_request 方法
    • 我们传入 HS.FHIRServer.API.Service 对象、HS.FHIRServer.API.Data.Request 对象、请求正文以及超时时间
Method OnBeforeRequest(
	pFHIRService As HS.FHIRServer.API.Service,
	pFHIRRequest As HS.FHIRServer.API.Data.Request,
	pTimeout As %Integer)
{
	// OnBeforeRequest is called before each request is processed.
	if $ISOBJECT(..PythonClass) {
		set body = ##class(%SYS.Python).None()
		if pFHIRRequest.Json '= "" {
			set jsonLib = ##class(%SYS.Python).Import("json")
			set body = jsonLib.loads(pFHIRRequest.Json.%ToJSON())
		}
		do ..PythonClass."on_before_request"(pFHIRService, pFHIRRequest, body, pTimeout)
	}
}
  • OnAfterRequest:在请求发送至服务器后被调用

    • 我们调用 Python 类的 on_after_request 方法
    • 我们传递 HS.FHIRServer.API.Service 对象、HS.FHIRServer.API.Data.Request 对象、HS.FHIRServer.API.Data.Response 对象以及响应正文
Method OnAfterRequest(
	pFHIRService As HS.FHIRServer.API.Service,
	pFHIRRequest As HS.FHIRServer.API.Data.Request,
	pFHIRResponse As HS.FHIRServer.API.Data.Response)
{
	// OnAfterRequest is called after each request is processed.
	if $ISOBJECT(..PythonClass) {
		set body = ##class(%SYS.Python).None()
		if pFHIRResponse.Json '= "" {
			set jsonLib = ##class(%SYS.Python).Import("json")
			set body = jsonLib.loads(pFHIRResponse.Json.%ToJSON())
		}
		do ..PythonClass."on_after_request"(pFHIRService, pFHIRRequest, pFHIRResponse, body)
	}
}
  • 以此类推……

Python 中的交互

FHIR.Python.Interactions 类调用了该 Python 类的 on_before_request、on_after_request 等方法。

以下是一个抽象的 Python 类:

import abc
import iris

class Interaction(object):
    __metaclass__ = abc.ABCMeta

    @abc.abstractmethod
    def on_before_request(self, 
                          fhir_service:'iris.HS.FHIRServer.API.Service',
                          fhir_request:'iris.HS.FHIRServer.API.Data.Request',
                          body:dict,
                          timeout:int):
        """
        on_before_request is called before the request is sent to the server.
        param fhir_service: the fhir service object iris.HS.FHIRServer.API.Service
        param fhir_request: the fhir request object iris.FHIRServer.API.Data.Request
        param timeout: the timeout in seconds
        return: None
        """
        

    @abc.abstractmethod
    def on_after_request(self,
                         fhir_service:'iris.HS.FHIRServer.API.Service',
                         fhir_request:'iris.HS.FHIRServer.API.Data.Request',
                         fhir_response:'iris.HS.FHIRServer.API.Data.Response',
                         body:dict):
        """
        on_after_request is called after the request is sent to the server.
        param fhir_service: the fhir service object iris.HS.FHIRServer.API.Service
        param fhir_request: the fhir request object iris.FHIRServer.API.Data.Request
        param fhir_response: the fhir response object iris.FHIRServer.API.Data.Response
        return: None
        """
        

    @abc.abstractmethod
    def post_process_read(self,
                          fhir_object:dict) -> bool:
        """
        post_process_read is called after the read operation is done.
        param fhir_object: the fhir object
        return: True the resource should be returned to the client, False otherwise
        """
        

    @abc.abstractmethod
    def post_process_search(self,
                            rs:'iris.HS.FHIRServer.Util.SearchResult',
                            resource_type:str):
        """
        post_process_search is called after the search operation is done.
        param rs: the search result iris.HS.FHIRServer.Util.SearchResult
        param resource_type: the resource type
        return: None
        """

抽象 Python 类的实现

from FhirInteraction import Interaction

class CustomInteraction(Interaction):

    def on_before_request(self, fhir_service, fhir_request, body, timeout):
        #Extract the user and roles for this request
        #so consent can be evaluated.
        self.requesting_user = fhir_request.Username
        self.requesting_roles = fhir_request.Roles

    def on_after_request(self, fhir_service, fhir_request, fhir_response, body):
        #Clear the user and roles between requests.
        self.requesting_user = ""
        self.requesting_roles = ""

    def post_process_read(self, fhir_object):
        #Evaluate consent based on the resource and user/roles.
        #Returning 0 indicates this resource shouldn't be displayed - a 404 Not Found
        #will be returned to the user.
        return self.consent(fhir_object['resourceType'],
                        self.requesting_user,
                        self.requesting_roles)

    def post_process_search(self, rs, resource_type):
        #Iterate through each resource in the search set and evaluate
        #consent based on the resource and user/roles.
        #Each row marked as deleted and saved will be excluded from the Bundle.
        rs._SetIterator(0)
        while rs._Next():
            if not self.consent(rs.ResourceType,
                            self.requesting_user,
                            self.requesting_roles):
                #Mark the row as deleted and save it.
                rs.MarkAsDeleted()
                rs._SaveRow()

    def consent(self, resource_type, user, roles):
        #Example consent logic - only allow users with the role '%All' to see
        #Observation resources.
        if resource_type == 'Observation':
            if '%All' in roles:
                return True
            else:
                return False
        else:
            return True

内容过长,请总结

FHIR.Python.Interactions 类是一个用于调用 Python 类的封装类。

IRIS 抽象类是专门用于封装 Python 抽象类的 🥳。

这有助于我们将 Python 代码与 ObjectScript 代码分离,从而兼顾两者的优势。