Patrones de diseño de Python: para código elegante y de moda
Publicado: 2022-03-11Digámoslo de nuevo: Python es un lenguaje de programación de alto nivel con escritura dinámica y vinculación dinámica. Lo describiría como un poderoso lenguaje dinámico de alto nivel. Muchos desarrolladores están enamorados de Python por su sintaxis clara, módulos y paquetes bien estructurados, y por su enorme flexibilidad y variedad de funciones modernas.
En Python, nada te obliga a escribir clases e instanciar objetos a partir de ellas. Si no necesita estructuras complejas en su proyecto, simplemente puede escribir funciones. Aún mejor, puede escribir un script plano para ejecutar una tarea simple y rápida sin estructurar el código en absoluto.
Al mismo tiempo, Python es un lenguaje 100 por ciento orientado a objetos. ¿Como es que? Bueno, en pocas palabras, todo en Python es un objeto. Las funciones son objetos, objetos de primera clase (lo que sea que eso signifique). Este hecho de que las funciones son objetos es importante, así que por favor recuérdalo.
Por lo tanto, puede escribir scripts simples en Python, o simplemente abrir la terminal de Python y ejecutar declaraciones allí mismo (¡eso es muy útil!). Pero al mismo tiempo, puede crear marcos, aplicaciones, bibliotecas, etc. complejos. Puedes hacer mucho en Python. Por supuesto, hay una serie de limitaciones, pero ese no es el tema de este artículo.
Sin embargo, debido a que Python es tan poderoso y flexible, necesitamos algunas reglas (o patrones) al programar en él. Entonces, veamos qué son los patrones y cómo se relacionan con Python. También procederemos a implementar algunos patrones de diseño de Python esenciales.
¿Por qué Python es bueno para los patrones?
Cualquier lenguaje de programación es bueno para los patrones. De hecho, los patrones deben considerarse en el contexto de cualquier lenguaje de programación dado. Tanto los patrones, la sintaxis del lenguaje y la naturaleza imponen limitaciones a nuestra programación. Las limitaciones que provienen de la sintaxis del lenguaje y la naturaleza del lenguaje (dinámico, funcional, orientado a objetos y similares) pueden diferir, al igual que las razones detrás de su existencia. Las limitaciones que provienen de los patrones están ahí por una razón, tienen un propósito. Ese es el objetivo básico de los patrones; para decirnos cómo hacer algo y cómo no hacerlo. Hablaremos de patrones, y especialmente de patrones de diseño de Python, más adelante.
La filosofía de Python se basa en la idea de mejores prácticas bien pensadas. Python es un lenguaje dinámico (¿ya lo dije?) y, como tal, ya implementa, o facilita la implementación, una serie de patrones de diseño populares con unas pocas líneas de código. Algunos patrones de diseño están integrados en Python, por lo que los usamos incluso sin saberlo. No se necesitan otros patrones debido a la naturaleza del idioma.
Por ejemplo, Factory es un patrón de diseño estructural de Python destinado a crear nuevos objetos, ocultando la lógica de instanciación del usuario. Pero la creación de objetos en Python es dinámica por diseño, por lo que no son necesarias adiciones como Factory. Por supuesto, eres libre de implementarlo si quieres. Puede haber casos en los que sería realmente útil, pero son una excepción, no la norma.
¿Qué tiene de bueno la filosofía de Python? Empecemos con esto (explóralo en la terminal de 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!Es posible que estos no sean patrones en el sentido tradicional, pero son reglas que definen el enfoque "Pythonic" de la programación de la manera más elegante y útil.
También tenemos pautas de código PEP-8 que ayudan a estructurar nuestro código. Es imprescindible para mí, con algunas excepciones apropiadas, por supuesto. Por cierto, estas excepciones son alentadas por el mismo PEP-8:
Pero lo más importante: sepa cuándo ser inconsistente; a veces, la guía de estilo simplemente no se aplica. En caso de duda, utilice su mejor juicio. Mire otros ejemplos y decida cuál se ve mejor. ¡Y no dudes en preguntar!
Combine PEP-8 con The Zen of Python (también un PEP - PEP-20), y tendrá una base perfecta para crear código legible y mantenible. Agregue patrones de diseño y estará listo para crear todo tipo de sistema de software con consistencia y capacidad de evolución.
Patrones de diseño de Python
¿Qué es un patrón de diseño?
Todo comienza con la Banda de los Cuatro (GOF). Realice una búsqueda rápida en línea si no está familiarizado con el GOF.
Los patrones de diseño son una forma común de resolver problemas bien conocidos. Dos principios fundamentales están en la base de los patrones de diseño definidos por el GOF:
- Programa a una interfaz, no a una implementación.
- Favorecer la composición de objetos sobre la herencia.
Echemos un vistazo más de cerca a estos dos principios desde la perspectiva de los programadores de Python.
Programa a una interfaz, no a una implementación.
Piensa en Duck Typing. En Python no nos gusta definir interfaces y programar clases de acuerdo con estas interfaces, ¿verdad? ¡Pero, escúchame! Esto no significa que no pensemos en las interfaces, de hecho, con Duck Typing lo hacemos todo el tiempo.
Digamos algunas palabras sobre el infame enfoque Duck Typing para ver cómo encaja en este paradigma: programa para una interfaz.
No nos preocupamos por la naturaleza del objeto, no tiene que importarnos qué es el objeto; solo queremos saber si puede hacer lo que necesitamos (solo nos interesa la interfaz del objeto).
¿Puede el objeto graznar? Entonces, ¡déjalo graznar!
try: bird.quack() except AttributeError: self.lol()¿Definimos una interfaz para nuestro pato? ¡No! ¿Programamos la interfaz en lugar de la implementación? ¡Sí! Y, encuentro esto tan agradable.
Como señala Alex Martelli en su conocida presentación sobre patrones de diseño en Python, “enseñar a los patos a escribir lleva un tiempo, ¡pero te ahorra mucho trabajo después!”
Favorecer la composición de objetos sobre la herencia
¡Ahora, eso es lo que yo llamo un principio pitónico ! He creado menos clases/subclases en comparación con envolver una clase (o más a menudo, varias clases) en otra clase.
En lugar de hacer esto:
class User(DbObject): passPodemos hacer algo como esto:
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)Las ventajas son obvias. Podemos restringir qué métodos de la clase envuelta exponer. ¡Podemos inyectar la instancia persistente en tiempo de ejecución! Por ejemplo, hoy es una base de datos relacional, pero mañana podría ser lo que sea, con la interfaz que necesitamos (otra vez esos molestos patos).
La composición es elegante y natural para Python.
Patrones de comportamiento
Los patrones de comportamiento implican la comunicación entre objetos, cómo interactúan los objetos y cumplen una tarea determinada. Según los principios GOF, hay un total de 11 patrones de comportamiento en Python: Cadena de responsabilidad, Comando, Intérprete, Iterador, Mediador, Memento, Observador, Estado, Estrategia, Plantilla, Visitante.
Encuentro estos patrones muy útiles, pero esto no significa que los otros grupos de patrones no lo sean.
iterador
Los iteradores están integrados en Python. Esta es una de las características más poderosas del lenguaje. Hace años, leí en alguna parte que los iteradores hacen que Python sea increíble, y creo que sigue siendo así. Aprenda lo suficiente sobre los iteradores y generadores de Python y sabrá todo lo que necesita sobre este patrón particular de Python.
Cadena de responsabilidad
Este patrón nos brinda una forma de tratar una solicitud utilizando diferentes métodos, cada uno de los cuales aborda una parte específica de la solicitud. Ya sabes, uno de los mejores principios para un buen código es el principio de responsabilidad única .
Cada pieza de código debe hacer una y solo una cosa.
Este principio está profundamente integrado en este patrón de diseño.
Por ejemplo, si queremos filtrar algún contenido podemos implementar diferentes filtros, cada uno haciendo un tipo de filtrado preciso y claramente definido. Estos filtros podrían usarse para filtrar palabras ofensivas, anuncios, contenido de video inadecuado, etc.
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)Mando
Este es uno de los primeros patrones de diseño de Python que implementé como programador. Eso me recuerda: los patrones no se inventan, se descubren . Existen, solo necesitamos encontrarlos y ponerlos en uso. Descubrí este para un proyecto increíble que implementamos hace muchos años: un editor XML WYSIWYM de propósito especial. Después de usar este patrón de forma intensiva en el código, leí más sobre él en algunos sitios.
El patrón de comando es útil en situaciones en las que, por alguna razón, necesitamos comenzar preparando lo que se ejecutará y luego ejecutarlo cuando sea necesario. La ventaja es que encapsular acciones de esta manera permite a los desarrolladores de Python agregar funcionalidades adicionales relacionadas con las acciones ejecutadas, como deshacer/rehacer, o mantener un historial de acciones y similares.
Veamos cómo se ve un ejemplo simple y de uso frecuente:
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()Patrones creacionales
Comencemos señalando que los patrones de creación no se usan comúnmente en Python. ¿Por qué? Por la naturaleza dinámica del lenguaje.
Alguien más sabio que yo dijo una vez que Factory está integrado en Python. Significa que el propio lenguaje nos proporciona toda la flexibilidad que necesitamos para crear objetos de una manera suficientemente elegante; rara vez necesitamos implementar algo en la parte superior, como Singleton o Factory.
En un tutorial de patrones de diseño de Python, encontré una descripción de los patrones de diseño de creación que decía que estos patrones de diseño "proporcionan una forma de crear objetos mientras ocultan la lógica de creación, en lugar de instanciar objetos directamente usando un nuevo operador".

