Coletânea Python do ZERO às Redes Neurais Artificiais

Fernando Feltrin · Capítulo 194 de 287

Páginas do PDF

Coletânea Python do ZERO às Redes Neurais Artificiais

Filas (Queues) e Nodes

 

Uma prática bastante comum, embora demande a criação de um grande bloco de código de funções validadoras, é gerenciar filas via OO, fazendo uso de Nodes (nós que são dinâmicos tanto para inserção de dados quanto para atualização dos mesmos).

Raciocine que via OO podemos criar toda uma estrutura base que terá performance superior se comparado a um modelo equivalente estruturado, ou em outras palavras, melhor gerenciamento de listas do que pela forma básica por meio de seus índices, já que neste tipo de arquitetura de dados, temos elementos de uma lista como objetos dinamicamente alocados, permitindo a manipulação dos mesmos de forma semi automatizada assim como suporte a grandes volumes de dados.

Tenha em mente que em uma lista estruturada, a cada atualização de elementos em fila, é necessário internamente mover o elemento e atualizar seu índice um a um, enquanto nos exemplos que você verá usando de OO, este processo é internamente dinâmico, de modo que todos elementos assim como seus índices (se houver) estarão dentro de um objeto.

Para isso, você notará que os blocos exibidos a seguir foram criados de modo a criar uma cadeia de dependências, ou seja, espaços alocados esperados para serem alimentados com dados, e ao mesmo tempo, tais estruturas realizarão uma série de validações para que os elementos da fila tenham o comportamento esperado, atualizando sua posição na fila conforme a fila anda.

Sendo assim, teremos “nós” em uma fila que respeitam a lógica first in first out, e que podem trabalhar com qualquer elemento, sem restrições de tipo ou número.

Como mencionado anteriormente, o que justifica a criação desta estrutura de gerenciamento de filas é uma questão de performance em função da maneira como os dados serão tratados via OO.

Apesar da estrutura ser um tanto quanto grande, procure se familiarizar com a mesma, principalmente se seu objetivo é posteriormente aprender sobre redes neurais artificiais.

Este sistema de dependências e validações é muito comum em arquiteturas como as de redes neurais artificiais intuitivas, onde uma IA realiza processamento em tempo real se retroalimentando, atualizando e validando seus estados constantemente.

 

Como de costume, em casos onde se faz o uso de bibliotecas, módulos e pacotes, logo no início do nosso código realizamos as devidas importações.

Nesse caso, da biblioteca typing importamos Any, que por sua vez é uma estrutura de dado que pode ser inserida em qualquer contexto, guardando o lugar de algum tipo de dado que a venha substituir.

Em seguida é criada uma constante de nome EMPTY_NODE_VALUE que recebe como atributo uma string homônima. Por mais estranho que isso pareça uma vez que em Python não temos constantes, todos tipos de dados tanto estruturados quanto orientados a objeto são sempre dinâmicos.

Aqui, apenas por convenção, criamos uma variável equivalente a uma constante, com a simples função de atribuir algum dado onde possa não existir, evitando erros de interpretação.

Também é criada uma classe Erro, que levanta uma exceção via parâmetro Exception, esta estrutura, que também será usada ao longo de nosso código, da mesma forma que a constante EMPTY_NODE_VALUE, evitando que o programa pare de funcionar em função de um erro de interpretação.

Na sequência criamos a nossa classe Node, dentro da mesma temos um método construtor __init__( ) que recebe como atributo um valor qualquer via value: Any e retorna um None.

Por hora raciocine que um dos fatores que devem ser considerados dentro desta lógica é que existirá um momento inicial onde a fila pode estar vazia, assim como um momento final onde a fila novamente pode ficar vazia, em ambos os casos, os espaços alocados por value podem (Any) ou não (None) tem algum dado atribuído a si.

Como esperado, são criados os objetos value que recebe um valor qualquer, da mesma forma é criado o objeto next que por sua vez recebe Node.

De forma opcional podem ser criados um método de classe chamado __repr__( ) que retorna value em forma de string, simplesmente para que se possa realizar a leitura do dado/valor atribuído a value.

