Artigo
· Maio 20 21min de leitura

Feedback : Usando Python embutido diariamente por mais de dois anos

Há 2 anos eu venho utilizando Python embutido diariamente.
Talvez seja o momento de compartilhar um feedback sobre essa jornada.

Por que escrever esse feedback? Porque eu acredito que sou como a maioria das pessoas aqui, um desenvolvedor ObjectScript, e penso que a comunidade poderia ter algum benefício desse feedback e entender melhor os prós e contras de escolher Python embutido para desenvolver em IRIS. Além de evitar algumas armadilhas.

image

Introdução

Eu sou desenvolvedor desde 2010 e trabalho com ObjectScript desde 2013.

Então são aproximadamente 10 anos de experiência com ObjectScript.

Desde 2021 e o lançamento do Python Embutido no IRIS, eu me propus um desafio:

  • Aprender Python
  • Fazer tudo o máximo possível com Python

Quando eu comecei essa jornada, eu não tinha ideia do que era Python. Então, comecei com o básico e ainda estou aprendendo todo dia.

Começando com Python

O lado bom do Python é que é fácil de aprender. É ainda mais fácil se você já conhece ObjectScript.

Por quê? Elas têm muito em comum.

ObjectScript Python
Não tipada Não tipada
Scripting language Scripting language
Orientada a Objeto Orientada a Objeto
Interpretada Interpretada
Fácil integração com C Fácil integração com C

Então, se você conhece ObjectScript você já sabe muito sobre Python.

Porém, há algumas diferenças, dentre as quais algumas não são fáceis de entender.

Python não é ObjectScript

Para manter as coisas simples, vou focar nas principais diferenças entre ObjectScript e Python.

Para mim, há 3 principais diferenças:

  • Pep8
  • Módulos
  • Dunders

Pep8

O que é Pep8 ?

É um conjunto de regras para escrever códigos Python.

pep8.org

Algumas delas são:

  • Convenção de nomenclatura
    • nomes de variáveis
      • snake_case
    • nomes de classes
      • CamelCase
  • indentação
  • comprimento da linha
  • etc.

Por que isso é importante?

Porque é a maneira de escrever código Python. Se você não seguir essas regras, terá dificuldade em ler o código de outras pessoas e elas terão dificuldade de ler o seu.

Como desenvolvedores ObjectScript, também temos algumas regras para seguir, mas não são tão rígidas como a Pep8.

Eu aprendi Pep8 da maneira mais difícil.

Para contar a história, eu sou engenheiro de vendas na InterSystems e estou fazendo várias demos. Certo dia, eu estava fazendo uma demo de Python Embutido para um cliente que era desenvolvedor Python, e a conversa encurtou quando ele viu meu código. Ele me contou que meu código não era nada "Pythonico" (ele estava certo). Eu estava programando em Python como eu programava em ObjectScript. Por causa disso, ele disse que não estava interessado no Python Embutido mais. Eu fiquei chocado e decidi aprender Python do jeito certo.

Assim, se você quer aprender Python, aprenda Pep8 primeiro.

Módulos

Os módulos são uma funcionalidade que não temos em ObjectScript.

Geralmente, em linguagens orientadas a objetos, há classes e pacotes. No Python, temos classes, pacotes e módulos.

O que é um módulo?

É um arquivo com extensão .py. É a maneira de organizar seu código.

Você não entendeu? No começo eu também não. Portanto vamos usar um exemplo.

Geralmente, quando você quer criar uma classe em ObjectScript, você cria um arquivo .cls e coloca sua classe ali dentro. E, se quiser criar outra classe, usa outro arquivo .cls. Por fim, se quiser criar um pacote, você gera uma pasta e coloca os arquivos .cls dentro dela.

Em Python fazemos o mesmo, mas ele tem a habilidade de guardar várias classes num mesmo arquivo. Esse arquivo é chamado módulo.
É importante saber que é "Pythônico" ter muitas classes em um mesmo arquivo.

Portanto, planeje de antemão como pretende organizar seu código e como vai nomear seus módulos para não terminar como eu, com um monte de módulos com o mesmo nome das suas classes.

Um mau exemplo:

A bad example :

MinhaClasse.py

class MinhaClasse:
    def __init__(self):
        pass

    def meu_metodo(self):
        pass

