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:
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
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.
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:
'I'm a string'
e o inteiro 42
ou um dicionáriodatetime.datetime
objetoOs dois últimos são os mais surpreendentes, especialmente se você vem de outras línguas, e precisam de mais discussão.
A instrução de função em Python faz duas coisas:
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)
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.
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 .
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.
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):
Foo
. Este é o nome que você receberá de volta se fizer Foo.__name__
.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:
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)
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.
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:
__name__
atributo.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
cls
para evitar o conflito com a palavra-chave class
(você também verá class_
e klass
usados para essa finalidade).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 .
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 )
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:
À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:
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.
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.
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.
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.