portaldacalheta.pt
  • Principal
  • Receita E Crescimento
  • Aprendendo
  • Processo Interno
  • Processo De Design
Tecnologia

Garantia de código limpo: um olhar sobre o Python, parametrizado



Neste post, vou falar sobre o que considero ser a técnica ou padrão mais importante na produção de código Pythônico limpo - ou seja, parametrização. Esta postagem é para você se:

  • Você é relativamente novo em toda a coisa de padrões de design e talvez um pouco confuso com as longas listas de nomes de padrões e diagramas de classes. A boa notícia é que realmente existe apenas um padrão de design que você absolutamente deve conhecer para Python. Melhor ainda, você provavelmente já sabe, mas talvez não de todas as maneiras como pode ser aplicado.
  • Você veio para Python de outra linguagem OOP, como Java ou C #, e quer saber como traduzir seu conhecimento de padrões de projeto dessa linguagem para Python. Em Python e em outras linguagens tipadas dinamicamente, muitos padrões comuns em linguagens OOP tipadas estaticamente são “invisíveis ou mais simples”, como disse o autor Peter Norvig.

Neste artigo, exploraremos a aplicação de 'parametrização' e como ela pode se relacionar com os padrões de design convencionais conhecidos como Injeção de dependência , estratégia , método de modelo , fábrica abstrata , método de fábrica e decorador . Em Python, muitos deles acabam se tornando simples ou desnecessários pelo fato de que os parâmetros em Python podem ser objetos ou classes que podem ser chamados.



Parametrização é o processo de tomar valores ou objetos definidos dentro de uma função ou método, e torná-los parâmetros para aquela função ou método, a fim de generalizar o código. Este processo também é conhecido como refatoração do “parâmetro de extração”. De certa forma, este artigo é sobre padrões de design e refatoração.



causa raiz da crise da dívida da Grécia

O caso mais simples de Python parametrizado

Para a maioria dos nossos exemplos, usaremos a biblioteca padrão instrucional tartaruga módulo para fazer alguns gráficos.



Aqui está um código que desenhará um quadrado de 100 x 100 usando turtle:

from turtle import Turtle turtle = Turtle() for i in range(0, 4): turtle.forward(100) turtle.left(90)

Suponha que agora desejemos desenhar um quadrado de tamanho diferente. Um programador iniciante neste ponto ficaria tentado a copiar e colar este bloco e modificá-lo. Obviamente, um método muito melhor seria primeiro extrair o código do desenho do quadrado para uma função e, em seguida, tornar o tamanho do quadrado um parâmetro para esta função:



def draw_square(size): for i in range(0, 4): turtle.forward(size) turtle.left(90) draw_square(100)

Portanto, agora podemos desenhar quadrados de qualquer tamanho usando draw_square. Isso é tudo que há para a técnica essencial de parametrização, e acabamos de ver o primeiro uso principal - eliminar a programação copiar e colar.

Um problema imediato com o código acima é que draw_square depende de uma variável global. Este tem muitas consequências ruins , e há duas maneiras fáceis de corrigi-lo. O primeiro seria para draw_square para criar o Turtle instância em si (que discutirei mais tarde). Isso pode não ser desejável se quisermos usar um único Turtle para todos os nossos desenhos. Então, por enquanto, vamos simplesmente usar a parametrização novamente para fazer turtle um parâmetro para draw_square:



from turtle import Turtle def draw_square(turtle, size): for i in range(0, 4): turtle.forward(size) turtle.left(90) turtle = Turtle() draw_square(turtle, 100)

Isso tem um nome sofisticado - injeção de dependência. Significa apenas que se uma função precisa de algum tipo de objeto para fazer seu trabalho, como draw_square precisa de um Turtle, o chamador é responsável por passar esse objeto como um parâmetro. Não, realmente, se você já teve curiosidade sobre a injeção de dependência do Python, é isso.

Até agora, lidamos com dois usos muito básicos. A observação principal para o restante deste artigo é que, em Python, há uma grande variedade de coisas que podem se tornar parâmetros - mais do que em algumas outras linguagens - e isso a torna uma técnica muito poderosa.



Qualquer coisa que seja um objeto

Em Python, você pode usar essa técnica para parametrizar qualquer coisa que seja um objeto, e em Python, a maioria das coisas que você encontra são, na verdade, objetos. Isso inclui:

  • Instâncias de tipos integrados, como a string 'I'm a string' e o inteiro 42 ou um dicionário
  • Instâncias de outros tipos e classes, por exemplo, a datetime.datetime objeto
  • Funções e métodos
  • Tipos integrados e classes personalizadas

