Como tornar as funções do Python ainda melhores

Na verdade, o título deste maravilhoso artigo de Jeff Knapp, autor do livro " Writing Idiomatic Python ", reflete completamente sua essência. Leia com atenção e fique à vontade para comentar.

Como realmente não queríamos deixar o termo importante em letras latinas no texto, nos permitimos traduzir a palavra "docstring" como "docstring", tendo descoberto esse termo em várias fontes em língua russa .

No Python, como na maioria das linguagens de programação modernas, a função é o principal método de abstração e encapsulamento. Você, como desenvolvedor, provavelmente já escreveu centenas de funções. Mas funções para funções - discórdia. Além disso, se você escrever funções "ruins", isso afetará imediatamente a legibilidade e o suporte do seu código. Então, o que é uma função "ruim" e, mais importante - como torná-la uma função "boa"?

Atualize o tópico


A matemática está repleta de funções, no entanto, é difícil lembrá-las. Então, voltemos à nossa disciplina favorita: análise. Você provavelmente já viu fórmulas como f(x) = 2x + 3 . Essa é uma função chamada f que recebe um argumento x e depois "retorna" duas vezes x + 3 . Embora não seja muito semelhante às funções com as quais estamos acostumados no Python, é completamente semelhante ao seguinte código:

 def f(x): return 2*x + 3 

As funções existem há muito tempo na matemática, mas na ciência da computação elas são completamente transformadas. No entanto, esse poder não é dado em vão: você precisa passar por várias armadilhas. Vamos discutir o que deve ser uma função "boa" e o que "sinos e assobios" são típicos para funções que podem exigir refatoração.

Segredos da boa função


O que distingue uma função “boa” do Python de uma função medíocre? Você ficará surpreso com quantas interpretações a palavra "bom" permite. Como parte deste artigo, considerarei a função Python "boa" se ela satisfizer a maioria dos itens da lista a seguir (às vezes não é possível concluir todos os itens para uma função específica):

  • É claramente nomeado
  • Em conformidade com o princípio do dever único
  • Contém Dock
  • Retorna valor
  • Consiste em não mais que 50 linhas
  • Ela é idempotente e, se possível, pura

Para muitos de vocês, esses requisitos podem parecer excessivamente rígidos. No entanto, prometo: se suas funções cumprirem essas regras, elas ficarão tão bonitas que até perfuram um unicórnio com uma lágrima. Abaixo, dedicarei uma seção a cada um dos elementos da lista acima e depois completarei a história contando como eles se harmonizam e ajudam a criar boas funções.

Nomeação

Aqui está minha citação favorita sobre esse assunto, muitas vezes atribuída erroneamente a Donald, mas na verdade de propriedade de Phil Carleton :
Existem dois desafios para a ciência da computação: invalidação e nomeação de cache.
Não importa o quão bobo pareça, nomear é realmente uma coisa complicada. Aqui está um exemplo de um nome de função "ruim":

 def get_knn_from_df(df): 

Agora, nomes ruins me aparecem quase em toda parte, mas esse exemplo é retirado do campo da Ciência de Dados (mais precisamente, aprendizado de máquina), onde os profissionais geralmente escrevem código em um caderno Jupyter e tentam montar um programa digerível a partir dessas células.

O primeiro problema com o nome dessa função é que ela usa abreviações. É melhor usar palavras completas em inglês, em vez de abreviações e abreviações não conhecidas . A única razão pela qual desejo reduzir as palavras é não perder tempo digitando muito texto, mas qualquer editor moderno tem uma função de preenchimento automático , portanto, você deve digitar o nome completo da função apenas uma vez. A abreviação é um problema, porque geralmente é específica para uma área de assunto. No código acima, knn significa "vizinhos mais próximos de K" e df significa "DataFrame", uma estrutura de dados comumente usada na biblioteca de pandas . Se um programador que não conhece essas abreviações ler o código, ele não entenderá quase nada no nome da função.

Existem mais duas falhas menores no nome desta função. Em primeiro lugar, a palavra "get" redundante. Na maioria das funções nomeadas com competência, fica imediatamente claro que essa função retorna algo, o que é refletido especificamente no nome. O elemento from_d f também não é necessário. No encaixe da função ou (se estiver na periferia) na anotação de tipo, o tipo do parâmetro será descrito se essas informações ainda não forem óbvias no nome do parâmetro .

Então, como renomeamos esse recurso? Apenas:

 def k_nearest_neighbors(dataframe): 

Agora, mesmo um leigo entende o que está sendo calculado nessa função, e o nome do parâmetro (dataframe) não deixa dúvidas sobre qual argumento deve ser passado para ele.

