Padrões de design Python: para código elegante e moderno
Publicados: 2022-03-11Vamos dizer novamente: Python é uma linguagem de programação de alto nível com tipagem dinâmica e vinculação dinâmica. Eu a descreveria como uma linguagem dinâmica poderosa e de alto nível. Muitos desenvolvedores estão apaixonados pelo Python por causa de sua sintaxe clara, módulos e pacotes bem estruturados e por sua enorme flexibilidade e variedade de recursos modernos.
Em Python, nada obriga você a escrever classes e instanciar objetos delas. Se você não precisa de estruturas complexas em seu projeto, você pode simplesmente escrever funções. Melhor ainda, você pode escrever um script simples para executar alguma tarefa simples e rápida sem estruturar o código.
Ao mesmo tempo, Python é uma linguagem 100% orientada a objetos. Como é isso? Bem, de forma simples, tudo em Python é um objeto. Funções são objetos, objetos de primeira classe (o que quer que isso signifique). Este fato sobre funções serem objetos é importante, então lembre-se disso.
Assim, você pode escrever scripts simples em Python ou simplesmente abrir o terminal Python e executar instruções ali mesmo (isso é muito útil!). Mas, ao mesmo tempo, você pode criar estruturas complexas, aplicativos, bibliotecas e assim por diante. Você pode fazer muito em Python. É claro que existem várias limitações, mas esse não é o tópico deste artigo.
No entanto, como o Python é tão poderoso e flexível, precisamos de algumas regras (ou padrões) ao programar nele. Então, vamos ver o que são padrões e como eles se relacionam com o Python. Também continuaremos a implementar alguns padrões de design essenciais do Python.
Por que o Python é bom para padrões?
Qualquer linguagem de programação é boa para padrões. Na verdade, os padrões devem ser considerados no contexto de qualquer linguagem de programação. Tanto os padrões, a sintaxe da linguagem e a natureza impõem limitações à nossa programação. As limitações que vêm da sintaxe da linguagem e da natureza da linguagem (dinâmica, funcional, orientada a objetos e similares) podem diferir, assim como as razões por trás de sua existência. As limitações provenientes de padrões existem por uma razão, são propositais. Esse é o objetivo básico dos padrões; para nos dizer como fazer algo e como não fazê-lo. Falaremos sobre padrões, e especialmente sobre padrões de projeto Python, mais tarde.
A filosofia do Python é construída sobre a ideia de boas práticas bem pensadas. Python é uma linguagem dinâmica (eu já disse isso?) e, como tal, já implementa, ou facilita a implementação, uma série de padrões de design populares com poucas linhas de código. Alguns padrões de design são incorporados ao Python, então os usamos mesmo sem saber. Outros padrões não são necessários devido à natureza da linguagem.
Por exemplo, Factory é um padrão de design estrutural do Python destinado a criar novos objetos, ocultando a lógica de instanciação do usuário. Mas a criação de objetos em Python é dinâmica por design, portanto, adições como Factory não são necessárias. Claro, você é livre para implementá-lo se quiser. Pode haver casos em que seria realmente útil, mas eles são uma exceção, não a norma.
O que há de tão bom na filosofia do Python? Vamos começar com isso (explore-o no terminal Python):
> >> import this The Zen of Python, by Tim Peters Beautiful is better than ugly. Explicit is better than implicit. Simple is better than complex. Complex is better than complicated. Flat is better than nested. Sparse is better than dense. Readability counts. Special cases aren't special enough to break the rules. Although practicality beats purity. Errors should never pass silently. Unless explicitly silenced. In the face of ambiguity, refuse the temptation to guess. There should be one-- and preferably only one --obvious way to do it. Although that way may not be obvious at first unless you're Dutch. Now is better than never. Although never is often better than *right* now. If the implementation is hard to explain, it's a bad idea. If the implementation is easy to explain, it may be a good idea. Namespaces are one honking great idea -- let's do more of those!
Esses podem não ser padrões no sentido tradicional, mas são regras que definem a abordagem “Pythonic” da programação da maneira mais elegante e útil.
Também temos diretrizes de código PEP-8 que ajudam a estruturar nosso código. É uma obrigação para mim, com algumas exceções apropriadas, é claro. Aliás, essas exceções são incentivadas pelo próprio PEP-8:
Mas o mais importante: saiba quando ser inconsistente – às vezes o guia de estilo simplesmente não se aplica. Na dúvida, use seu bom senso. Veja outros exemplos e decida o que parece melhor. E não hesite em perguntar!
Combine PEP-8 com The Zen of Python (também um PEP - PEP-20), e você terá uma base perfeita para criar código legível e sustentável. Adicione Design Patterns e você estará pronto para criar todo tipo de sistema de software com consistência e capacidade de evolução.
Padrões de Design Python
O que é um padrão de design?
Tudo começa com o Gang of Four (GOF). Faça uma pesquisa online rápida se você não estiver familiarizado com o GOF.
Padrões de projeto são uma maneira comum de resolver problemas bem conhecidos. Dois princípios principais estão na base dos padrões de projeto definidos pelo GOF:
- Programe para uma interface e não para uma implementação.
- Favorecer a composição do objeto sobre a herança.
Vamos dar uma olhada nesses dois princípios da perspectiva dos programadores Python.
Programe para uma interface e não uma implementação
Pense na digitação de pato. Em Python não gostamos de definir interfaces e classes de programa de acordo com essas interfaces, gostamos? Mas, ouça-me! Isso não significa que não pensamos em interfaces, na verdade com Duck Typing fazemos isso o tempo todo.
Vamos dizer algumas palavras sobre a infame abordagem Duck Typing para ver como ela se encaixa nesse paradigma: programar para uma interface.
Não nos preocupamos com a natureza do objeto, não precisamos nos importar com o que é o objeto; queremos apenas saber se ele é capaz de fazer o que precisamos (estamos interessados apenas na interface do objeto).
O objeto pode grasnar? Então, deixe-o quack!
try: bird.quack() except AttributeError: self.lol()
Definimos uma interface para o nosso pato? Não! Programamos para a interface em vez da implementação? Sim! E, eu acho isso tão legal.
Como Alex Martelli aponta em sua conhecida apresentação sobre Design Patterns em Python, “Ensinar os patos a digitar demora um pouco, mas economiza muito trabalho depois!”
Favorecer a composição do objeto sobre a herança
Agora, isso é o que eu chamo de princípio Pythonico ! Eu criei menos classes/subclasses em comparação ao encapsulamento de uma classe (ou mais frequentemente, várias classes) em outra classe.
Em vez de fazer isso:
class User(DbObject): pass
Podemos fazer algo assim:
class User: _persist_methods = ['get', 'save', 'delete'] def __init__(self, persister): self._persister = persister def __getattr__(self, attribute): if attribute in self._persist_methods: return getattr(self._persister, attribute)
As vantagens são óbvias. Podemos restringir quais métodos da classe encapsulada expor. Podemos injetar a instância persistente em tempo de execução! Por exemplo, hoje é um banco de dados relacional, mas amanhã pode ser o que for, com a interface que precisamos (de novo aqueles patos traquinas).
A composição é elegante e natural para Python.
Padrões comportamentais
Padrões Comportamentais envolvem a comunicação entre objetos, como os objetos interagem e cumprem uma determinada tarefa. De acordo com os princípios GOF, há um total de 11 padrões comportamentais em Python: Cadeia de responsabilidade, Comando, Interpretador, Iterador, Mediador, Memento, Observador, Estado, Estratégia, Modelo, Visitante.
Acho esses padrões muito úteis, mas isso não significa que os outros grupos de padrões não sejam.
Iterador
Os iteradores são incorporados ao Python. Esta é uma das características mais poderosas da linguagem. Anos atrás, li em algum lugar que os iteradores tornam o Python incrível, e acho que esse ainda é o caso. Aprenda o suficiente sobre iteradores e geradores do Python e você saberá tudo o que precisa sobre esse padrão específico do Python.
Cadeia de responsabilidade
Esse padrão nos dá uma maneira de tratar uma solicitação usando métodos diferentes, cada um abordando uma parte específica da solicitação. Você sabe, um dos melhores princípios para um bom código é o princípio da responsabilidade única .
Cada pedaço de código deve fazer uma, e apenas uma, coisa.
Este princípio está profundamente integrado neste padrão de projeto.
Por exemplo, se queremos filtrar algum conteúdo, podemos implementar filtros diferentes, cada um fazendo um tipo de filtragem preciso e claramente definido. Esses filtros podem ser usados para filtrar palavras ofensivas, anúncios, conteúdo de vídeo inadequado e assim por diante.
class ContentFilter(object): def __init__(self, filters=None): self._filters = list() if filters is not None: self._filters += filters def filter(self, content): for filter in self._filters: content = filter(content) return content filter = ContentFilter([ offensive_filter, ads_filter, porno_video_filter]) filtered_content = filter.filter(content)
Comando
Este é um dos primeiros padrões de design Python que implementei como programador. Isso me lembra: os padrões não são inventados, eles são descobertos . Eles existem, só precisamos encontrá-los e colocá-los em uso. Descobri este para um projeto incrível que implementamos há muitos anos: um editor de XML WYSIWYM para fins especiais. Depois de usar esse padrão intensamente no código, li mais sobre ele em alguns sites.
O padrão de comando é útil em situações em que, por algum motivo, precisamos começar preparando o que será executado e depois executá-lo quando necessário. A vantagem é que encapsular ações dessa forma permite que desenvolvedores Python adicionem funcionalidades adicionais relacionadas às ações executadas, como desfazer/refazer, ou manter um histórico de ações e similares.
Vamos ver como é um exemplo simples e frequentemente usado:
class RenameFileCommand(object): def __init__(self, from_name, to_name): self._from = from_name self._to = to_name def execute(self): os.rename(self._from, self._to) def undo(self): os.rename(self._to, self._from) class History(object): def __init__(self): self._commands = list() def execute(self, command): self._commands.append(command) command.execute() def undo(self): self._commands.pop().undo() history = History() history.execute(RenameFileCommand('docs/cv.doc', 'docs/cv-en.doc')) history.execute(RenameFileCommand('docs/cv1.doc', 'docs/cv-bg.doc')) history.undo() history.undo()
Padrões de criação
Vamos começar apontando que os padrões de criação não são comumente usados em Python. Por quê? Por causa da natureza dinâmica da linguagem.
Alguém mais sábio do que eu disse uma vez que o Factory está embutido no Python. Isso significa que a própria linguagem nos dá toda a flexibilidade de que precisamos para criar objetos de maneira suficientemente elegante; raramente precisamos implementar algo no topo, como Singleton ou Factory.
Em um tutorial de padrões de design do Python, encontrei uma descrição dos padrões de design de criação que afirmavam que esses padrões de design “fornecem uma maneira de criar objetos enquanto ocultam a lógica de criação, em vez de instanciar objetos diretamente usando um novo operador”.