Os dois últimos são os mais surpreendentes, especialmente se você vem de outras línguas, e precisam de mais discussão.



Funções como parâmetros

A instrução de função em Python faz duas coisas:

  1. Ele cria um objeto de função.
  2. Ele cria um nome no escopo local que aponta para aquele objeto.

Podemos brincar com esses objetos em um REPL:



> >> def foo(): ... return 'Hello from foo' > >> > >> foo() 'Hello from foo' > >> print(foo) > >> type(foo) > >> foo.name 'foo'

E, assim como todos os objetos, podemos atribuir funções a outras variáveis:

> >> bar = foo > >> bar() 'Hello from foo'

Observe que bar é outro nome para o mesmo objeto, portanto, tem o mesmo __name__ interno | propriedade como antes:

> >> bar.name 'foo' > >> bar

Mas o ponto crucial é que, como as funções são apenas objetos, em qualquer lugar que você vir uma função sendo usada, ela pode ser um parâmetro.

Então, suponha que estendamos nossa função de desenho de quadrados acima, e agora às vezes, quando desenhamos quadrados, queremos fazer uma pausa em cada canto - uma chamada para time.sleep().

Mas suponha que às vezes não queremos fazer uma pausa. A maneira mais simples de fazer isso seria adicionar um pause parâmetro, talvez com um padrão de zero para que, por padrão, não pausemos.

No entanto, mais tarde descobrimos que às vezes realmente queremos fazer algo completamente diferente nos cantos. Talvez queiramos desenhar outra forma em cada canto, mudar a cor da caneta, etc. Podemos ficar tentados a adicionar muitos outros parâmetros, um para cada coisa que precisamos fazer. No entanto, uma solução muito mais agradável seria permitir que qualquer função seja passada como a ação a ser executada. Por padrão, faremos uma função que não faz nada. Também faremos com que essa função aceite o local turtle e size parâmetros, caso sejam necessários:

def do_nothing(turtle, size): pass def draw_square(turtle, size, at_corner=do_nothing): for i in range(0, 4): turtle.forward(size) at_corner(turtle, size) turtle.left(90) def pause(turtle, size): time.sleep(5) turtle = Turtle() draw_square(turtle, 100, at_corner=pause)

Ou podemos fazer algo um pouco mais legal, como desenhar recursivamente quadrados menores em cada canto:

def smaller_square(turtle, size): if size <10: return draw_square(turtle, size / 2, at_corner=smaller_square) draw_square(turtle, 128, at_corner=smaller_square)

Ilustração de quadrados menores desenhados recursivamente, conforme demonstrado no código parametrizado em Python acima

Existem, é claro, variações disso. Em muitos exemplos, o valor de retorno da função seria usado. Aqui, temos um estilo de programação mais imperativo, e a função é chamada apenas por seus efeitos colaterais.

Em outros idiomas ...

Ter funções de primeira classe em Python torna isso muito fácil. Em linguagens sem eles, ou em algumas linguagens de tipo estático que requerem assinaturas de tipo para parâmetros, isso pode ser mais difícil. Como faríamos isso se não tivéssemos funções de primeira classe?

Uma solução seria transformar draw_square em uma classe, SquareDrawer:

class SquareDrawer: def __init__(self, size): self.size = size def draw(self, t): for i in range(0, 4): t.forward(self.size) self.at_corner(t, size) t.left(90) def at_corner(self, t, size): pass

Agora podemos criar uma subclasse SquareDrawer e adicione um at_corner método que faz o que precisamos. Este padrão python é conhecido como o modelo de método padrão - uma classe base define a forma de toda a operação ou algoritmo e as partes variantes da operação são colocadas em métodos que precisam ser implementados por subclasses.

Embora isso às vezes possa ser útil em Python, extrair o código variante em uma função que é simplesmente passada como um parâmetro costuma ser muito mais simples.

Uma segunda maneira de abordar esse problema em linguagens sem funções de primeira classe é agrupar nossas funções como métodos dentro de classes, como este:

class DoNothing: def run(self, turtle, size): pass def draw_square(turtle, size, at_corner=DoNothing()): for i in range(0, 4): turtle.forward(size) at_corner.run(turtle, size) t.left(90) class Pauser: def run(self, turtle, size): time.sleep(5) draw_square(turtle, 100, at_corner=Pauser())

Isso é conhecido como padrão de estratégia . Novamente, este é certamente um padrão válido para usar em Python, especialmente se a classe de estratégia realmente contém um conjunto de funções relacionadas, em vez de apenas uma. No entanto, muitas vezes tudo o que realmente precisamos é uma função e podemos pare de escrever aulas .