Para instanciar essa classe, você fará:

import MinhaClasse.MinhaClasse # estranho, né?

minha_classe= MinhaClasse()

Estranho, né?

Dunders

Dunders são métodos especiais em Python. Eles são chamados de dunder porque começam e terminam com dois caracteres underscores (_).

Eles são similares aos métodos do ObjectScript com '%'.

Eles são usados para:

  • construtor
  • sobrecarga de operador
  • representação do objeto
  • etc.

Exemplo:

class MinhaClasse:
    def __init__(self):
        pass

    def __repr__(self):
        return "MyClass"

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

Aqui temos 3 métodos dunder:
* __init__ : construtor
* __repr__ : representação do objeto
* __add__ : sobrecarga de operador

Métodos Dunders estão por toda parte em Python. São uma grande parte da linguagem, mas não se preocupe; você vai aprendê-los rapidamente,

Conclusão

Python não é ObjectScript, e você terá que aprender. Mas não é tão difícil, então você vai aprender rápido.
Apenas mantenha em mente que você deverá aprender Pep8 e como organizar seu código com módulos e métodos dunder.

Bons sites para aprender Python


Python Embutido

Agora que você conhece um pouco mais sobre o Python, vamos falar de Python Embutido.

O que é o Python Embutido?

Python Embutido é uma maneira de executar código Python dentro do IRIS. É uma nova funcionalidade do IRIS 2021.2+.
Isso significa que seu código Python será executado no mesmo processo que o IRIS.

No mais, toda classe ObjectScript é uma classe Python, e o mesmo cale para métodos e atributos, e vice-versa. 🥳
Isso é bacana!

Como usar Python Embutido?

Há 3 maneiras diferentes de usar Python Embutido:

  • Usando a tag de linguagem do ObjectScript
    • Method Foo() As %String [ Language = python ]
  • Usando a função ##class(%SYS.Python).Import()
  • Usando o interpretador de Python
    • python3 -c "import iris; print(iris.system.Version.GetVersion())"

Mas se você quiser usar mesmo o Python Embutido, você terá que evitar usar a tag de linguagem.

image

Por quê?

  • Porque não é Pythônico
  • Porque também não é ObjectScript
  • Porque você não terá um debugador
  • Porque você não terá um linter
  • Porque você não terá um formatador
  • Porque você não terá um framework de testes
  • Porque você não terá um gerenciador de pacotes
  • Porque você está misturando duas linguagens no mesmo arquivo
  • Porque quando o seu processo quebra, você não tem um rastreio da pilha
  • Porque você não pode usar um ambiente virtual ou ambientes conda
  • ...

Não me entenda mal, isso funciona e pode ser útil se você quiser testar algo rapidamente, mas na minha opinião não é uma boa prática.

Então, o que eu aprendi nesses 2 anos de Python Embutido, e como utilizá-lo da maneira correta?

Como eu uso Python embutido

Para mim, existem duas opções:

  • Usar libraries do Python como se fossem classes do ObjectScript
    • with ##class(%SYS.Python).Import() function
  • Usar uma abordagem de Python em primeiro lugar.

Usar libraries do Python como se fossem classes do ObjectScript

Você ainda quer usar Python no seu código ObjectScript, mas não quer usar a tag de linguagem. O que você pode fazer, então?

"Simplesmente" usar códigos e libraries do Python como se fossem classes ObjectScript.

Vamos tomar como exemplo:

Você quer usar a library requests (é uma library para fazer requisições HTTP) no seu código ObjectScript

Com a tag de linguagem

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

    url = "https://httpbin.org/get"
    # faça uma requisição get
    response = requests.get(url)
    # pegue os dados do json de resposta
    data = response.json()
    # iterar pelos dados e printar os pares chave-valoe
    for key, value in data.items():
        print(key, ":", value)
}

Por que eu não acho uma boa ideia?

Porque você está misturando duas linguagens no mesmo arquivo, e você não tem um debugador, um linter, um formatador, etc.
Se esse código der algum problema, você terá dificuldade de debugar.
Se você não tem um rastreio da pilha, não saberá da onde veio o erro.
E também não tem a funcionalidade de completar automaticamente.

Sem a tag de linguagem

