Programação Funcionado COMO FAZER — documentação Python 3.10.4
Probabilidade formal
Fácil de depurar e testar
Tipos de Dados que Suportam Iteradores
Expressões do gerador e compreensões de lista
Passando valores para um gerador
O módulo itertools
Criando novos iteradores
O módulo functools
O módulo operator
Pequenas funções e as expressões lambda
Histórico de Revisão e Reconhecimentos
HowTo - Guia de descritores
Programação Funcionado COMO FAZER¶
Nesse documento, nós vamos passear pelos recursos do Python que servem para implementar programas de forma funcional. Após uma introdução dos conceitos da programação funcional, nós veremos as propriedades da linguagem como iteradores e geradores e bibliotecas de módulos relevantes como um itertools e functools.
Essa seção explica o conceito básico da programação funcional; se você está interessado em aprender sobre os recursos da linguagem Python, pule essa etapa para a seção Iteradores.
As linguagens de programação suportam decompor problemas de diversas maneiras diferentes:
A Maioria das linguagens de programação são procedural: os programas são listas de instruções que dizem ao computador o que fazer com as entradas do programa. C, Pascal, e mesmo o Unix shells são linguagens procedurais.
Em linguagens declarativas, você escreve uma especificação que descreve o problema a ser resolvido, e a implementação do idioma descreve como executar a computação de forma eficiente. O SQL é o idioma declarativo com o qual provavelmente você está familiarizado; Uma consulta SQL descreve o conjunto de dados que deseja recuperar e o mecanismo SQL decide se deseja escanear tabelas ou usar índices, quais subcláusulas devem ser realizadas primeiro, etc.
Os programas ** orientados a objetos ** manipulam coleções de objetos. Os objetos têm estado interno e métodos de suporte que consultam ou modificam esse estado interno de alguma forma. Smalltalk e Java são linguagens orientadas a objetos. C++ e Python são idiomas que suportam programação orientada a objetos, mas não forçam o uso de recursos orientados a objetos.
A programação funcional decompõe um problema em um conjunto de funções. Idealmente, as funções apenas recebem entradas e produzem saídas, e não têm nenhum estado interno que afete a saída produzida para uma determinada entrada. Linguagens funcionais bem conhecidos incluem a família ML (Standard ML, OCaml e outras variantes) e Haskell.
Os designers de algumas linguagens de computadores escolhem enfatizar uma abordagem particular da programação. Isso muitas vezes torna difícil escrever programas que usam uma abordagem diferente. Outros idiomas são linguagens com multiparadigmas que suportam várias abordagens diferentes. Lisp, C++ e Python são multiparadigmas; Você pode escrever programas ou bibliotecas que são em grande parte processuais, orientadas a objetos ou funcionais em todos esses idiomas. Em um grande programa, diferentes seções podem ser escritas usando diferentes abordagens; A GUI pode ser orientada a objetos enquanto a lógica de processamento é processual ou funcional, por exemplo.
Em um programa funcional, a entrada flui através de um conjunto de funções. Cada função opera em sua entrada e produz alguma saída. O estilo funcional desencoraja funções com efeitos colaterais que modificam o estado interno ou fazem outras alterações que não são visíveis no valor de retorno da função. As funções que não têm efeitos colaterais são chamadas puramente funcionais. Evitar efeitos colaterais significa não usar estruturas de dados que sejam atualizadas à medida que um programa é executado; A saída de cada função só deve depender da sua entrada.
Os programas Python escritos em estilo funcional geralmente não irão ao extremo de evitar todas as I/O ou todas as atribuições; Em vez disso, eles fornecerão uma interface de aparência funcional, mas usarão recursos não funcionais internamente. Por exemplo, a implementação de uma função ainda usará atribuições para variáveis locais, mas não modificará variáveis globais ou terá outros efeitos colaterais.
A programação funcional pode ser considerada o oposto da programação orientada a objetos. Os objetos são pequenas cápsulas contendo algum estado interno, juntamente com uma coleção de chamadas de método que permitem modificar este estado, e os programas consistem em fazer o conjunto correto de mudanças de estado. A programação funcional quer evitar as mudanças de estado tanto quanto possível e funciona com o fluxo de dados entre as funções. Em Python você pode combinar as duas abordagens escrevendo funções que levam e retornam instâncias que representam objetos em seu aplicativo (mensagens de e-mail, transações, etc.).
O design funcional pode parecer uma restrição estranha para trabalhar por baixo. Por que você deve evitar objetos e efeitos colaterais? Existem vantagens teóricas e práticas para o estilo funcional:
Probabilidade formal.
Componibilidade.
Fácil de depurar e testar.
Probabilidade formal¶
Um benefício teórico é que é mais fácil construir uma prova matemática de que um programa funcional é correto.
Durante muito tempo os pesquisadores estiveram interessados em encontrar maneiras de provar matematicamente programas corretos. Isso é diferente de testar um programa em inúmeras entradas e concluir que sua saída geralmente é correta, ou ler o código-fonte de um programa e concluir que o código parece certo; O objetivo é uma prova rigorosa de que um programa produz o resultado certo para todas as entradas possíveis.
A técnica usada para comprovar os programas corretos é escrever invariantes, propriedades dos dados de entrada e das variáveis do programa que são sempre verdadeiras. Para cada linha de código, você mostra que se os invariantes X e Y forem verdadeiros antes da linha ser executada, os invariantes X' e Y' ligeiramente diferentes são verdadeiros após a linha ser executada. Isso continua até chegar ao final do programa, em que ponto as invariantes devem corresponder às condições desejadas na saída do programa.
A evitação das tarefas funcionais ocorreu porque as tarefas são difíceis de tratar com esta técnica; As atribuições podem invadir invariantes que eram verdadeiras antes da atribuição sem produzir novos invariantes que possam ser propagados para a frente.
Infelizmente, prpvar que os programas estão corretos são praticamente impraticáveis e não relevantes para o software Python. Mesmo os programas triviais exigem provas de várias páginas; A prova de correção para um programa moderadamente complicado seria enorme, e poucos ou nenhum dos programas que você usa diariamente (o interpretador Python, seu analisador XML, seu navegador) poderia ser comprovado correto. Mesmo que você tenha anotado ou gerado uma prova, então haveria a questão de verificar a prova; Talvez haja um erro nisso, e você acredita erroneamente que você provou o programa corretamente.
Modularidade¶
Um benefício mais prático da programação funcional é que isso força você a quebrar seu problema em pequenos pedaços. Os programas são mais modulares como resultado. É mais fácil especificar e escrever uma pequena função que faz uma coisa do que uma grande função que realiza uma transformação complicada. Pequenas funções também são mais fáceis de ler e verificar erros.
Fácil de depurar e testar¶
Testar e depurar um programa de estilo funcional é mais fácil.
A depuração é simplificada porque as funções são geralmente pequenas e claramente especificadas. Quando um programa não funciona, cada função é um ponto de interface onde você pode verificar se os dados estão corretos. Você pode observar as entradas e saídas intermediárias para isolar rapidamente a função responsável por um erro.
O teste é mais fácil porque cada função é um assunto potencial para um teste unitário. As funções não dependem do estado do sistema que precisa ser replicado antes de executar um teste; Em vez disso, você só precisa sintetizar a entrada certa e depois verificar se o resultado corresponde às expectativas.
Componibilidade¶
À medida que você trabalha em um programa de estilo funcional, você escreverá várias funções com diferentes entradas e saídas. Algumas dessas funções serão inevitavelmente especializadas em um aplicativo particular, mas outras serão úteis em uma grande variedade de programas. Por exemplo, uma função que leva um caminho de diretório e retorna todos os arquivos XML no diretório, ou uma função que leva um nome de arquivo e retorna seu conteúdo, pode ser aplicada em muitas situações diferentes.
Com o tempo você formará uma biblioteca pessoal de utilitários. Muitas vezes você montará novos programas organizando funções existentes em uma nova configuração e escrevendo algumas funções especializadas para a tarefa atual.
Começarei por olhar para um recurso de linguagem Python que é uma base importante para escrever programas de estilo funcional: iteradores.
Um iterador é um objeto que representa um fluxo de dados; este objeto retorna os dados um elemento por vez. Um iterador Python deve suportar um método chamado iterator.__next__() que não leva argumentos e sempre retorna o próximo elemento do fluxo. Se não houver mais elementos no fluxo, iterator.__next__() deve aumentar a exceção StopIteration. Os iteradores não precisam ser finitos; no entanto, é perfeitamente razoável escrever um iterador que produza um fluxo infinito de dados.
A função embutida iter() leva um objeto arbitrário e tenta retornar um iterador que retornará os conteúdos ou elementos do objeto, caso o contrario retorna TypeError se o objeto não suportar a iteração. Vários dos tipos de dados embutidos do Python suportam a iteração, sendo as listas e os dicionários mais comuns. Um objeto é chamado iterable se você pode obter um iterador para ele.
Você pode experimentar a interface de iteração manualmente:
Python espera objetos iteráveis em vários contextos diferentes, sendo o mais importante a instrução for. Na declaração for X in Y, Y deve ser um iterador ou algum objeto para o qual iter() pode criar um iterador. Estas duas declarações são equivalentes:
Iteradores também podem ser materializados como listas ou tuplas, utilizando funções de construção list() ou tuple():
A descompilação de sequência também suporta iteradores: se você sabe que um iterador retornará N elementos, você pode descompactá-los em uma N-tupla:
Funções embutidas, tais como max() e min() podem ter um único argumento de iterador e retornarão o elemento maior ou menor. Os operadores "in" e "not in" também aceitam iteradores: X in iterator é verdadeiro se X for encontrado no fluxo retornado pelo iterador. Você enfrentará problemas óbvios se o iterador for infinito; max(), min() nunca retornará, e se o elemento X nunca aparecer no fluxo, os operadores "in" e "not in" não retornarão também.
Observe que você só pode avançar em um iterador; não há como obter o elemento anterior, redefinir o iterador ou fazer uma cópia dele. Os objetos iteradores podem opcionalmente fornecer esses recursos adicionais, mas o protocolo do iterador especifica apenas o método iterator.__next__(). As funções podem, portanto, consumir toda a saída do iterador, e se você precisa fazer algo diferente com o mesmo fluxo, você terá que criar um novo iterador.
Tipos de Dados que Suportam Iteradores¶
Já vimos como listas e tuplas suportam iteradores. De fato, qualquer tipo de sequência de Python, como strings, suportará automaticamente a criação de um iterador.
Chamar iter() em um dicionário retorna um iterador que irá percorrer as chaves do dicionário:
Aplicando iter() para um dicionário sempre percorre as teclas, mas os dicionários têm métodos que retornam outros iteradores. Se você deseja iterar sobre valores ou pares de chave/valor, você pode chamar explicitamente os métodos values() ou items() para obter um iterador apropriado.
O construtor dict() pode aceitar um iterador que retorna um fluxo finito de tuplas (chave, valor):
Os arquivos também suportam a iteração chamando o método readline() até que não haja mais linhas no arquivo. Isso significa que você pode ler cada linha de um arquivo como este:
Os conjuntos podem tirar seus conteúdos de uma iterável e permitir que você faça uma iteração sobre os elementos do conjunto:
Expressões do gerador e compreensões de lista¶
Duas operações comuns na saída de um iterador são 1) executando alguma operação para cada elemento, 2) selecionando um subconjunto de elementos que atendam a alguma condição. Por exemplo, com uma lista de cadeias de caracteres, você pode querer retirar o espaço em branco de cada linha ou extrair todas as sequências de caracteres que contenham uma determinada substring.
As compreensões da lista e as expressões do gerador (forma curta: "listcomps" e "genexps") são uma notação concisa para tais operações, emprestado da linguagem de programação funcional Haskell (https://www.haskell.org/). Você pode tirar todos os espaços em branco de um fluxo de strings com o seguinte código:
Você pode selecionar apenas determinados elementos adicionando uma condição "if":
Com uma compreensão de lista, você recebe uma lista Python; Stripped_list é uma lista contendo as linhas resultantes, e não um iterador. As expressões do gerador retornam um iterador que calcula os valores conforme necessário, não precisando materializar todos os valores ao mesmo tempo. Isso significa que as compreensões da lista não são úteis se você estiver trabalhando com iteradores que retornam um fluxo infinito ou uma quantidade muito grande de dados. As expressões do gerador são preferíveis nessas situações.
As expressões do gerador são cercadas por parênteses ("()") e as compreensões da lista são cercadas por colchetes ("[]"). As expressões do gerador têm a forma:
Novamente, para uma compreensão de lista, apenas os suportes externos são diferentes (colchetes em vez de parênteses).
Os elementos do resultado gerado serão os valores sucessivos de expression. As cláusulas if são todas opcionais; Se presente, expression só é avaliado e adicionado ao resultado quando condition é verdadeiro.
As expressões do gerador sempre devem ser escritas dentro de parênteses, mas os parênteses que sinalizam uma chamada de função também contam. Se você quiser criar um iterador que será imediatamente passado para uma função, você pode escrever:
As cláusulas for...in contêm as sequências a serem repetidas. As sequências não precisam ser do mesmo comprimento, porque são iteradas da esquerda para a direita, não em paralelo. Para cada elemento em sequence1, sequence2 é enrolado desde o início. sequence3 é então percorrido para cada par resultante de elementos de sequence1 e sequence2.
Em outras palavras, uma lista de compreensão ou expressão do gerador é equivalente ao seguinte código Python:
Isso significa que, quando existem várias cláusulas for...in, mas não if, o comprimento da saída resultante será igual ao produto dos comprimentos de todas as sequências. Se você tiver duas listas de comprimento 3, a lista de saída tem 9 elementos de comprimento:
Para evitar a introdução de uma ambiguidade na gramática de Python, se expression estiver criando uma tupla, ela deve estar cercada de parênteses. A primeira lista de compreensão abaixo é um erro de sintaxe, enquanto o segundo está correto:
Os geradores são uma classe especial de funções que simplificam a tarefa de escrever iteradores. As funções convencionais calculam um valor e o retornam, mas os geradores retornam um iterador que retorna um fluxo de valores.
Você está sem dúvida familiarizado com o funcionamento das funções convencionais em Python ou C. Quando você chama uma função, ela recebe um espaço de nome privado onde suas variáveis locais são criadas. Quando a função atinge uma instrução return, as variáveis locais são destruídas e o valor retornado ao chamador. Uma chamada posterior para a mesma função cria um novo espaço de nome privado e um novo conjunto de variáveis locais. Mas, e se as variáveis locais não fossem descartadas ao sair de uma função? E se você pudesse retomar a função onde ele deixou? Isto é o que os geradores fornecem; Eles podem ser pensados como funções resumíveis.
Aqui está um exemplo simples de uma função geradora:
Qualquer função contendo um argumento nomeado yield é uma função de gerador; isso é detectado pelo compilador de bytecode do Python, que compila a função especialmente como resultado.
Quando você chama uma função de gerador, não retorna um único valor; em vez disso, ele retorna um objeto gerador que suporte o protocolo do iterador. Ao executar a expressão yield, o gerador exibe o valor de i, semelhante a uma instrução return. A grande diferença entre yield e uma declaração return é que, ao atingir um yield, o estado de execução do gerador é suspenso e as variáveis locais são preservadas. Na próxima chamada ao método do gerador __next__(), a função irá continuar a ser executada.
Aqui está um uso simples do gerador generate_ints()
Você pode igualmente escrever for i in generate_ints(5), ou a, b, c = generate_ints(3).
Dentro de uma função de gerador, return value faz com que StopIteration(value) seja gerado a partir do método __next__(). Uma vez que isso acontece, ou a parte inferior da função é alcançada, a procissão dos valores termina e o gerador não pode render mais valores.
Você pode conseguir o efeito de geradores manualmente, escrevendo sua própria classe e armazenando todas as variáveis locais do gerador como variáveis de instância. Por exemplo, retornar uma lista de inteiros pode ser feito configurando self.count para 0, e tendo o iterator.__next__() incrementar o método self.count e devolvê-lo. No entanto, para um gerador moderadamente complicado, escrever uma classe correspondente pode ser muito mais complicado.
O conjunto de teste incluído na biblioteca do Python, lib/test/test_generators.py, contém vários exemplos mais interessantes. Aqui está um gerador que implementa uma passagem em ordem de uma árvore usando geradores de forma recursiva.
Dois outros exemplos no test_generators.py produzem soluções para o problema N-Queens (colocando N rainhas em um tabuleiro de xadrez NxN para que nenhuma rainha ameace a outra) e o Passeio do Cavalo (encontrando uma rota que leva um cavalo para cada quadrado de um tabuleiro de xadrez NxN sem visitar nenhum quadrado duas vezes).
Passando valores para um gerador¶
No Python 2.4 e anteriores, os geradores apenas produziram saída. Uma vez que o código de um gerador foi invocado para criar um iterador, não havia como passar qualquer nova informação na função quando sua execução foi retomada. Você pode cortar essa habilidade fazendo com que o gerador olhe para uma variável global ou passando em algum objeto mutável que os chamadores então modifiquem, mas essas abordagens são desordenadas.
No Python 2.5 há uma maneira simples de passar valores para um gerador. yield tornou-se uma expressão, retornando um valor que pode ser atribuído a uma variável ou operado de outra forma:
Eu recomendo que você sempre coloque parênteses em torno de uma expressão yield quando você está fazendo algo com o valor retornado, como no exemplo acima. Os parênteses nem sempre são necessários, mas é mais fácil adicioná-los sempre em vez de ter que lembrar quando são necessários.
(PEP 342 explica as regras exatas, é que uma expressão yield deve sempre ser entre parênteses, exceto quando ocorre na expressão de nível superior no lado direito de uma atribuição. Isso significa que você pode escrever val = yield i, mas tem que usar parênteses quando há uma operação, como em val = (yield i) + 12.)
Os valores são enviados para um gerador chamando seu método send(value). Este método retoma o código do gerador e a expressão yield retorna o valor especificado. Se o método regular __next__() for chamado, o yield retorna None.
Aqui está um contador simples que aumenta em 1 e permite alterar o valor do contador interno.
E aqui um exemplo de mudança de contador:
Porque yield retornará frequentemente None, você deve verificar sempre este caso. Não use apenas seu valor em expressões, a menos que tenha certeza de que o método send() será o único método usado para retomar a função do gerador.
É claro que você pode alcançar o mesmo efeito com uma compreensão de lista.
isso também pode ser escrito como uma compreensão de lista:
(For a more detailed discussion of sorting, see the HowTo - Ordenação.)
O módulo itertools¶
Funções que criam um novo iterador com base em um iterador existente.
Funções para tratar os elementos de um iterador como argumentos de funções.
Funções para selecionar partes da saída de um iterador.
Criando novos iteradores¶
Selecionando elementos¶
Agrupando elementos¶
O módulo functools¶
O módulo operator¶
Algumas funcionalidades desse módulo são:
Pequenas funções e as expressões lambda¶
Um dos motivos da minha preferência é que lambda é bastante limitado nas funções que pode definir. O resultado deve ser computável como uma única expressão, o que significa que você não pode ter comparações multi-canais if ... elif ... else ou try ... except. Se você tentar fazer muito em uma declaração lambda, você acabará com uma expressão excessivamente complicada que é difícil de ler. Rápido, o que o seguinte código está fazendo?
Você pode descobrir isso, mas leva tempo para desenredar a expressão para descobrir o que está acontecendo. Usar uma breve instrução de def aninhada torna as coisas um pouco melhor:
Mas seria o melhor de tudo se eu tivesse usado simplesmente um bucle for:
Ou o sum() embutida e uma expressão do gerador:
Muitas utilizações de functools.reduce() são mais claras quando escritas como loops for.
Fredrik Lundh sugeriu uma vez o seguinte conjunto de regras para refatoração de usos de lambda:
Escrever uma função lambda.
Escreva um comentário explicando o que o lambda faz.
Estude o comentário por um tempo e pense em um nome que capture a essência do comentário.
Converta a lambda para uma declaração de definição, usando esse nome.
Remover o comentário.
Eu realmente gosto dessas regras, mas você está livre para discordar sobre se esse estilo sem lambda é melhor.
Histórico de Revisão e Reconhecimentos¶
O autor agradece as seguintes pessoas por oferecer sugestões, correções e assistência com vários rascunhos deste artigo: Ian Bicking, Nick Coghlan, Nick Efford, Raymond Hettinger, Jim Jewett, Mike Krell, Leandro Lameiro, Jussi Salmela, Collin Winter, Blake Winton.
Versão 0.1: publicado em 30 de junho de 2006.
Versão 0.11: publicado em 1º de julho de 2006. Correções de erros de escrita.
Versão 0.2: publicado em 10 de julho de 2006. Incorporou as seções genexp e listcomp em uma. Correções de erros de escrita.
Versão 0.21: adicionou mais referências sugeridas na lista de correspondência do tutor.
Versão 0.30: Adiciona uma seção no módulo functional escrito por Collin Winter; Adiciona seção curta no módulo do operador; Algumas outras edições.
Estrutura e Interpretação de Programas de Computador, de Harold Abelson e Gerald Jay Sussman com Julie Sussman. Texto completo em https://mitpress.mit.edu/sicp/. Neste clássico livro de informática, os capítulos 2 e 3 discutem o uso de sequências e fluxos para organizar o fluxo de dados dentro de um programa. O livro usa o Scheme para seus exemplos, mas muitas das abordagens de design descritas nestes capítulos são aplicáveis ao código Python de estilo funcional.
http://www.defmacro.org/ramblings/fp.html: Uma introdução geral sobre programação funcional que utiliza exemplos Java e tem uma introdução histórica longa.
https://pt.wikipedia.org/wiki/Programação_funcional: Informação geral do Wikipédia para descrever programação funcional.
https://pt.wikipedia.org/wiki/Corrotina: Entrada para corrotinas.
https://pt.wikipedia.org/wiki/Currying: Entrada para o conceito de currying.
http://gnosis.cx/TPiP/: O primeiro capítulo do livro de David Mertz Text Processing in Python onde discute programação funcional para processamento de texto, em uma secção intitulada "Utilizing Higher-Order Functions in Text Processing".