Outros Callables

Nos exemplos acima, eu falei sobre passar funções para outras funções como parâmetros. No entanto, tudo o que escrevi era, de fato, verdadeiro para qualquer objeto chamável. Funções são o exemplo mais simples, mas também podemos considerar métodos.

Suponha que temos uma lista foo:

foo = [1, 2, 3]

foo agora tem um monte de métodos anexados a ele, como .append() e .count(). Esses 'métodos vinculados' podem ser transmitidos e usados ​​como funções:

> >> appendtofoo = foo.append > >> appendtofoo(4) > >> foo [1, 2, 3, 4]

Além desses métodos de instância, existem outros tipos de objetos que podem ser chamados - staticmethods e classmethods , instâncias de classes que implementam __call__ e as próprias classes / tipos.

Classes como parâmetros

No Python, as classes são de “primeira classe” - são objetos de tempo de execução, como dicts, strings, etc. Isso pode parecer ainda mais estranho do que funções sendo objetos, mas felizmente, é realmente mais fácil demonstrar esse fato do que para funções.

A instrução de classe com a qual você está familiarizado é uma boa maneira de criar classes, mas não é a única maneira - também podemos usar o versão de três argumentos do tipo . As duas instruções a seguir fazem exatamente a mesma coisa:

class Foo: pass Foo = type('Foo', (), {})

Na segunda versão, observe as duas coisas que acabamos de fazer (que são feitas de forma mais conveniente usando a instrução de classe):

  1. No lado direito do sinal de igual, criamos uma nova classe, com um nome interno de Foo. Este é o nome que você receberá de volta se fizer Foo.__name__.
  2. Com a atribuição, criamos um nome no escopo atual, Foo, que se refere ao objeto de classe que acabamos de criar.

Fizemos as mesmas observações para o que a declaração de função faz.

O principal insight aqui é que as classes são objetos que podem receber nomes (ou seja, podem ser colocados em uma variável). Em qualquer lugar que você vê uma classe em uso, na verdade está apenas vendo uma variável em uso. E se for uma variável, pode ser um parâmetro.

Podemos dividir isso em uma série de usos:

Classes como fábricas

Uma classe é um objeto que pode ser chamado que cria uma instância de si mesmo:

> >> class Foo: ... pass > >> Foo()

E como um objeto, ele pode ser atribuído a outras variáveis:

> >> myclass = Foo > >> myclass()

Voltando ao nosso exemplo de tartaruga acima, um problema com o uso de tartarugas para desenhar é que a posição e orientação do desenho dependem da posição e orientação atuais da tartaruga, e também pode deixá-la em um estado diferente, o que pode ser inútil para o chamador. Para resolver isso, nosso draw_square função poderia criar sua própria tartaruga, movê-la para a posição desejada e, em seguida, desenhar um quadrado:

def draw_square(x, y, size): turtle = Turtle() turtle.penup() # Don't draw while moving to the start position turtle.goto(x, y) turtle.pendown() for i in range(0, 4): turtle.forward(size) turtle.left(90)

No entanto, agora temos um problema de personalização. Suponha que o autor da chamada deseje definir alguns atributos da tartaruga ou usar um tipo diferente de tartaruga com a mesma interface, mas com algum comportamento especial.

Poderíamos resolver isso com injeção de dependência, como fizemos antes - o chamador seria responsável por configurar o Turtle objeto. Mas e se nossa função às vezes precisar fazer muitas tartarugas para diferentes propósitos de desenho, ou se talvez desejar lançar quatro fios, cada um com sua própria tartaruga para desenhar um lado do quadrado? A resposta é simplesmente tornar a classe Turtle um parâmetro para a função. Podemos usar um argumento de palavra-chave com um valor padrão, para manter as coisas simples para chamadores que não se importam:

def draw_square(x, y, size, make_turtle=Turtle): turtle = make_turtle() turtle.penup() turtle.goto(x, y) turtle.pendown() for i in range(0, 4): turtle.forward(size) turtle.left(90)

Para usar isso, poderíamos escrever um make_turtle função que cria uma tartaruga e a modifica. Suponha que queremos ocultar a tartaruga ao desenhar quadrados:

def make_hidden_turtle(): turtle = Turtle() turtle.hideturtle() return turtle draw_square(5, 10, 20, make_turtle=make_hidden_turtle)

Ou podemos criar uma subclasse Turtle para tornar esse comportamento integrado e passar a subclasse como o parâmetro:

class HiddenTurtle(Turtle): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) self.hideturtle() draw_square(5, 10, 20, make_turtle=HiddenTurtle)