ClassMethod Get() As %Status
{
    set status = $$$OK
    set url = "https://httpbin.org/get"
    // Importar o módulo Python "requests" como uma classe ObjectScript 
    set request = ##class(%SYS.Python).Import("requests")
    // Chamar o método get da classe request
    set response = request.get(url)
    // Chamar o método json da classe response
    set data = response.json()
    // Aqui os dados são um dicionário Python
    // Para iterar por um dicionário Python, você deve usar o método dunder e items().
    // Importar um módulo pré-existente de Python.
    set builtins = ##class(%SYS.Python).Import("builtins")
    // Aqui estamos usando "len" do módulo de pré-existentes (built in) para saber o comprimento do dicionário.
    For i = 0:1:builtins.len(data)-1 {
        // Agora nós convertemos os itens do dicionário para uma lista, então pegamos a chave e o valor usando o método dunder __getitem__
        Write builtins.list(data.items())."__getitem__"(i)."__getitem__"(0),": ",builtins.list(data.items())."__getitem__"(i)."__getitem__"(1),!
    }
    quit status
}

Por que eu acho isso uma boa ideia?

Porque você está utilizando Python como se fosse ObejctScript. Você está importando a library requests como uma classe ObjectScript e usando como se fosse uma classe ObjectScript.
Toda a lógica está em ObjectScript, e você utiliza o Python como uma library.
Até para manutenção, é mais fácil de ler e entender; qualquer desenvolvedor ObjectScript consegue entender esse código.
A desvantagem é que você tem que saber como usar os métodos dunders, e como usar Python como se fosse ObjectScript.

Conclusão

Acredite em mim, dessa maneira você vai acabar com um código mais robusto e conseguirá debugar facilmente.
A princípio parece difícil, mas você vai entender os benefícios de aprender Python mais rápido do que você pensa.

Usar uma abordagem de Python em primeiro lugar

Essa é a maneira que eu prefiro usar Python Embutido

Eu construí várias ferramentas usando essa abordagem e estou muito feliz com isso.

Alguns exemplos:

Então, o que é uma abordagem de Python em primeiro lugar?

Existe apenas uma regra: o código Python deve estar em arquivos . py, o código ObjectScript deve estar em arquivos .cls

Como conseguir isso?

A ideia é criar classes de invólucro de ObjectScript para chamar o código Python.


Vamos tomar o exemplo de iris-fhir-python-strategy :

Exemplo : iris-fhir-python-strategy

Primeiramente, nós temos que entender como o servidor IRIS FHIR funciona.

Todo servidor IRIS FHIR implementa uma Strategy (estratégia).

Uma Strategy é um conjunto de duas classes:

Superclases Parâmetros de Subclasse
HS.FHIRServer.API.InteractionsStrategy StrategyKey — Especifica um identificador único para a InteractionsStrategy.
InteractionsClass — Especifica o nome da sua subclasse de Interactions (interações).
HS.FHIRServer.API.RepoManager StrategyClass —Especifica o nome da sua sublcasse InteractionsStrategy.
StrategyKey — Especifica um identificaod único para a InteractionsStrategy. Deve ter o mesmo valor do parâmetro StrategyKey na subclasse InteractionsStrategy.

As duas classes são do tipo Abstract (abstratas).

  • HS.FHIRServer.API.InteractionsStrategy é uma classe Abstractque deve ser implementada para customizar o comportamento do Servidor FHIR.
  • HS.FHIRServer.API.RepoManager é uma classe Abstract que deve ser implementada para customizar o armazenamento do servidor FHIR.

Observações

Para o nosso exemplo, vamos focar apenas na classe HS.FHIRServer.API.InteractionsStrategy, mesmo que a classe HS.FHIRServer.API.RepoManager também seja implementada e obrigatória para customizar o Servidor FHIR.
A classe HS.FHIRServer.API.RepoManager é implementada pela classe HS.FHIRServer.Storage.Json.RepoManager, que é a implementação padrão do Servidor FHIR.

Onde achar o código

Todo o código fonte pode ser achado nesse repositório: iris-fhir-python-strategy
A pasta src contém as seguintes pastas:

  • python : contém o código Python
  • cls : contem o código ObjectScript que é usado para chamar o código Python

Como implementar uma Strategy