Única responsabilidade


Desenvolvendo a idéia de Bob Martin, direi que o princípio da responsabilidade exclusiva se aplica a funções não menos que classes e módulos (sobre os quais o Sr. Martin escreveu originalmente). De acordo com esse princípio (no nosso caso), uma função deve ter uma única responsabilidade. Ou seja, ela deve fazer uma e apenas uma coisa. Uma das razões mais convincentes para isso: se uma função fizer apenas uma coisa, terá que ser reescrita no único caso: se essa mesma coisa tiver que ser feita de uma nova maneira. Também fica claro quando uma função pode ser excluída; se, fazendo alterações em outro lugar, entendemos que o único dever de uma função não é mais relevante, simplesmente nos livraremos dela.

É melhor dar um exemplo. Aqui está uma função que faz mais de uma "coisa":

 def calculate_and print_stats(list_of_numbers): sum = sum(list_of_numbers) mean = statistics.mean(list_of_numbers) median = statistics.median(list_of_numbers) mode = statistics.mode(list_of_numbers) print('-----------------Stats-----------------') print('SUM: {}'.format(sum) print('MEAN: {}'.format(mean) print('MEDIAN: {}'.format(median) print('MODE: {}'.format(mode) 

Ou seja, dois: calcula um conjunto de estatísticas em uma lista de números e as exibe em STDOUT . Uma função viola uma regra: deve haver uma única razão específica para que ela precise ser alterada. Nesse caso, há duas razões óbvias pelas quais isso é necessário: você precisa calcular estatísticas novas ou diferentes ou alterar o formato de saída. Portanto, é melhor reescrever essa função na forma de duas funções separadas: uma executará cálculos e retornará seus resultados, e a outra receberá esses resultados e os exibirá no console. Uma função (ou melhor, ela tem duas responsabilidades) com miúdos fornece a palavra e em seu nome .

Essa separação também simplifica bastante o teste da função e também permite dividi-la em duas funções no mesmo módulo, mas também separar essas duas funções em módulos completamente diferentes, se apropriado. Isso contribui ainda mais para testes mais limpos e simplifica o suporte ao código.

De fato, funções que executam exatamente duas coisas são raras. Com mais freqüência, você encontra funções que executam muito, muito mais operações. Novamente, por razões de legibilidade e testabilidade, essas funções de "múltiplas estações" devem ser divididas em tarefas únicas, cada uma das quais contém um único aspecto do trabalho.

Docstrings


Parece que todos estão cientes de que existe um documento PEP-8 que fornece recomendações sobre o estilo do código Python, mas há muito menos pessoas entre nós que conhecem o PEP-257 , nas quais as mesmas recomendações são dadas em relação às dockstrings. Para não recontar o conteúdo do PEP-257, estou enviando você para este documento - leia no seu tempo livre. No entanto, suas principais idéias são as seguintes:

  • Cada função precisa de uma sequência de documentos.
  • Deve observar gramática e pontuação; escreva frases completas
  • A doutrina começa com uma breve descrição (em uma frase) do que a função faz.
  • A doutrina é formulada em um estilo prescritivo e não descritivo

Todos esses pontos são fáceis de seguir ao escrever recursos. Apenas escrever docstrings deve se tornar um hábito e tentar escrevê-los antes de prosseguir com o código da própria função. Se você não conseguir escrever uma sequência de documentos clara que descreva a função, esse é um bom motivo para pensar por que você está escrevendo esta função.

Retornar valores


As funções podem (e devem ) ser interpretadas como pequenos programas independentes. Eles recebem alguma entrada na forma de parâmetros e retornam o resultado. Os parâmetros, é claro, são opcionais. Mas os valores de retorno são necessários do ponto de vista da estrutura interna do Python . Se você tentar escrever uma função que não retorne um valor, não poderá. Se a função nem retornar valores, o interpretador Python "forçará" a retornar None . Não acredita? Tente você mesmo:

 ❯ python3 Python 3.7.0 (default, Jul 23 2018, 20:22:55) [Clang 9.1.0 (clang-902.0.39.2)] on darwin Type "help", "copyright", "credits" or "license" for more information. >>> def add(a, b): ... print(a + b) ... >>> b = add(1, 2) 3 >>> b >>> b is None True 

Como você pode ver, o valor de b é essencialmente None . Portanto, mesmo se você escrever uma função sem uma declaração de retorno, ela ainda retornará algo. E deveria. Afinal, este é um programa pequeno, certo? Qual a utilidade dos programas dos quais não há conclusão - e, portanto, é impossível julgar se este programa foi executado corretamente? Mas o mais importante, como você vai testar esse programa?

Não tenho medo de dizer o seguinte: cada função deve retornar um valor útil, pelo menos por uma questão de testabilidade. O código que escrevo deve ser testado (isso não é discutido). Imagine como o teste desajeitado da função de add acima pode resultar (dica: você terá que redirecionar a entrada / saída, após o que tudo dará errado em breve). Além disso, retornando um valor, podemos encadear métodos e, portanto, escrever código como este:

 with open('foo.txt', 'r') as input_file: for line in input_file: if line.strip().lower().endswith('cat'): # ...     -  

String if line.strip().lower().endswith('cat'): funciona porque cada um dos métodos de string ( strip() , lower() , endswith() ) retorna uma string como resultado da chamada da função.

Aqui estão alguns motivos comuns que um programador pode fornecer ao explicar por que uma função que ele escreve não retorna um valor:
“É apenas [algum tipo de operação relacionada à entrada / saída, por exemplo, armazenando um valor em um banco de dados]. Aqui não posso devolver nada de útil.
Eu não concordo A função pode retornar True se a operação for concluída com êxito.
"Aqui, alteramos um dos parâmetros disponíveis, use-o como parâmetro de referência." ""
Aqui estão dois pontos. Primeiro, faça o seu melhor para não fazer isso. Em segundo lugar, fornecer uma função com algum tipo de argumento apenas para descobrir que ela mudou é surpreendente na melhor das hipóteses e simplesmente perigoso na pior. Em vez disso, como nos métodos de string, tente retornar uma nova instância do parâmetro que já reflete as alterações aplicadas a ele. Mesmo que isso não possa ser feito, já que a criação de uma cópia de algum parâmetro está repleta de custos excessivos, você ainda poderá reverter para a opção “Retornar True se a operação for concluída com êxito” proposta acima.
“Eu preciso retornar vários valores. Não existe um valor único que, neste caso, seria aconselhável retornar. ”
Este argumento é um pouco rebuscado, mas eu o ouvi. A resposta, é claro, é exatamente o que o autor queria fazer - mas não sabia como: use uma tupla para retornar vários valores .

Finalmente, o argumento mais forte de que é melhor retornar um valor útil em qualquer caso é que o chamador sempre pode ignorar esses valores de maneira justificável. Em resumo, retornar um valor de uma função é quase certamente uma boa idéia, e é altamente improvável que danifiquemos qualquer coisa dessa maneira, mesmo nas bases de código existentes.

Comprimento da função


Eu admiti mais de uma vez que sou muito burra. Eu posso manter cerca de três coisas na minha cabeça ao mesmo tempo. Se você me deixar ler a função de 200 linhas e perguntar o que ela faz, provavelmente a encararei por pelo menos 10 segundos. O comprimento de uma função afeta diretamente sua legibilidade e, portanto, seu suporte . Portanto, tente manter suas funções curtas. 50 linhas - um valor retirado completamente do teto, mas me parece razoável. (Espero) que a maioria das funções que você escreve seja muito menor.

Se uma função estiver em conformidade com o Princípio da Responsabilidade Única, é provável que seja breve o suficiente. Se estiver lendo ou for idempotente (falaremos sobre isso) abaixo - então, provavelmente, também será breve. Todas essas idéias são harmoniosamente combinadas entre si e ajudam a escrever um código bom e limpo.

Então, o que fazer se sua função for muito longa? REFATOR! Você provavelmente precisa refatorar o tempo todo, mesmo que não conheça o termo. A refatoração é simplesmente alterar a estrutura de um programa, sem alterar seu comportamento. Portanto, extrair várias linhas de código de uma função longa e transformá-las em uma função independente é um dos tipos de refatoração. Acontece que essa também é a maneira mais comum e rápida de reduzir produtivamente funções longas. Como você está dando nomes adequados a essas novas funções, o código resultante é muito mais fácil de ler. Eu escrevi um livro inteiro sobre refatoração (na verdade, eu faço isso o tempo todo), então não vou entrar em detalhes aqui. Apenas saiba que se você tem uma função muito longa, deve refatorá-la.

Idempotência e limpeza funcional


O título desta seção pode parecer um pouco intimidador, mas conceitualmente a seção é simples. Uma função idempotente com o mesmo conjunto de argumentos sempre retorna o mesmo valor, independentemente de quantas vezes é chamada. O resultado não depende de variáveis ​​não locais, variabilidade de argumentos ou dados provenientes de fluxos de entrada / saída. A seguinte função add_three(number) é idempotente:

 def add_three(number): """ ** + 3.""" return number + 3 

Independentemente de quantas vezes chamamos add_three(7) , a resposta será sempre 10. Mas outro caso é uma função que não é idempotente:

 def add_three(): """ 3 + ,  .""" number = int(input('Enter a number: ')) return number + 3 

Essa função inventada francamente não é idempotente, pois o valor de retorno da função depende da entrada / saída, ou seja, do número digitado pelo usuário. Obviamente, com chamadas diferentes para add_three() valores retornados serão diferentes. Se chamarmos essa função duas vezes, o usuário no primeiro caso poderá digitar 3, e no segundo - 7, e duas chamadas para add_three() retornarão 6 e 10, respectivamente.

Fora da programação, também existem exemplos de idempotência - por exemplo, o botão para cima do elevador é projetado de acordo com este princípio. Ao pressioná-lo pela primeira vez, "notificamos" o elevador que queremos subir. Como o botão é idempotente, não importa o quanto você o pressione mais tarde, nada de ruim acontecerá. O resultado será sempre o mesmo.

Por que a idempotência é tão importante


Suporte para testabilidade e usabilidade. As funções idempotentes são fáceis de testar, pois elas garantem o mesmo resultado em qualquer caso, se você as chamar com os mesmos argumentos. O teste se resume a verificar se, com uma variedade de chamadas, a função sempre retorna o valor esperado. Além disso, esses testes serão rápidos: a velocidade do teste é uma questão importante que geralmente é ignorada nos testes de unidade. E refatorar ao trabalhar com funções idempotentes geralmente é fácil. Não importa como você altera o código fora da função - o resultado de chamá-lo com os mesmos argumentos sempre será o mesmo.

O que é uma função "pura"?


Na programação funcional, uma função é considerada pura se, em primeiro lugar , é idempotente e, em segundo lugar , não causa os efeitos colaterais observados. Não se esqueça: uma função é idempotente se sempre retornar o mesmo resultado com um conjunto específico de argumentos. No entanto, isso não significa que a função não possa afetar outros componentes - por exemplo, variáveis ​​não locais ou fluxos de entrada / saída. Por exemplo, se a versão idempotente da função add_three(number) acima add_three(number) o resultado para o console e depois devolvê-lo, ele ainda será considerado idempotente, porque quando acessar o fluxo de entrada / saída, essa operação de acesso não afetará o valor retornado da função. A chamada print() é apenas um efeito colateral : interação com o restante do programa ou sistema como tal, ocorrendo junto com o valor de retorno.

Vamos desenvolver um pouco o nosso exemplo com add_three(number) . Você pode escrever o código a seguir para determinar quantas vezes o add_three(number) foi chamado:

 add_three_calls = 0 def add_three(number): """ ** + 3.""" global add_three_calls print(f'Returning {number + 3}') add_three_calls += 1 return number + 3 def num_calls(): """,     *add_three*.""" return add_three_calls 

Agora, executamos a saída no console (esse é um efeito colateral) e alteramos a variável não local (outro efeito colateral), mas, como nada disso afeta o valor retornado pela função, é idempotente de qualquer maneira.

A função pura não tem efeitos colaterais. Ele não utiliza apenas "dados externos" ao calcular o valor, mas não interage com o restante do programa / sistema, apenas calcula e retorna o valor especificado. Portanto, embora nossa nova definição de add_three(number) permaneça idempotente, essa função não é mais pura.

Nas funções puras, não há instruções de registro nem chamadas de print() . Ao trabalhar, eles não acessam o banco de dados e não usam conexões com a Internet. Não acesse ou modifique variáveis ​​não locais. E não chame outras funções não puras .

Em resumo, eles não têm "ações terríveis de longo alcance", como expressam as palavras de Einstein (mas no contexto da ciência da computação, não da física). Eles não alteram de forma alguma o restante do programa ou sistema. Na programação imperativa (que é o que você faz ao escrever código em Python), essas funções são as mais seguras. Eles são conhecidos por sua testabilidade e facilidade de suporte; além disso, por serem idempotentes, é garantido que testar essas funções seja tão rápido quanto a execução. Os testes também são simples: você não precisa se conectar ao banco de dados ou simular recursos externos, preparar a configuração inicial do código e, no final do trabalho, não precisa limpar nada.

Honestamente, idempotência e limpeza são muito desejáveis, mas não necessárias. , , , . , , , , . , , .

Conclusão


Isso é tudo. , – . . , . – ! . , , , « ». .

Source: https://habr.com/ru/post/pt426381/


All Articles