Em outros idiomas ...

Várias outras linguagens OOP, como Java e C #, não possuem classes de primeira classe. Para instanciar uma classe, você deve usar o new palavra-chave seguida por um nome de classe real.

Essa limitação é a razão de padrões como fábrica abstrata (que requer a criação de um conjunto de classes cujo único trabalho é instanciar outras classes) e o Padrão de método de fábrica . Como você pode ver, em Python, é apenas uma questão de retirar a classe como um parâmetro porque uma classe é sua própria fábrica.

Classes como classes base

Suponha que estamos criando subclasses para adicionar o mesmo recurso a classes diferentes. Por exemplo, queremos um Turtle subclasse que gravará em um log quando ele for criado:

import logging logger = logging.getLogger() class LoggingTurtle(Turtle): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) logger.debug('Turtle got created')

Mas então, nos encontramos fazendo exatamente a mesma coisa com outra classe:

class LoggingHippo(Hippo): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) logger.debug('Hippo got created')

As únicas coisas que variam entre esses dois são:

  1. A classe base
  2. O nome da subclasse - mas não nos importamos muito com isso e poderíamos gerá-lo automaticamente a partir da classe base __name__ atributo.
  3. O nome usado dentro de debug chamar - mas, novamente, podemos gerar isso a partir do nome da classe base.

Diante de dois bits de código muito semelhantes com apenas uma variante, o que podemos fazer? Assim como em nosso primeiro exemplo, criamos uma função e retiramos a parte variante como um parâmetro:

def make_logging_class(cls): class LoggingThing(cls): def __init__(self, *args, **kwargs): super().__init__(*args, **kwargs) logger.debug('{0} got created'.format(cls.__name__)) LoggingThing.__name__ = 'Logging{0}'.format(cls.__name__) return LoggingThing LoggingTurtle = make_logging_class(Turtle) LoggingHippo = make_logging_class(Hippo)

Aqui, temos uma demonstração das aulas de primeira classe:

c ++ usando arquivos de cabeçalho
  • Passamos uma classe para uma função, dando ao parâmetro um nome convencional cls para evitar o conflito com a palavra-chave class (você também verá class_ e klass usados ​​para essa finalidade).
  • Dentro da função, criamos uma classe - observe que cada chamada para esta função cria um Novo classe.
  • Retornamos essa classe como o valor de retorno da função.

Também definimos LoggingThing.__name__ que é totalmente opcional, mas pode ajudar na depuração.

Outra aplicação dessa técnica é quando temos um monte de recursos que às vezes queremos adicionar a uma classe e podemos querer adicionar várias combinações desses recursos. Criar manualmente todas as combinações diferentes de que precisamos pode ser muito difícil de manejar.

Em linguagens onde as classes são criadas em tempo de compilação, em vez de tempo de execução, isso não é possível. Em vez disso, você deve usar o padrão de decorador . Esse padrão pode ser útil às vezes em Python, mas principalmente você pode apenas usar a técnica acima.

Normalmente, eu evito criar muitas subclasses para personalizar. Normalmente, existem métodos mais simples e Pythônicos que não envolvem classes. Mas essa técnica está disponível se você precisar. Veja também Tratamento completo de Brandon Rhodes do padrão decorador em Python .

Classes como exceções

Outro lugar onde você vê as classes sendo usadas é no except cláusula de uma instrução try / except / finally. Nenhuma surpresa em adivinhar que podemos parametrizar essas classes também.

Por exemplo, o código a seguir implementa uma estratégia muito genérica de tentar uma ação que pode falhar e tentar novamente com recuo exponencial até que um número máximo de tentativas seja atingido:

import time def retry_with_backoff(action, exceptions_to_catch, max_attempts=10, attempts_so_far=0): try: return action() except exceptions_to_catch: attempts_so_far += 1 if attempts_so_far >= max_attempts: raise else: time.sleep(attempts_so_far ** 2) return retry_with_backoff(action, exceptions_to_catch, attempts_so_far=attempts_so_far, max_attempts=max_attempts)

Retiramos a ação a ser executada e as exceções a serem capturadas como parâmetros. O parâmetro exceptions_to_catch pode ser uma única classe, como IOError ou httplib.client.HTTPConnectionError, ou uma tupla dessas classes. (Queremos evitar cláusulas 'nuas exceto' ou mesmo except Exception porque isto é conhecido por esconder outros erros de programação )

Avisos e Conclusão