Isso resume bem o problema: não temos um novo operador em Python!
No entanto, vamos ver como podemos implementar alguns, se sentirmos que podemos obter uma vantagem usando esses padrões.
Singleton
O padrão Singleton é usado quando queremos garantir que apenas uma instância de uma determinada classe exista durante o tempo de execução. Nós realmente precisamos desse padrão em Python? Com base na minha experiência, é mais fácil simplesmente criar uma instância intencionalmente e usá-la em vez de implementar o padrão Singleton.
Mas se você quiser implementá-lo, aqui estão algumas boas notícias: em Python, podemos alterar o processo de instanciação (junto com praticamente qualquer outra coisa). Lembre-se do método __new__()
que mencionei anteriormente? Aqui vamos nós:
class Logger(object): def __new__(cls, *args, **kwargs): if not hasattr(cls, '_logger'): cls._logger = super(Logger, cls ).__new__(cls, *args, **kwargs) return cls._logger
Neste exemplo, Logger é um Singleton.
Estas são as alternativas para usar um Singleton em Python:
- Use um módulo.
- Crie uma instância em algum lugar no nível superior do seu aplicativo, talvez no arquivo de configuração.
- Passe a instância para cada objeto que precisar dela. Isso é uma injeção de dependência e é um mecanismo poderoso e fácil de dominar.
Injeção de dependência
Não pretendo entrar em uma discussão sobre se a injeção de dependência é um padrão de design, mas direi que é um mecanismo muito bom de implementação de acoplamentos soltos e ajuda a tornar nosso aplicativo sustentável e extensível. Combine-o com Duck Typing e a Força estará com você. Sempre.
Eu o listei na seção de padrões de criação deste post porque trata da questão de quando (ou melhor ainda: onde) o objeto é criado. É criado lá fora. Melhor dizer que os objetos não são criados onde os usamos, então a dependência não é criada onde é consumida. O código do consumidor recebe o objeto criado externamente e o utiliza. Para referência adicional, leia a resposta mais votada a esta pergunta do Stackoverflow.
É uma boa explicação da injeção de dependência e nos dá uma boa ideia do potencial dessa técnica em particular. Basicamente, a resposta explica o problema com o seguinte exemplo: não pegue você mesmo as coisas para beber da geladeira, declare uma necessidade. Diga aos seus pais que você precisa de algo para beber no almoço.
Python nos oferece tudo o que precisamos para implementar isso facilmente. Pense em sua possível implementação em outras linguagens, como Java e C#, e você perceberá rapidamente a beleza do Python.
Vamos pensar em um exemplo simples de injeção de dependência:
class Command: def __init__(self, authenticate=None, authorize=None): self.authenticate = authenticate or self._not_authenticated self.authorize = authorize or self._not_autorized def execute(self, user, action): self.authenticate(user) self.authorize(user, action) return action() if in_sudo_mode: command = Command(always_authenticated, always_authorized) else: command = Command(config.authenticate, config.authorize) command.execute(current_user, delete_user_action)
Injetamos os métodos autenticador e autorizador na classe Command. Tudo o que a classe Command precisa é executá-los com sucesso sem se preocupar com os detalhes de implementação. Dessa forma, podemos usar a classe Command com qualquer mecanismo de autenticação e autorização que decidamos usar em tempo de execução.
Mostramos como injetar dependências através do construtor, mas podemos injetá-las facilmente definindo diretamente as propriedades do objeto, liberando ainda mais potencial:
command = Command() if in_sudo_mode: command.authenticate = always_authenticated command.authorize = always_authorized else: command.authenticate = config.authenticate command.authorize = config.authorize command.execute(current_user, delete_user_action)
Há muito mais a aprender sobre injeção de dependência; pessoas curiosas procurariam por IoC, por exemplo.
Mas antes de fazer isso, leia outra resposta do Stackoverflow, a mais votada para essa pergunta.
Novamente, acabamos de demonstrar como implementar esse maravilhoso padrão de design em Python é apenas uma questão de usar as funcionalidades internas da linguagem.
Não vamos esquecer o que tudo isso significa: A técnica de injeção de dependência permite testes de unidade muito flexíveis e fáceis. Imagine uma arquitetura onde você pode alterar o armazenamento de dados em tempo real. Zombar de um banco de dados se torna uma tarefa trivial, não é? Para mais informações, você pode conferir a Introdução ao Mocking in Python de Toptal.
Você também pode pesquisar os padrões de projeto Prototype , Builder e Factory .
Padrões Estruturais
Fachada
Este pode muito bem ser o padrão de projeto Python mais famoso.
Imagine que você tenha um sistema com um número considerável de objetos. Cada objeto oferece um rico conjunto de métodos de API. Você pode fazer muitas coisas com este sistema, mas que tal simplificar a interface? Por que não adicionar um objeto de interface expondo um subconjunto bem pensado de todos os métodos da API? Uma fachada!
Exemplo de padrão de design Python Facade:
class Car(object): def __init__(self): self._tyres = [Tyre('front_left'), Tyre('front_right'), Tyre('rear_left'), Tyre('rear_right'), ] self._tank = Tank(70) def tyres_pressure(self): return [tyre.pressure for tyre in self._tyres] def fuel_level(self): return self._tank.level
Não há surpresa, nem truques, a classe Car
é uma Fachada , e isso é tudo.
Adaptador
Se as fachadas são usadas para simplificar a interface, os adaptadores tratam de alterar a interface. Como usar uma vaca quando o sistema está esperando um pato.
Digamos que você tenha um método de trabalho para registrar informações em um determinado destino. Seu método espera que o destino tenha um método write()
(como todo objeto de arquivo tem, por exemplo).
def log(message, destination): destination.write('[{}] - {}'.format(datetime.now(), message))
Eu diria que é um método bem escrito com injeção de dependência, que permite grande extensibilidade. Digamos que você queira logar em algum soquete UDP em vez de em um arquivo, você sabe como abrir esse soquete UDP, mas o único problema é que o objeto de socket
não tem o método write()
. Você precisa de um adaptador !
import socket class SocketWriter(object): def __init__(self, ip, port): self._socket = socket.socket(socket.AF_INET, socket.SOCK_DGRAM) self._ip = ip self._port = port def write(self, message): self._socket.send(message, (self._ip, self._port)) def log(message, destination): destination.write('[{}] - {}'.format(datetime.now(), message)) upd_logger = SocketWriter('1.2.3.4', '9999') log('Something happened', udp_destination)
Mas por que eu acho o adaptador tão importante? Bem, quando é efetivamente combinado com injeção de dependência, nos dá uma enorme flexibilidade. Por que alterar nosso código bem testado para suportar novas interfaces quando podemos apenas implementar um adaptador que traduzirá a nova interface para a conhecida?
Você também deve verificar e dominar os padrões de design de ponte e proxy , devido à sua semelhança com o adaptador . Pense em como eles são fáceis de implementar em Python e pense em diferentes maneiras de usá-los em seu projeto.
Decorador
Ah, como somos sortudos! Os decoradores são muito legais e já os temos integrados à linguagem. O que mais gosto no Python é que usá-lo nos ensina a usar as melhores práticas. Não é que não precisemos estar conscientes sobre as melhores práticas (e padrões de design, em particular), mas com o Python sinto que estou seguindo as melhores práticas, independentemente. Pessoalmente, acho que as melhores práticas do Python são intuitivas e de segunda natureza, e isso é algo apreciado por desenvolvedores iniciantes e de elite.
O padrão decorador trata da introdução de funcionalidades adicionais e, em particular, de fazê-lo sem usar herança.
Então, vamos ver como decoramos um método sem usar a funcionalidade interna do Python. Aqui está um exemplo direto.
def execute(user, action): self.authenticate(user) self.authorize(user, action) return action()
O que não é tão bom aqui é que a função execute
faz muito mais do que executar algo. Não estamos seguindo o princípio da responsabilidade única ao pé da letra.
Seria bom simplesmente escrever apenas o seguinte:
def execute(action): return action()
Podemos implementar qualquer funcionalidade de autorização e autenticação em outro lugar, em um decorador , assim:
def execute(action, *args, **kwargs): return action() def autheticated_only(method): def decorated(*args, **kwargs): if check_authenticated(kwargs['user']): return method(*args, **kwargs) else: raise UnauthenticatedError return decorated def authorized_only(method): def decorated(*args, **kwargs): if check_authorized(kwargs['user'], kwargs['action']): return method(*args, **kwargs) else: raise UnauthorizeddError return decorated execute = authenticated_only(execute) execute = authorized_only(execute)
Agora o método execute()
é:
- Simples de ler
- Faz apenas uma coisa (pelo menos ao olhar para o código)
- Está decorado com autenticação
- Está decorado com autorização
Escrevemos o mesmo usando a sintaxe do decorador integrado do Python:
def autheticated_only(method): def decorated(*args, **kwargs): if check_authenticated(kwargs['user']): return method(*args, **kwargs ) else: raise UnauthenticatedError return decorated def authorized_only(method): def decorated(*args, **kwargs): if check_authorized(kwargs['user'], kwargs['action']): return method(*args, **kwargs) else: raise UnauthorizedError return decorated @authorized_only @authenticated_only def execute(action, *args, **kwargs): return action()
É importante notar que você não está limitado a funções como decoradores. Um decorador pode envolver classes inteiras. O único requisito é que eles sejam callables . Mas não temos nenhum problema com isso; precisamos apenas definir o método __call__(self)
.
Você também pode querer dar uma olhada no módulo functools do Python. Há muito para descobrir lá!
Conclusão
Mostrei como é fácil e natural usar os padrões de design do Python, mas também mostrei como a programação em Python também deve ser fácil.
“Simples é melhor que complexo”, lembra disso? Talvez você tenha notado que nenhum dos padrões de projeto está completa e formalmente descrito. Nenhuma implementação complexa em grande escala foi mostrada. Você precisa “sentir” e implementá-los da maneira que melhor se adapta ao seu estilo e necessidades. Python é uma ótima linguagem e oferece todo o poder que você precisa para produzir código flexível e reutilizável.
No entanto, dá-lhe mais do que isso. Dá a você a “liberdade” para escrever códigos realmente ruins . Não faça isso! Não se repita (DRY) e nunca escreva linhas de código com mais de 80 caracteres. E não se esqueça de usar padrões de design quando aplicável; é uma das melhores maneiras de aprender com os outros e ganhar com sua riqueza de experiência gratuitamente.