Nesta prova de conceito, só estaremos interessados em como implementar uma Strategy em Python, não em como implementar um RepoManager (gerenciador de repositório).

Para implementar uma Strategy você deve criar pelo menos duas classes:

  • Uma classe que herda da HS.FHIRServer.API.InteractionsStrategy
  • Uma classe que herda da HS.FHIRServer.API.Interactions class

Implementação de InteractionsStrategy

A classe HS.FHIRServer.API.InteractionsStrategy foca em customizar o comportamento do Servidor FHIR ao sobrescrever os seguintes métodos:

  • GetMetadataResource: chamado para pegar os metadados do servidor FHIR
    • esse é o único método que vamos sobrescrever nessa prova de conceito

HS.FHIRServer.API.InteractionsStrategy também tem dois parâmetros:

  • StrategyKey : um identificador único para a InteractionsStrategy
  • InteractionsClass : o nome da subclasse de Interactions

Implementação de Interactions

A classe HS.FHIRServer.API.Interactions foca em customizar o comportamento do Servidor FHIR ao sobrescrever os seguintes métodos:

  • OnBeforeRequest : chamada antes da requisição ser enviada ao servidor
  • OnAfterRequest : chamada após a requisição ser enviada ao servidor
  • PostProcessRead : chamada após a operação de leitura estar finalizada
  • PostProcessSearch : chamada após a operação de procura estar finalizada
  • Read : chamada para ler um recurso
  • Add : chamada para adicionar um recurso
  • Update : chamada para atualizar um recurso
  • Delete : chamada para deletar um recurso
  • e muito mais...

Nós implementamos a classe HS.FHIRServer.API.Interactions no arquivo src/cls/FHIR/Python/Interactions.cls.

 

Spoiler

 

A classeFHIR.Python.Interactions herda das classes HS.FHIRServer.Storage.Json.Interactions e FHIR.Python.Helper.

A classe HS.FHIRServer.Storage.Json.Interactions é o padrão de implementação do Servidor FHIR.

A classe FHIR.Python.Helper foca em ajudar chamar o código Python pelo ObjectScript .

A classe FHIR.Python.Interactions sobrescreve os métodos a seguir:

  • %OnNew : chamado quando o objeto é criado
    • nós usamos esse método para definir o caminho, nome da classe de, e nome de módulo do Python a partir das variáveis de ambiente.
    • se as variáveis de ambiente não forem definidas, utilizamos valores padrão.
    • também definimos a classe python
    • chamamos o método %OnNew da classe pai.
Method %OnNew(pStrategy As HS.FHIRServer.Storage.Json.InteractionsStrategy) As %Status
{
    // Primeiro, definimos o caminho Python por uma variável de ambiente
    set ..PythonPath = $system.Util.GetEnviron("INTERACTION_PATH")
    // Então, definimos o nome da classe Python pela variável de ambiente
    set ..PythonClassname = $system.Util.GetEnviron("INTERACTION_CLASS")
    // Depois, definimos o nome do módulo Python da variável de ambiente
    set ..PythonModule = $system.Util.GetEnviron("INTERACTION_MODULE")

    if (..PythonPath = "") || (..PythonClassname = "") || (..PythonModule = "") {
        // usa valores padrão
        set ..PythonPath = "/irisdev/app/src/python/"
        set ..PythonClassname = "CustomInteraction"
        set ..PythonModule = "custom"
    }

    // A seguir, definimos a classe Python
    do ..SetPythonPath(..PythonPath)
    set ..PythonClass = ..GetPythonInstance(..PythonModule, ..PythonClassname)

    quit ##super(pStrategy)
}
  • OnBeforeRequest : chamada antes da requisição ser enviada ao servidor
    • chamamos o método on_before_request da classe Python
    • passamos os objetos HS.FHIRServer.API.Service e HS.FHIRServer.API.Data.Request , o corpo da requisição e o tempo limite de execução.