A parametrização é uma técnica poderosa para reutilizar código e reduzir a duplicação de código. Tem alguns inconvenientes. Na busca pela reutilização de código, vários problemas costumam surgir:

  • Código excessivamente genérico ou abstrato que se torna muito difícil de entender.
  • Código com um proliferação de parâmetros que obscurece o quadro geral ou introduz bugs porque, na realidade, apenas certas combinações de parâmetros são testadas adequadamente.
  • Acoplamento inútil de diferentes partes da base de código porque seu “código comum” foi fatorado em um único lugar. Às vezes, o código em dois lugares é semelhante apenas acidentalmente, e os dois lugares devem ser independentes um do outro porque eles podem precisar mudar independentemente .

Às vezes, um pouco de código “duplicado” é muito melhor do que esses problemas, portanto, use essa técnica com cuidado.

Nesta postagem, cobrimos os padrões de design conhecidos como Injeção de dependência , estratégia , método de modelo , fábrica abstrata , método de fábrica e decorador . Em Python, muitos deles realmente acabam sendo uma aplicação simples de parametrização ou são definitivamente desnecessários pelo fato de que os parâmetros em Python podem ser objetos ou classes que podem ser chamados. Esperançosamente, isso ajuda a aliviar a carga conceitual de 'coisas que você deveria saber como uma realidade Desenvolvedor Python ”E permite que você escreva um código Pythônico conciso!

Leitura adicional:

  • Padrões de design Python: para código elegante e moderno
  • Padrões Python : Para Python Design Patterns
  • Registro Python: um tutorial detalhado

Compreender o básico

O que é uma função parametrizada?

Em uma função parametrizada, um ou mais detalhes do que a função faz são definidos como parâmetros em vez de serem definidos na função; eles devem ser passados ​​pelo código de chamada.

Qual é o propósito de refatorar código?

Ao refatorar o código, você altera a forma dele para que possa ser reutilizado ou modificado com mais facilidade - por exemplo, para corrigir bugs ou adicionar recursos.

Posso passar uma função como parâmetro em Python?

Sim - funções são objetos em Python e, como todos os objetos, podem ser passados ​​como argumentos em funções e métodos. Nenhuma sintaxe especial é necessária.

O que são parâmetros e argumentos em Python?

Os parâmetros são as variáveis ​​que aparecem entre os colchetes na linha 'def' de uma definição de função Python. Os argumentos são os objetos ou valores reais que você passa para uma função ou método ao chamá-lo. Esses termos são freqüentemente usados ​​de forma intercambiável.

Como desenvolver cultura em equipes remotas

Equipes Distribuídas

Como desenvolver cultura em equipes remotas
Dicas e práticas recomendadas de portfólio de UX

Dicas e práticas recomendadas de portfólio de UX

Design Ux

Publicações Populares
Linha de assunto - Como abordar o design de e-mail
Linha de assunto - Como abordar o design de e-mail
Gerentes de produto x Gerentes de projeto: entendendo as semelhanças e diferenças essenciais
Gerentes de produto x Gerentes de projeto: entendendo as semelhanças e diferenças essenciais
Tutorial de física de videogame - Parte II: Detecção de colisão para objetos sólidos
Tutorial de física de videogame - Parte II: Detecção de colisão para objetos sólidos
Estética e percepção - como abordar as imagens da experiência do usuário
Estética e percepção - como abordar as imagens da experiência do usuário
Escreva testes que importem: enfrente o código mais complexo primeiro
Escreva testes que importem: enfrente o código mais complexo primeiro
 
Como Facilitar a Mudança por meio da Liderança de Servo Ágil
Como Facilitar a Mudança por meio da Liderança de Servo Ágil
O lendário alpinista Jim Bridwell morre aos 73 anos
O lendário alpinista Jim Bridwell morre aos 73 anos
Tutorial Grape Gem: como construir uma API semelhante a REST em Ruby
Tutorial Grape Gem: como construir uma API semelhante a REST em Ruby
Energia híbrida: vantagens e benefícios da vibração
Energia híbrida: vantagens e benefícios da vibração
Dê a eles incentivos - como aproveitar uma estrutura de design de programa de fidelidade
Dê a eles incentivos - como aproveitar uma estrutura de design de programa de fidelidade
Publicações Populares
  • as linguagens de programação orientadas a objetos não são mais usadas por desenvolvedores de software.
  • Internet das coisas questões de segurança
  • o que é uma linguagem declarativa
  • propósito de um quadro de humor
  • leis gestálticas da percepção visual
Categorias
  • Receita E Crescimento
  • Aprendendo
  • Processo Interno
  • Processo De Design
  • © 2022 | Todos Os Direitos Reservados

    portaldacalheta.pt