Também é criado um método de classe __bool__( ) que irá retornar um valor True quando o valor de value for diferente de EMPTY_NODE_VALUE, o que em outras palavras nos diz que value não está mais em seu estado inicial vazio, possuindo agora dados atribuídos a si.

Na sequência é criada a classe Queue que em seu método construtor retorna None, dentro deste método são criados os objetos first e last, ambos recebendo como atributos Node, que chama a classe Node parametrizando a mesma com EMPTY_NODE_VALUE. Em outras palavras, aqui definimos uma posição inicial e uma final da fila, inicialmente vazias.

Por fim, é criado um contador inicialmente zerado, lembrando que aqui, apenas por controle, toda vez que for adicionado um elemento a fila, _count será incrementado, da mesma forma, toda vez que um elemento for removido da fila, _count será decrementado.

 

Ainda dentro de Queue, criamos o método de classe enqueue, função que ficará responsável por adicionar elementos na fila e gerenciar a mesma na sequência lógica esperada.

Inicialmente dentro de seu corpo é criado um objeto de nome novo_node que instancia Node repassando para o mesmo o valor que estiver atribuído a node_value.

Em seguida é criada uma estrutura condicional onde inicialmente se first ainda não tiver sido construído e alocado na memória, é criado o mesmo com os dados iniciais de novo_node. Da mesma forma é criado last com valor inicial oriundo de novo_node.

O importante a se entender nesta etapa é que pensando em evitar erros, esses objetos são criados mesmo que não tenham dados inicialmente, sendo apenas espaços reservados para que sejam alimentados posteriormente.

Caso as condições impostas anteriormente não sejam alcançadas, last.next recebe os dados de novo_node, atualizando last e incrementando o contador em 1.

Raciocine que aqui nesse processo estamos forçando para que o elemento que entrar na fila ocupe a última posição da fila, claro que quando é adicionado o primeiro elemento ele ocupa a primeira posição da fila, mas o importante é que novos elementos que sejam adicionados à fila entrem na ordem correta, ao final da fila.

 

Criada a função que permitirá a adição de novos elementos na fila, hora de criar a função de objetivo oposto, uma vez que uma das funcionalidades que não podem faltar em um programa desses é justamente a de poder remover elementos da lista.

Sendo assim, criamos o método de classe pop( ) que retorna um Node. Em seu corpo é criada uma estrutura condicional onde caso não exista nenhum dado em first, e o usuário ainda assim tentar remover algum elemento desta posição, é levantado um erro dizendo ao usuário que não existem elementos a serem removidos da lista.

Da mesma forma é criado o método hasattr( ) palavra reservada do sistema que nada mais é do que uma simples função validadora, verificando se de fato existem ou não dados em first.

Note que neste caso, first é atualizado com o dado que houver em first.next, uma vez que quando a fila anda, o segundo elemento passa a ser o primeiro, o terceiro o segundo, e assim para todos elementos sucessivamente.

Aqui também ocorre a validação onde caso não existam elementos para ocupar a primeira posição da fila, será adicionado um EMPTY_NODE_VALUE para que não ocorram erros de interpretação.

Por fim é retornado o valor atualizado de first.

 

Dando prosseguimento, é possível criar de forma totalmente opcional alguns métodos de classe validadores ou verificadores. Por exemplo peek( ) pode simplesmente pegar o dado/valor do primeiro elemento e exibir para o usuário; __len__( ) pode ler o último valor de _count e retornar o tamanho da fila; __bool__( ) pode retornar True caso o valor de _count seja 1 ou maior que 1, significando que existe ao menos um elemento na fila; __iter__( ) que retorna toda a lista; __next__( ) que retorna qualquer coisa, validando se não existem mais elementos a serem removidos da lista.

Por fim, é perfeitamente possível criar um objeto qualquer que instancia nossa classe Queue, e a partir disso se pode adicionar elementos a lista via função enqueue( ) assim como remover elementos da mesma via função pop( ).

Como dito no início desse capítulo, essa é uma estrutura grande e um pouco confusa, porém de performance e usabilidade muito mais eficiente do que se trabalhar com o gerenciamento de filas de forma estruturada.