Method OnBeforeRequest(
    pFHIRService As HS.FHIRServer.API.Service,
    pFHIRRequest As HS.FHIRServer.API.Data.Request,
    pTimeout As %Integer)
{
    // OnBeforeRequest é chamado antes de cada requisição ser processada.
    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 : chamado após a requisição ser enviada ao servidor
    • chamamos o método on_after_request da classe Python
    • passamos os objetos HS.FHIRServer.API.Service, HS.FHIRServer.API.Data.Request e HS.FHIRServer.API.Data.Response e o corpo da resposta
Method OnAfterRequest(
    pFHIRService As HS.FHIRServer.API.Service,
    pFHIRRequest As HS.FHIRServer.API.Data.Request,
    pFHIRResponse As HS.FHIRServer.API.Data.Response)
{
    // OnAfterRequest é chamado após cada requisição ser processada
    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)
    }
}
  • E por aí em diante...

Interactions em Python

A classe FHIR.Python.Interactions chama os métodos on_before_request, on_after_request, ... da classe Python.

Aqui está uma classe Python abstrata:

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 é chamada depois que a requisição é enviada ao servidor.
        param fhir_service: o objeto de serviço fhir iris.HS.FHIRServer.API.Service
        param fhir_request: o objeto de requisição fhir iris.FHIRServer.API.Data.Request
        param timeout: o tempo limite de execução em segundos
        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 é chamada após a requisição ser enviada ao servidor.
        param fhir_service: o objeto de serviço fhir iris.HS.FHIRServer.API.Service
        param fhir_request: o objeto de requisição fhir iris.FHIRServer.API.Data.Request
        param fhir_response: o objeto de resposta fhir iris.FHIRServer.API.Data.Response
        return: None
        """


    @abc.abstractmethod
    def post_process_read(self,
                          fhir_object:dict) -> bool:
        """
        post_process_read é chamado após a operação de leitura finalizar.
        param fhir_object: o objeto fhir
        return: True - o recurso deve ser  retornado ao cliente, False - caso contrário
        """


    @abc.abstractmethod
    def post_process_search(self,
                            rs:'iris.HS.FHIRServer.Util.SearchResult',
                            resource_type:str):
        """
        post_process_search é chamada após a operação de pesquisa finalizar.
        param rs: o resultado da pesquisa iris.HS.FHIRServer.Util.SearchResult
        param resource_type: o tipo de recurso
        return: None
        """

Implementação da classe Python abstrata

from FhirInteraction import Interaction

class CustomInteraction(Interaction):

    def on_before_request(self, fhir_service, fhir_request, body, timeout):
        # Extrai o usuário e seus papéis para essa requisição
        # então o consentimento pode avaliado.
        self.requesting_user = fhir_request.Username
        self.requesting_roles = fhir_request.Roles

    def on_after_request(self, fhir_service, fhir_request, fhir_response, body):
        # Limpa o usuário e seus papéis entre as requisições.
        self.requesting_user = ""
        self.requesting_roles = ""

    def post_process_read(self, fhir_object):
        # Avalia consentimento baseado no recurso e usuários/papéis
        # Retornar 0 indica que esse recurso não deveria ser exibido - um erro  "404 Not Found" (não encontrado)
        # Será retornado ao usuário.
        return self.consent(fhir_object['resourceType'],
                        self.requesting_user,
                        self.requesting_roles)

    def post_process_search(self, rs, resource_type):
        # Itera por cada recurso no conjunto de pesquisa e avalia
        # Consentimento baseado no recurso e usuário/papéis
        # Cada linha marcada como deletada e salva será excluída do Bundle (conjunto ad resposta).
        rs._SetIterator(0)
        while rs._Next():
            if not self.consent(rs.ResourceType,
                            self.requesting_user,
                            self.requesting_roles):
                # Marca a linha como deletada e salva.
                rs.MarkAsDeleted()
                rs._SaveRow()

    def consent(self, resource_type, user, roles):
        # Exemplo de lógica de consentimento - só permite que usuários com o papel '%All' vejam
        # Recursos de observações.
        if resource_type == 'Observation':
            if '%All' in roles:
                return True
            else:
                return False
        else:
            return True

Muito longo, faça um resumo

A classe FHIR.Python.Interactions é um invólucro para chamar a classe Python.

As classes abstratas IRSI são implementadas para envolver as classes abstratas Python 🥳.

Isso nos ajuda a manter o código Python e o código ObjectScript separados e então se beneficiar do melhor dos dois mundos.

Discussão (0)1
Entre ou crie uma conta para continuar