Calidad y Diseño de Código Python
Cuándo Usar
- Escribir o revisar código Python que funciona pero sigue siendo difícil de entender, extender o mantener.
- Reemplazar una traducción directa de Java o C++ con una construcción adecuada al lenguaje.
- Elegir una abstracción, evaluar el acoplamiento o refactorizar tras un nuevo requisito.
- Reconocer la intención de un patrón sin convertir su nombre en una obligación arquitectónica.
- Este ámbito se refiere al razonamiento sobre el código; no prescribe ni un entorno de despliegue ni un marco de trabajo.
Filosofía
Buscar la construcción más pequeña que resuelva el problema observado, no el diseño más general imaginable. Un patrón conserva su intención cuando Python ya proporciona su estructura; eliminar esa estructura es una mejora, no una falta de sofisticación. Una buena abstracción hace que el cambio sea menos costoso sin borrar la información que sus clientes necesitan. Síntesis operativa de Thinking in Python, de Bruce Eckel; corpus a8feb860. Respetar la versión de Python realmente soportada por el proyecto: el objetivo reciente del corpus no autoriza ninguna actualización implícita de versión.
Reglas
Sintaxis Legible y Nombres Predecibles
- Hacer que la indentación coincida con la estructura real del bloque; usar cuatro espacios y no mezclar tabulaciones y espacios.
- Terminar los encabezados de bloque con dos puntos; evitar puntos y comas y múltiples sentencias que compriman artificialmente el código.
- Para una sentencia larga, usar continuación entre paréntesis, corchetes o llaves en lugar de una división difícil de seguir.
- No confundir bloques con ámbito: una asignación en un
ifo unforpuede permanecer visible después del bloque. - Verificar que una asignación se ejecute en todos los caminos antes de que se use el nombre; la indentación por sí sola no garantiza su vinculación.
- Usar
snake_casepara variables, funciones, métodos y archivos;PascalCasepara clases; mayúsculas para indicar constantes. - Reservar la excepción de clases con nombres de funciones para objetos realmente usados de esa manera; no convertirlo en un estilo general.
- No inferir protección en tiempo de ejecución de un nombre en mayúsculas: indica una intención al lector.
- Mantener las convenciones consistentes del proyecto y usar su linter existente en lugar de crear reglas locales arbitrarias.
Referencias, Valores y Mutación
- Leer una asignación como vincular un nombre a un objeto; no copia el objeto.
- Distinguir explícitamente la reasignación de la mutación: reasignar un nombre deja sus alias anteriores sin cambios, mientras que mutar el objeto puede afectarlos a todos.
- Usar
==para igualdad de valor; reservarispara identidad intencionada, notablementeis Noney otros singletons. - No usar la identidad de cadenas o enteros pequeños como prueba de valor: las optimizaciones de compartir no proporcionan ese contrato.
- Una rebanada o copia superficial de una lista recrea la estructura externa, no los elementos anidados; verificar qué objetos permanecen compartidos.
- Usar una copia profunda solo cuando la necesidad sea también duplicar los objetos contenidos; una asignación no puede proporcionar este aislamiento.
- No reemplazar mecánicamente
x += yporx = x + y: para una lista, la primera forma la modifica in situ y la segunda produce una nueva. - Para números inmutables, entender que la asignación aumentada re vincula el nombre al resultado en lugar de modificar el número compartido.
- Preferir datos inmutables cuando no necesiten evolucionar, sin confundir la inmutabilidad de un contenedor con la de su contenido.
Números y Veracidad
- Distinguir
/, división que produce un flotante para operandos enteros, de//, división redondeada hacia el infinito negativo. - Probar casos negativos al portar: el cociente entero no es truncamiento hacia cero, y el resto
%sigue el signo del divisor. - No asumir que los enteros de Python tienen un ancho fijo; no se desbordan como los enteros acotados en otros lenguajes.
- Separar la aritmética exacta de enteros de la aritmética de punto flotante; no importar las garantías de una en la otra.
- Tener en cuenta el redondeo de empate a par con
round()y formato; no asumir que el redondeo siempre es alejándose de cero. - Usar separadores
_en literales para hacer números grandes legibles sin cambiar su valor. - Usar
+= 1y-= 1para incrementar o decrementar; Python no tiene operadores++o--. - Un booleano también es un entero: sumar condiciones puede contar casos verdaderos, pero
Trueno constituye un tipo numérico distinto deint. - Preferir
if items:a una prueba de longitud cuando la pregunta es simplemente si el contenedor está vacío. - Recordar que el valor de verdad de un objeto puede ser definido por
__bool__()o, en su defecto,__len__(). andyorcortocircuitan y devuelven un operando, no necesariamente un booleano; no anotar su resultado basándose solo en sus nombres.- Usar
x or defaultsolo si todos los valores falsos deben ser reemplazados; preservar cero o texto vacío con una prueba explícita deNone. - Para máscaras, distinguir operadores bit a bit de operadores lógicos;
~xno invierte una palabra de ancho fijo. - No esperar que
@realice un producto en números incorporados: la multiplicación de matrices depende del tipo y difiere de la sintaxis de decorador.
Cadenas e Interpolación
- Las cadenas son inmutables: retener el resultado de una transformación si debe reutilizarse.
- Elegir comillas que limiten el escape; usar comillas triples cuando los saltos de línea sean parte del texto.
- Usar una cadena cruda para barras invertidas literales, teniendo en cuenta la imposibilidad de terminar ese literal con una barra invertida sola.
- Usar operaciones incorporadas de búsqueda, división, reemplazo y unión en lugar de bucles de manipulación de caracteres sin una necesidad particular.
- No usar
strip,lstriporstrippara eliminar un prefijo exacto: su argumento denota caracteres, no una subcadena indivisible. - Usar
removeprefix()yremovesuffix()para eliminar un prefijo o sufijo exacto cuando la versión objetivo los soporte. +no convierte implícitamente un número a cadena; usar una conversión explícita o un f-string.- Preferir un f-string para producir texto ordinario; su expresión se evalúa y su resultado se renderiza inmediatamente.
- Usar la especificación de formato para precisión, ancho y alineación;
!rmuestra una representación útil para diagnóstico, y=etiqueta una expresión. - No reescribir usos antiguos de
%ostr.format()solo porque existen: siguen siendo válidos, y una migración debe mejorar el código concernido. - Considerar un t-string solo si la versión objetivo lo soporta y un consumidor necesita distinguir literales de valores antes del ensamblaje.
- Un t-string produce un objeto estructurado, no una cadena terminada; el consumidor debe realmente citar, escapar o rechazar interpolaciones según su dominio.
- No prometer seguridad basada únicamente en la sintaxis de interpolación, ni reutilizar un rechazo de carácter ilustrativo como protección general.
- Un consumidor de plantillas no debe asumir una alternancia estricta entre texto y valores: la iteración puede omitir literales vacíos.
Abstracciones, Dependencias y Cohesión
- Antes de añadir una nueva clase, verificar si una función, diccionario, módulo o protocolo del lenguaje es suficiente.
- Una clase que solo alberga un método no se justifica solo por esa organización.
- Nombrar el eje de cambio basándose en requisitos reales; los cambios repetidos en el mismo lugar son evidencia más útil que la flexibilidad anticipada.
- Separar lo que cambia de lo que permanece estable; no construir un mecanismo de variación cuando nada varía.
- Tratar un modismo, una solución específica y un patrón general como posibles etapas, no como una escalera que debe subirse.
- Para cada abstracción, identificar los detalles que borra y confirmar que ningún cliente legítimo necesita recuperarlos.
- También examinar el trabajo oculto detrás de la interfaz: copiar, ordenar o acceso indirecto pueden volverse dominantes en el tamaño real de los datos.
- Evaluar el acoplamiento por lo que una parte conoce: detalles del padre, clase concreta, interfaz, firma de llamada o un valor simple.
- No confundir una dependencia tipada con la ausencia de dependencia: un
Protocolevita la herencia declarada, pero sus miembros siguen siendo un contrato. - Preferir una firma de función cuando el cliente necesita solo una operación; no crear una interfaz multi-método sin un uso para ella.
- Un patrón a menudo mueve una dependencia en lugar de eliminarla; localizar la parte que aún debe nombrar las clases concretas.
- Aceptar una dependencia concreta cuando se coloca deliberadamente en un punto de ensamblaje que es económico de cambiar.
- No indagar en los detalles internos de objetos obtenidos indirectamente; solicitar el servicio requerido a través de una interfaz explícita.
- Agrupar elementos que sirven al mismo tema en un componente; separar ideas que pueden evolucionar independientemente.
- Preferir composición para variar un colaborador sin heredar sus detalles de implementación; retener herencia si la sustituibilidad es genuina.
- Verificar que un subtipo cumple con las expectativas de los clientes del tipo base, no solo los nombres de sus métodos.
- Factorizar la duplicación que expresa la misma intención por la misma razón; la semejanza accidental no es suficiente.
- Hacer que los casos comunes sean simples, los casos raros posibles y las interfaces consistentes; una excepción de comportamiento impone un costo de memorización.
- Eliminar una capa si eliminarla no elimina ni el comportamiento necesario ni el aislamiento útil; elegir entre dos soluciones suficientes por su simplicidad.
- Hacer que los cálculos sean puros cuando sea posible: entradas iguales producen el mismo resultado, sin cambios observables al mundo exterior.
Refactorizar a lo Largo de los Dos Ejes de Extensión
- Distinguir añadir un tipo de añadir una operación; un diseño a menudo favorece uno a expensas del otro.
- Un
matchque enumera todas las subclases de una familia abierta requiere mantener la lista y puede ignorar silenciosamente un nuevo tipo. - Si la operación solo agrupa por clase exacta, un diccionario indexado por
type(obj)evita esta enumeración. - No tratar este reemplazo como puramente sintáctico: una subclase obtiene entonces su propia clave, a diferencia de un patrón de clase padre.
- Para grupos creados en la primera aparición,
defaultdict(list)proporciona una lista nueva; una anotación de diccionario no crea este comportamiento. - Dejar una operación uniforme como una función ordinaria que lee los atributos relevantes; el despacho no añade nada si el algoritmo no varía por tipo.
- Mantener métodos en una jerarquía pequeña y controlada cuando cada tipo lleva naturalmente sus respuestas y las operaciones siguen siendo pocas.
- Considerar
functools.singledispatchpara añadir operaciones que varían por tipo sin modificar una jerarquía externa o esparcir nuevas operaciones por cada clase. - Con
singledispatch, la implementación se selecciona en el primer argumento y puede ser heredada según el MRO; no es una tabla de clase exacta. - Una respuesta predeterminada debe ser genuinamente válida para tipos no registrados; de lo contrario, plantear
NotImplementedErrorpara hacer visible la omisión. - No añadir métodos desde módulos arbitrarios: eso obliga a los lectores a buscar el comportamiento de una clase fuera de su definición.
- Medir una refactorización por las ubicaciones cambiadas y los datos preservados, no solo por el número de líneas eliminadas.
Usar Vocabulario de Patrones Sin un Catálogo Impuesto
- Describir primero el problema: creación, adaptación de interfaz, selección de algoritmo, notificación, un árbol o restauración de estado; el nombre viene después.
- Reducir Strategy y Command a pasar o almacenar una función cuando eso sea suficiente; una cadena puede ser una lista de funciones probadas en orden.
- Reconocer que un módulo compartido, una tabla de clases o el protocolo de iteración pueden ya cumplir la intención de Singleton, Factory o Iterator.
- Distinguir Adapter, que convierte una interfaz, Façade, que simplifica un subsistema, y Proxy, que controla el acceso a otro objeto.
- Distinguir State, que cambia el comportamiento según el estado, de una máquina de estados, que selecciona transiciones en respuesta a entradas.
- Distinguir inyectar un colaborador desde fuera de buscar ese colaborador en un registro; ambos mueven el control sin exponer dependencias de la misma manera.
- Mantener nombres arquitectónicos, de persistencia, mensajería y nube al nivel de intención descrito: su presencia en un catálogo no establece una receta de implementación.
- No inferir una recomendación de despliegue, recuperación o consistencia de una mera mención de Circuit Breaker, Saga, Repository o Event Sourcing.
Procedimiento
- Identificar el Python objetivo, las convenciones y las comprobaciones ya en uso; registrar el comportamiento a preservar.
- Enunciar un requisito concreto y el eje de cambio observado; listar los componentes afectados hoy.
- Verificar las referencias compartidas, los valores de verdad, las operaciones numéricas y las transformaciones de cadenas involucradas.
- Escribir la solución mínima, luego comparar la abstracción propuesta: detalles borrados, trabajo oculto y dependencias movidas.
- Probar añadir un tipo y añadir una operación por separado; notar los cambios requeridos en cada variante.
- Refactorizar en etapas mientras se retienen las pruebas de comportamiento y los recuentos de datos entrantes y salientes.
- Eliminar capas que se han vuelto innecesarias, luego ejecutar las pruebas y comprobaciones del proyecto en sus versiones soportadas.
Errores Comunes
- Un programa puede producir un total plausible después de perder nuevos tipos: verificar la preservación de datos, no solo la presentación de la salida.
- Una rama de respaldo o implementación predeterminada puede ocultar una extensión olvidada; preferir un fallo explícito si ninguna respuesta neutral es correcta.
- Generalizar antes de tener varios casos reales crea opciones sin evidencia de utilidad; rechazar el marco especulativo.
- Una abstracción corta puede ocultar un alto costo o borrar un detalle indispensable; contar líneas no es suficiente para juzgar su calidad.
- La sintaxis y API recientes del corpus no están necesariamente disponibles en un proyecto más antiguo; no elevar su versión mínima de Python para adoptarlas.
Verificación
- Cada nueva clase o interfaz protege una necesidad nombrada; ningún patrón está presente solo por su nombre.
- Los caminos condicionales vinculan sus variables antes de su uso; identidad, mutación e igualdad no se confunden.
- Cero, valores vacíos,
None, números negativos y datos anidados han sido ejercitados cuando su distinción importa. - Las nuevas subclases no desaparecen silenciosamente, y las comprobaciones de clase exacta o herencia han sido validadas explícitamente.