Como implementar sua própria estrutura de dados em Python

O Python fornece suporte completo para implementar sua própria estrutura de dados usando classes e operadores personalizados. Neste tutorial, você implementará uma estrutura de dados de pipeline personalizada que pode executar operações arbitrárias em seus dados. Usaremos o Python 3.

A estrutura de dados do pipeline

A estrutura de dados do pipeline é interessante porque é muito flexível. Consiste em uma lista de funções arbitrárias que podem ser aplicadas a uma coleção de objetos e produzir uma lista de resultados. Vou aproveitar a extensibilidade do Python e usar o caractere pipe (|) para construir o pipeline.

Exemplo ao Vivo

Antes de mergulhar em todos os detalhes, vamos ver um pipeline muito simples em ação:

O que está acontecendo aqui? Vamos decompô-lo passo a passo. o primeiro elemento range(5) cria uma lista de inteiros [0, 1, 2, 3, 4]. Os inteiros são alimentados em um pipeline vazio designado por Pipeline(). Então uma double função é adicionada ao pipeline e, finalmente, o cool Ω A função encerra o pipeline e faz com que ele se avalie.

A avaliação consiste em pegar a entrada e aplicar todas as funções do pipeline (neste caso apenas o double função). Finalmente, armazenamos o resultado em uma variável chamada x e imprimi-lo.

Aulas de Python

O Python oferece suporte a classes e possui um modelo orientado a objetos muito sofisticado, incluindo herança múltipla, mixins e sobrecarga dinâmica. Um __init__() função serve como um construtor que cria novas instâncias. O Python também oferece suporte a um modelo avançado de metaprogramação, que não abordaremos neste artigo.

Aqui está uma classe simples que tem um __init__() construtor que recebe um argumento opcional x (o padrão é 5) e o armazena em um self.x atributo. Ele também tem um foo() método que retorna o self.x atributo multiplicado por 3:

Aqui está como instanciá-lo com e sem um argumento x explícito:

Operadores personalizados

Com o Python, você pode usar operadores personalizados para suas classes para obter uma sintaxe mais agradável. Existem métodos especiais conhecidos como métodos “dunder”. O “dunder” significa “sublinhado duplo”. Esses métodos como __eq__, __gt__ e __or__ permitem que você use operadores como ==, > e | com suas instâncias de classe (objetos). Vamos ver como eles trabalham com o A classe.

Se você tentar comparar duas instâncias diferentes de A entre si, o resultado será sempre False independente do valor x:

Isso ocorre porque o Python compara os endereços de memória dos objetos por padrão. Digamos que queremos comparar o valor de x. Podemos adicionar um especial __eq__ operador que recebe dois argumentos, self e othere compara seus x atributo:

Vamos verificar:

Implementando o Pipeline como uma classe Python

Agora que abordamos o básico de classes e operadores personalizados em Python, vamos usá-lo para implementar nosso pipeline. o __init__() O construtor recebe três argumentos: funções, entrada e terminais. O argumento “funções” é uma ou mais funções. Essas funções são os estágios no pipeline que operam nos dados de entrada.

O argumento “input” é a lista de objetos nos quais o pipeline irá operar. Cada item da entrada será processado por todas as funções do pipeline. O argumento “terminals” é uma lista de funções e, quando uma delas é encontrada, o pipeline avalia a si mesmo e retorna o resultado. Os terminais são por padrão apenas a função de impressão (no Python 3, print é uma função).

Observe que dentro do construtor, um misterioso Ω é adicionado aos terminais. Vou explicar isso a seguir.

O Construtor de Oleoduto

Aqui está a definição de classe e o __init__() construtor:

Python 3 suporta totalmente Unicode em nomes de identificadores. Isso significa que podemos usar símbolos legais como Ω para nomes de variáveis ​​e funções. Aqui, declarei uma função de identidade chamada Ωque serve como uma função terminal:

Eu poderia ter usado a sintaxe tradicional também:

o __or__ e __ror__ operadores

Aqui vem o núcleo da classe Pipeline. Para usar o | (símbolo de tubo), precisamos substituir alguns operadores. o | O símbolo é usado pelo Python para bit a bit ou inteiros. No nosso caso, queremos substituí-lo para implementar o encadeamento de funções, bem como alimentar a entrada no início do pipeline. Essas são duas operações separadas.

o __ror__ operador é invocado quando o segundo operando é uma instância de Pipeline, desde que o primeiro operando não seja. Ele considera o primeiro operando como entrada e o armazena na self.input atributo e retorna a instância do Pipeline de volta (o self). Isso permite o encadeamento de mais funções posteriormente.

Aqui está um exemplo onde o __ror__() operador seria invocado: 'hello there' | Pipeline()

o __or__ operador é invocado quando o primeiro operando é um Pipeline (mesmo que o segundo operando também seja um Pipeline). Ele aceita o operando como uma função chamável e afirma que o func operando é realmente chamável.

Em seguida, ele anexa a função ao self.functions atributo e verifica se a função é uma das funções do terminal. Se for um terminal, todo o pipeline é avaliado e o resultado é retornado. Se não for um terminal, o próprio pipeline é retornado.

Avaliando o Pipeline

À medida que você adiciona mais e mais funções não terminais ao pipeline, nada acontece. A avaliação efetiva é adiada até o eval() método é chamado. Isso pode acontecer adicionando uma função de terminal ao pipeline ou chamando eval() diretamente.

A avaliação consiste em iterar todas as funções no pipeline (incluindo a função de terminal, se houver) e executá-las em ordem na saída da função anterior. A primeira função no pipeline recebe um elemento de entrada.

Usando o Pipeline Efetivamente

Uma das melhores maneiras de usar um pipeline é aplicá-lo a vários conjuntos de entrada. No exemplo a seguir, um pipeline sem entradas e sem funções de terminal é definido. Tem duas funções: a famigerada double função que definimos anteriormente e o padrão math.floor.

Em seguida, fornecemos três entradas diferentes. No loop interno, adicionamos o Ω função terminal quando a invocamos para coletar os resultados antes de imprimi-los:

Você poderia usar o print função de terminal diretamente, mas cada item será impresso em uma linha diferente:

Melhorias Futuras

Existem algumas melhorias que podem tornar o pipeline mais útil:

  • Adicione streaming para que ele possa funcionar em fluxos infinitos de objetos (por exemplo, leitura de arquivos ou eventos de rede).
  • Forneça um modo de avaliação em que toda a entrada é fornecida como um único objeto para evitar a incômoda solução alternativa de fornecer uma coleção de um item.
  • Adicione várias funções de pipeline úteis.

Conclusão

Python é uma linguagem muito expressiva e está bem equipada para projetar sua própria estrutura de dados e tipos personalizados. A capacidade de substituir operadores padrão é muito poderosa quando a semântica se presta a essa notação. Por exemplo, o símbolo de tubo (|) é muito natural para um pipeline.

Muitos desenvolvedores Python apreciam as estruturas de dados integradas do Python, como tuplas, listas e dicionários. No entanto, projetar e implementar sua própria estrutura de dados pode tornar seu sistema mais simples e fácil de trabalhar, elevando o nível de abstração e ocultando detalhes internos dos usuários. De uma chance.

Deixe uma resposta