Eso resume bastante bien el problema: ¡ No tenemos un nuevo operador en Python!
Sin embargo, veamos cómo podemos implementar algunos, si creemos que podemos obtener una ventaja al usar tales patrones.
único
El patrón Singleton se usa cuando queremos garantizar que solo existe una instancia de una clase determinada durante el tiempo de ejecución. ¿Realmente necesitamos este patrón en Python? Según mi experiencia, es más fácil simplemente crear una instancia intencionalmente y luego usarla en lugar de implementar el patrón Singleton.
Pero si desea implementarlo, aquí hay algunas buenas noticias: en Python, podemos modificar el proceso de creación de instancias (junto con prácticamente cualquier otra cosa). ¿Recuerdas el __new__() que mencioné anteriormente? Aquí vamos:
class Logger(object): def __new__(cls, *args, **kwargs): if not hasattr(cls, '_logger'): cls._logger = super(Logger, cls ).__new__(cls, *args, **kwargs) return cls._loggerEn este ejemplo, Logger es un Singleton.
Estas son las alternativas al uso de un Singleton en Python:
- Usa un módulo.
- Cree una instancia en algún lugar del nivel superior de su aplicación, quizás en el archivo de configuración.
- Pase la instancia a cada objeto que lo necesite. Esa es una inyección de dependencia y es un mecanismo poderoso y fácil de dominar.
Inyección de dependencia
No tengo la intención de entrar en una discusión sobre si la inyección de dependencia es un patrón de diseño, pero diré que es un mecanismo muy bueno para implementar acoplamientos flexibles y ayuda a que nuestra aplicación sea mantenible y extensible. Combínalo con Duck Typing y la Fuerza estará contigo. Siempre.
Lo enumeré en la sección de patrones de creación de esta publicación porque trata la cuestión de cuándo (o mejor aún: dónde) se crea el objeto. Se crea afuera. Es mejor decir que los objetos no se crean donde los usamos, por lo que la dependencia no se crea donde se consume. El código de consumidor recibe el objeto creado externamente y lo usa. Para obtener más información, lea la respuesta más votada a esta pregunta de Stackoverflow.
Es una buena explicación de la inyección de dependencia y nos da una buena idea del potencial de esta técnica en particular. Básicamente, la respuesta explica el problema con el siguiente ejemplo: no saque cosas para beber de la nevera usted mismo, en su lugar, indique una necesidad. Dile a tus padres que necesitas algo de beber con el almuerzo.
Python nos ofrece todo lo que necesitamos para implementar eso fácilmente. Piense en su posible implementación en otros lenguajes como Java y C#, y rápidamente se dará cuenta de la belleza de Python.
Pensemos en un ejemplo simple de inyección de dependencia:
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)Inyectamos los métodos de autenticación y autorización en la clase Command. Todo lo que necesita la clase Command es ejecutarlos con éxito sin preocuparse por los detalles de implementación. De esta forma, podemos usar la clase Command con cualquier mecanismo de autenticación y autorización que decidamos usar en tiempo de ejecución.
Hemos mostrado cómo inyectar dependencias a través del constructor, pero podemos inyectarlas fácilmente configurando directamente las propiedades del objeto, desbloqueando aún más 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)Hay mucho más que aprender sobre la inyección de dependencia; los curiosos buscarían IoC, por ejemplo.
Pero antes de hacer eso, lea otra respuesta de Stackoverflow, la más votada a esta pregunta.
Nuevamente, acabamos de demostrar cómo implementar este maravilloso patrón de diseño en Python es solo una cuestión de usar las funcionalidades integradas del lenguaje.
No olvidemos lo que todo esto significa: la técnica de inyección de dependencia permite pruebas unitarias muy flexibles y sencillas. Imagine una arquitectura en la que pueda cambiar el almacenamiento de datos sobre la marcha. Burlarse de una base de datos se convierte en una tarea trivial, ¿no es así? Para obtener más información, puede consultar la Introducción a la simulación en Python de Toptal.
También es posible que desee investigar patrones de diseño de prototipos , constructores y fábricas .
Patrones Estructurales
Fachada
Este puede muy bien ser el patrón de diseño de Python más famoso.
Imagina que tienes un sistema con un número considerable de objetos. Cada objeto ofrece un amplio conjunto de métodos API. Puedes hacer muchas cosas con este sistema, pero ¿qué tal simplificar la interfaz? ¿Por qué no agregar un objeto de interfaz que exponga un subconjunto bien pensado de todos los métodos API? ¡Una fachada!
Ejemplo de patrón de diseño de 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 No hay sorpresas, ni trucos, la clase Car es una Fachada , y eso es todo.
Adaptador
Si las fachadas se utilizan para la simplificación de la interfaz, los adaptadores tienen que ver con alterar la interfaz. Como usar una vaca cuando el sistema está esperando un pato.
Digamos que tiene un método de trabajo para registrar información en un destino determinado. Su método espera que el destino tenga un método write() (como lo tiene cada objeto de archivo, por ejemplo).
def log(message, destination): destination.write('[{}] - {}'.format(datetime.now(), message)) Diría que es un método bien escrito con inyección de dependencia, lo que permite una gran extensibilidad. Digamos que desea iniciar sesión en algún socket UDP en lugar de en un archivo, sabe cómo abrir este socket UDP, pero el único problema es que el objeto del socket no tiene un método de write() . ¡Necesitas un 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)Pero, ¿por qué encuentro tan importante el adaptador ? Bueno, cuando se combina de manera efectiva con la inyección de dependencia, nos brinda una gran flexibilidad. ¿Por qué modificar nuestro código bien probado para admitir nuevas interfaces cuando solo podemos implementar un adaptador que traducirá la nueva interfaz a la conocida?
También debe verificar y dominar los patrones de diseño de puente y proxy , debido a su similitud con el adaptador . Piense en lo fáciles que son de implementar en Python y piense en las diferentes formas en que podría usarlos en su proyecto.
Decorador
¡Ay, qué suerte tenemos! Los decoradores son muy simpáticos y ya los tenemos integrados en el lenguaje. Lo que más me gusta de Python es que usarlo nos enseña a usar las mejores prácticas. No es que no tengamos que ser conscientes de las mejores prácticas (y los patrones de diseño, en particular), pero con Python siento que estoy siguiendo las mejores prácticas, independientemente. Personalmente, creo que las mejores prácticas de Python son intuitivas y de segunda naturaleza, y esto es algo que aprecian tanto los desarrolladores novatos como los de élite.
El patrón decorador se trata de introducir funcionalidad adicional y, en particular, hacerlo sin usar herencia.
Entonces, veamos cómo decoramos un método sin usar la funcionalidad integrada de Python. Aquí hay un ejemplo sencillo.
def execute(user, action): self.authenticate(user) self.authorize(user, action) return action() Lo que no es tan bueno aquí es que la función de execute hace mucho más que ejecutar algo. No estamos siguiendo el principio de responsabilidad única al pie de la letra.
Sería bueno simplemente escribir lo siguiente:
def execute(action): return action()Podemos implementar cualquier funcionalidad de autorización y autenticación en otro lugar, en un decorador , así:
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) Ahora el método de execute() es:
- Fácil de leer
- Solo hace una cosa (al menos cuando mira el código)
- Está decorado con autenticación.
- Está decorado con autorización.
Escribimos lo mismo usando la sintaxis del decorador integrado de 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() Es importante tener en cuenta que no está limitado a funciones como decoradores. Un decorador puede involucrar clases enteras. El único requisito es que sean exigibles . Pero no tenemos ningún problema con eso; solo necesitamos definir el __call__(self) .
También es posible que desee echar un vistazo más de cerca al módulo de funciones de Python. ¡Hay mucho por descubrir allí!
Conclusión
He mostrado lo natural y fácil que es usar los patrones de diseño de Python, pero también he mostrado cómo la programación en Python también debería ser fácil.
"Lo simple es mejor que lo complejo", ¿ recuerdas eso? Tal vez haya notado que ninguno de los patrones de diseño se describe completa y formalmente. No se mostraron implementaciones complejas a gran escala. Necesitas “sentirlas” e implementarlas de la manera que mejor se adapte a tu estilo y necesidades. Python es un gran lenguaje y le brinda todo el poder que necesita para producir código flexible y reutilizable.
Sin embargo, te da más que eso. Te da la "libertad" para escribir código realmente malo . ¡No lo hagas! No se repita (DRY) y nunca escriba líneas de código de más de 80 caracteres. Y no olvide usar patrones de diseño cuando corresponda; es una de las mejores maneras de aprender de los demás y beneficiarse de su gran experiencia de forma gratuita.
