Control de símbolos y combinación de submódulos

Intermediate

This tutorial is from open-source community. Access the source code

Introducción

En este laboratorio, aprenderás conceptos importantes relacionados con la organización de paquetes de Python. En primer lugar, aprenderás cómo controlar los símbolos exportados utilizando __all__ en módulos de Python. Esta habilidad es crucial para gestionar lo que se expone desde tus módulos.

En segundo lugar, entenderás cómo combinar submódulos para importaciones más sencillas y dominar la técnica de división de módulos para una mejor organización del código. Estas prácticas mejorarán la legibilidad y mantenibilidad de tu código de Python.

Este es un Guided Lab, que proporciona instrucciones paso a paso para ayudarte a aprender y practicar. Sigue las instrucciones cuidadosamente para completar cada paso y obtener experiencia práctica. Los datos históricos muestran que este es un laboratorio de nivel intermedio con una tasa de finalización del 62%. Ha recibido una tasa de reseñas positivas del 100% por parte de los estudiantes.

Comprendiendo la complejidad de las importaciones de paquetes

Cuando empieces a trabajar con paquetes de Python, rápidamente te darás cuenta de que importar módulos puede volverse bastante complicado y prolijo. Esta complejidad puede hacer que tu código sea más difícil de leer y escribir. En este laboratorio, analizaremos de cerca este problema y aprenderemos cómo simplificar el proceso de importación.

Estructura actual de importación

Primero, abramos la terminal. La terminal es una herramienta poderosa que te permite interactuar con el sistema operativo de tu computadora. Una vez abierta la terminal, necesitamos navegar hasta el directorio del proyecto. El directorio del proyecto es donde se almacenan todos los archivos relacionados con nuestro proyecto de Python. Para hacer esto, usaremos el comando cd, que significa "cambiar directorio".

cd ~/project

Ahora que estamos en el directorio del proyecto, examinemos la estructura actual del paquete structly. Un paquete en Python es una forma de organizar módulos relacionados. Podemos usar el comando ls -la para listar todos los archivos y directorios dentro del paquete structly, incluyendo los archivos ocultos.

ls -la structly

Notarás que hay varios módulos de Python dentro del paquete structly. Estos módulos contienen funciones y clases que podemos usar en nuestro código. Sin embargo, si queremos usar la funcionalidad de estos módulos, actualmente necesitamos usar declaraciones de importación largas. Por ejemplo:

from structly.structure import Structure
from structly.reader import read_csv_as_instances
from structly.tableformat import create_formatter, print_table

Estas rutas de importación largas pueden ser un problema para escribir, especialmente si necesitas usarlas varias veces en tu código. También hacen que tu código sea menos legible, lo que puede ser un problema cuando estás tratando de entender o depurar tu código. En este laboratorio, aprenderemos cómo organizar el paquete de manera que estas importaciones sean más simples.

Empecemos por echar un vistazo al contenido del archivo __init__.py del paquete. El archivo __init__.py es un archivo especial en los paquetes de Python. Se ejecuta cuando se importa el paquete y se puede usar para inicializar el paquete y configurar cualquier importación necesaria.

cat structly/__init__.py

Lo más probable es que encuentres que el archivo __init__.py está vacío o contiene muy poco código. En los siguientes pasos, modificaremos este archivo para simplificar nuestras declaraciones de importación.

El objetivo

Al final de este laboratorio, nuestro objetivo es poder usar declaraciones de importación mucho más simples. En lugar de las rutas de importación largas que vimos anteriormente, podremos usar declaraciones como:

from structly import Structure, read_csv_as_instances, create_formatter, print_table

O incluso:

from structly import *

Usar estas declaraciones de importación más simples hará que nuestro código sea más limpio y fácil de manejar. También nos ahorrará tiempo y esfuerzo al escribir y mantener nuestro código.

Control de símbolos exportados con __all__

En Python, cuando se utiliza la declaración from module import *, es posible que desees controlar qué símbolos (funciones, clases, variables) se importan de un módulo. Aquí es donde la variable __all__ resulta útil. La declaración from module import * es una forma de importar todos los símbolos de un módulo al espacio de nombres actual. Sin embargo, a veces no se desea importar cada símbolo, especialmente si hay muchos o si algunos están destinados a ser internos del módulo. La variable __all__ te permite especificar exactamente qué símbolos deben importarse cuando se utiliza esta declaración.

¿Qué es __all__?

La variable __all__ es una lista de cadenas. Cada cadena en esta lista representa un símbolo (función, clase o variable) que un módulo exporta cuando alguien utiliza la declaración from module import *. Si la variable __all__ no está definida en un módulo, la declaración import * importará todos los símbolos que no comiencen con un guión bajo. Los símbolos que comienzan con un guión bajo generalmente se consideran privados o internos del módulo y no están destinados a ser importados directamente.

Modificación de cada submódulo

Ahora, agreguemos la variable __all__ a cada submódulo en el paquete structly. Esto nos ayudará a controlar qué símbolos se exportan de cada submódulo cuando alguien utiliza la declaración from module import *.

  1. Primero, modifiquemos structure.py:
touch ~/project/structly/structure.py

Este comando crea un nuevo archivo llamado structure.py en el directorio structly de tu proyecto. Después de crear el archivo, necesitamos agregar la variable __all__. Agrega esta línea cerca de la parte superior del archivo, justo después de las declaraciones de importación:

__all__ = ['Structure']

Esta línea le dice a Python que cuando alguien utilice from structure import *, solo se importará el símbolo Structure. Guarda el archivo y cierra el editor.

  1. A continuación, modifiquemos reader.py:
touch ~/project/structly/reader.py

Este comando crea un nuevo archivo llamado reader.py en el directorio structly. Ahora, revisa el archivo para encontrar todas las funciones que comiencen con read_csv_as_. Estas son las funciones que queremos exportar. Luego, agrega una lista __all__ con todos los nombres de estas funciones. Debería verse algo así:

__all__ = ['read_csv_as_instances', 'read_csv_as_dicts', 'read_csv_as_columns']

Ten en cuenta que los nombres reales de las funciones pueden variar dependiendo de lo que encuentres en el archivo. Asegúrate de incluir todas las funciones read_csv_as_* que encuentres. Guarda el archivo y cierra el editor.

  1. Ahora, modifiquemos tableformat.py:
touch ~/project/structly/tableformat.py

Este comando crea un nuevo archivo llamado tableformat.py en el directorio structly. Agrega esta línea cerca de la parte superior del archivo:

__all__ = ['create_formatter', 'print_table']

Esta línea especifica que cuando alguien utilice from tableformat import *, solo se importarán los símbolos create_formatter y print_table. Guarda el archivo y cierra el editor.

Importaciones unificadas en __init__.py

Ahora que cada módulo define lo que exporta, podemos actualizar el archivo __init__.py para importar todos estos símbolos. El archivo __init__.py es un archivo especial en los paquetes de Python. Se ejecuta cuando se importa el paquete y se puede utilizar para inicializar el paquete e importar símbolos de los submódulos.

touch ~/project/structly/__init__.py

Este comando crea un nuevo archivo __init__.py en el directorio structly. Cambia el contenido del archivo a:

## structly/__init__.py

from .structure import *
from .reader import *
from .tableformat import *

Estas líneas importan todos los símbolos exportados de los submódulos structure, reader y tableformat. El punto (.) antes de los nombres de los módulos indica que son importaciones relativas, lo que significa que son importaciones desde dentro del mismo paquete. Guarda el archivo y cierra el editor.

Prueba de nuestros cambios

Creemos un archivo de prueba sencillo para verificar que nuestros cambios funcionen. Este archivo de prueba intentará importar los símbolos que especificamos en las variables __all__ e imprimirá un mensaje de éxito si las importaciones son exitosas.

touch ~/project/test_structly.py

Este comando crea un nuevo archivo llamado test_structly.py en el directorio del proyecto. Agrega este contenido al archivo:

## A simple test to verify our imports work correctly

from structly import Structure
from structly import read_csv_as_instances
from structly import create_formatter, print_table

print("Successfully imported all required symbols!")

Estas líneas intentan importar la clase Structure, la función read_csv_as_instances y las funciones create_formatter y print_table del paquete structly. Si las importaciones son exitosas, el programa imprimirá el mensaje "Successfully imported all required symbols!". Guarda el archivo y cierra el editor. Ahora ejecutemos esta prueba:

cd ~/project
python test_structly.py

El comando cd ~/project cambia el directorio de trabajo actual al directorio del proyecto. El comando python test_structly.py ejecuta el script test_structly.py. Si todo está funcionando correctamente, deberías ver el mensaje "Successfully imported all required symbols!" impreso en la pantalla.

Exportando todo desde el paquete

En Python, la organización de paquetes es crucial para gestionar el código de manera efectiva. Ahora, llevaremos la organización de nuestro paquete un paso más allá. Definiremos qué símbolos deben exportarse a nivel de paquete. Exportar símbolos significa hacer ciertas funciones, clases o variables disponibles para otras partes de tu código o para otros desarrolladores que puedan utilizar tu paquete.

Agregando __all__ al paquete

Cuando trabajas con paquetes de Python, es posible que desees controlar qué símbolos son accesibles cuando alguien utiliza la declaración from structly import *. Aquí es donde la lista __all__ resulta útil. Al agregar una lista __all__ al archivo __init__.py del paquete, puedes controlar precisamente qué símbolos están disponibles cuando alguien utiliza la declaración from structly import *.

Primero, creemos o actualicemos el archivo __init__.py. Usaremos el comando touch para crear el archivo si no existe.

touch ~/project/structly/__init__.py

Ahora, abre el archivo __init__.py y agrega una lista __all__. Esta lista debe incluir todos los símbolos que queremos exportar. Los símbolos se agrupan según de dónde provienen, como los módulos structure, reader y tableformat.

## structly/__init__.py

from .structure import *
from .reader import *
from .tableformat import *

## Define what symbols are exported when using "from structly import *"
__all__ = ['Structure',  ## from structure
           'read_csv_as_instances', 'read_csv_as_dicts', 'read_csv_as_columns',  ## from reader
           'create_formatter', 'print_table']  ## from tableformat

Después de agregar el código, guarda el archivo y cierra el editor.

Entendiendo import *

El patrón from module import * generalmente no se recomienda en la mayoría del código Python. Hay varias razones para esto:

  1. Puede contaminar tu espacio de nombres con símbolos inesperados. Esto significa que podrías terminar con variables o funciones en tu espacio de nombres actual que no esperabas, lo que puede provocar conflictos de nombres.
  2. Hace que sea poco claro de dónde provienen ciertos símbolos. Cuando se utiliza import *, es difícil saber de qué módulo proviene un símbolo, lo que puede hacer que tu código sea más difícil de entender y mantener.
  3. Puede provocar problemas de sombreado (shadowing). El sombreado ocurre cuando una variable o función local tiene el mismo nombre que una variable o función de otro módulo, lo que puede causar un comportamiento inesperado.

Sin embargo, hay casos específicos en los que es adecuado utilizar import *:

  • Para paquetes diseñados para ser utilizados como un todo cohesivo. Si un paquete está destinado a ser utilizado como una unidad única, entonces utilizar import * puede facilitar el acceso a todos los símbolos necesarios.
  • Cuando un paquete define una interfaz clara a través de __all__. Al utilizar la lista __all__, puedes controlar qué símbolos se exportan, lo que hace que sea más seguro utilizar import *.
  • Para uso interactivo, como en un REPL (Read-Eval-Print Loop) de Python. En un entorno interactivo, puede ser conveniente importar todos los símbolos a la vez.

Prueba con Import *

Para verificar que podemos importar todos los símbolos a la vez, creemos otro archivo de prueba. Usaremos el comando touch para crear el archivo.

touch ~/project/test_import_all.py

Ahora, abre el archivo test_import_all.py y agrega el siguiente contenido. Este código importa todos los símbolos del paquete structly y luego prueba si algunos de los símbolos importantes están disponibles.

## Test importing everything at once

from structly import *

## Try using the imported symbols
print(f"Structure symbol is available: {Structure is not None}")
print(f"read_csv_as_instances symbol is available: {read_csv_as_instances is not None}")
print(f"create_formatter symbol is available: {create_formatter is not None}")
print(f"print_table symbol is available: {print_table is not None}")

print("All symbols successfully imported!")

Guarda el archivo y cierra el editor. Ahora, ejecutemos la prueba. Primero, navega al directorio del proyecto utilizando el comando cd y luego ejecuta el script de Python.

cd ~/project
python test_import_all.py

Si todo está configurado correctamente, deberías ver una confirmación de que todos los símbolos se importaron correctamente.

División de módulos para una mejor organización del código

A medida que crecen tus proyectos de Python, es posible que descubras que un solo archivo de módulo se vuelve bastante grande y contiene múltiples componentes relacionados pero distintos. Cuando esto sucede, es una buena práctica dividir el módulo en un paquete con submódulos. Este enfoque hace que tu código esté más organizado, sea más fácil de mantener y sea más escalable.

Comprendiendo la estructura actual

El módulo tableformat.py es un buen ejemplo de un módulo grande. Contiene varias clases de formateadores, cada una responsable de formatear datos de una manera diferente:

  • TableFormatter (clase base): Esta es la clase base para todas las demás clases de formateadores. Define la estructura básica y los métodos que las otras clases heredarán e implementarán.
  • TextTableFormatter: Esta clase formatea los datos en texto plano.
  • CSVTableFormatter: Esta clase formatea los datos en formato CSV (Comma-Separated Values, valores separados por comas).
  • HTMLTableFormatter: Esta clase formatea los datos en formato HTML (Hypertext Markup Language, lenguaje de marcado de hipertexto).

Reorganizaremos este módulo en una estructura de paquete con archivos separados para cada tipo de formateador. Esto hará que el código sea más modular y más fácil de gestionar.

Paso 1: Limpiar archivos de caché

Antes de comenzar a reorganizar el código, es una buena idea limpiar cualquier archivo de caché de Python. Estos archivos son creados por Python para acelerar la ejecución de tu código, pero a veces pueden causar problemas cuando estás realizando cambios en tu código.

cd ~/project/structly
rm -rf __pycache__

En los comandos anteriores, cd ~/project/structly cambia el directorio actual al directorio structly de tu proyecto. rm -rf __pycache__ elimina el directorio __pycache__ y todo su contenido. La opción -r significa recursivo, lo que significa que eliminará todos los archivos y subdirectorios dentro del directorio __pycache__. La opción -f significa forzar, lo que significa que eliminará los archivos sin pedir confirmación.

Paso 2: Crear la nueva estructura de paquete

Ahora, creemos una nueva estructura de directorios para nuestro paquete. Crearemos un directorio llamado tableformat y un subdirectorio llamado formats dentro de él.

mkdir -p tableformat/formats

El comando mkdir se utiliza para crear directorios. La opción -p significa padres, lo que significa que creará todos los directorios padres necesarios si no existen. Entonces, si el directorio tableformat no existe, se creará primero, y luego se creará el directorio formats dentro de él.

Paso 3: Mover y renombrar el archivo original

A continuación, moveremos el archivo original tableformat.py a la nueva estructura y lo renombraremos a formatter.py.

mv tableformat.py tableformat/formatter.py

El comando mv se utiliza para mover o renombrar archivos. En este caso, estamos moviendo el archivo tableformat.py al directorio tableformat y renombrándolo a formatter.py.

Paso 4: Dividir el código en archivos separados

Ahora necesitamos crear archivos para cada formateador y mover el código relevante a ellos.

1. Crear el archivo del formateador base

touch tableformat/formatter.py

El comando touch se utiliza para crear un archivo vacío. En este caso, estamos creando un archivo llamado formatter.py en el directorio tableformat.

Mantendremos la clase base TableFormatter y cualquier función de utilidad general como print_table y create_formatter en este archivo. El archivo debería verse algo así:

## Base TableFormatter class and utility functions

__all__ = ['TableFormatter', 'print_table', 'create_formatter']

class TableFormatter:
    def headings(self, headers):
        '''
        Emit table headings.
        '''
        raise NotImplementedError()

    def row(self, rowdata):
        '''
        Emit a single row of table data.
        '''
        raise NotImplementedError()

def print_table(objects, columns, formatter):
    '''
    Make a nicely formatted table from a list of objects and attribute names.
    '''
    formatter.headings(columns)
    for obj in objects:
        rowdata = [getattr(obj, name) for name in columns]
        formatter.row(rowdata)

def create_formatter(fmt):
    '''
    Create an appropriate formatter given an output format name.
    '''
    if fmt == 'text':
        from .formats.text import TextTableFormatter
        return TextTableFormatter()
    elif fmt == 'csv':
        from .formats.csv import CSVTableFormatter
        return CSVTableFormatter()
    elif fmt == 'html':
        from .formats.html import HTMLTableFormatter
        return HTMLTableFormatter()
    else:
        raise ValueError(f'Unknown format {fmt}')

La variable __all__ se utiliza para especificar qué símbolos deben importarse cuando se utiliza from module import *. En este caso, estamos especificando que solo se deben importar los símbolos TableFormatter, print_table y create_formatter.

La clase TableFormatter es la clase base para todas las demás clases de formateadores. Define dos métodos, headings y row, que están destinados a ser implementados por las subclases.

La función print_table es una función de utilidad que toma una lista de objetos, una lista de nombres de columnas y un objeto formateador, y muestra los datos en una tabla formateada.

La función create_formatter es una función fábrica que toma un nombre de formato como argumento y devuelve un objeto formateador adecuado.

Guarda y cierra el archivo después de realizar estos cambios.

2. Crear el formateador de texto

touch tableformat/formats/text.py

Agregaremos solo la clase TextTableFormatter a este archivo.

## Text formatter implementation

__all__ = ['TextTableFormatter']

from ..formatter import TableFormatter

class TextTableFormatter(TableFormatter):
    '''
    Emit a table in plain-text format
    '''
    def headings(self, headers):
        print(' '.join('%10s' % h for h in headers))
        print(('-'*10 + ' ')*len(headers))

    def row(self, rowdata):
        print(' '.join('%10s' % d for d in rowdata))

La variable __all__ especifica que solo se debe importar el símbolo TextTableFormatter cuando se utiliza from module import *.

La declaración from ..formatter import TableFormatter importa la clase TableFormatter del archivo formatter.py en el directorio padre.

La clase TextTableFormatter hereda de la clase TableFormatter e implementa los métodos headings y row para formatear los datos en texto plano.

Guarda y cierra el archivo después de realizar estos cambios.

3. Crear el formateador de CSV

touch tableformat/formats/csv.py

Agregaremos solo la clase CSVTableFormatter a este archivo.

## CSV formatter implementation

__all__ = ['CSVTableFormatter']

from ..formatter import TableFormatter

class CSVTableFormatter(TableFormatter):
    '''
    Output data in CSV format.
    '''
    def headings(self, headers):
        print(','.join(headers))

    def row(self, rowdata):
        print(','.join(str(d) for d in rowdata))

Similar a los pasos anteriores, estamos especificando la variable __all__, importando la clase TableFormatter e implementando los métodos headings y row para formatear los datos en formato CSV.

Guarda y cierra el archivo después de realizar estos cambios.

4. Crear el formateador de HTML

touch tableformat/formats/html.py

Agregaremos solo la clase HTMLTableFormatter a este archivo.

## HTML formatter implementation

__all__ = ['HTMLTableFormatter']

from ..formatter import TableFormatter

class HTMLTableFormatter(TableFormatter):
    '''
    Output data in HTML format.
    '''
    def headings(self, headers):
        print('<tr>', end='')
        for h in headers:
            print(f'<th>{h}</th>', end='')
        print('</tr>')

    def row(self, rowdata):
        print('<tr>', end='')
        for d in rowdata:
            print(f'<td>{d}</td>', end='')
        print('</tr>')

Nuevamente, estamos especificando la variable __all__, importando la clase TableFormatter e implementando los métodos headings y row para formatear los datos en formato HTML.

Guarda y cierra el archivo después de realizar estos cambios.

Paso 5: Crear archivos de inicialización de paquete

En Python, los archivos __init__.py se utilizan para marcar directorios como paquetes de Python. Necesitamos crear archivos __init__.py tanto en el directorio tableformat como en el directorio formats.

1. Crear uno para el paquete tableformat

touch tableformat/__init__.py

Agrega este contenido al archivo:

## Re-export the original symbols from tableformat.py
from .formatter import *

Esta declaración importa todos los símbolos del archivo formatter.py y los hace disponibles cuando se importa el paquete tableformat.

Guarda y cierra el archivo después de realizar estos cambios.

2. Crear uno para el paquete formats

touch tableformat/formats/__init__.py

Puedes dejar este archivo vacío o agregar una simple cadena de documentación (docstring):

'''
Format implementations for different output formats.
'''

La cadena de documentación proporciona una breve descripción de lo que hace el paquete formats.

Guarda y cierra el archivo después de realizar estos cambios.

Paso 6: Probar la nueva estructura

Creemos una prueba sencilla para verificar que nuestros cambios funcionen correctamente.

cd ~/project
touch test_tableformat.py

Agrega este contenido al archivo test_tableformat.py:

## Test the tableformat package restructuring

from structly import *

## Create formatters of each type
text_fmt = create_formatter('text')
csv_fmt = create_formatter('csv')
html_fmt = create_formatter('html')

## Define some test data
class TestData:
    def __init__(self, name, value):
        self.name = name
        self.value = value

## Create a list of test objects
data = [
    TestData('apple', 10),
    TestData('banana', 20),
    TestData('cherry', 30)
]

## Test text formatter
print("\nText Format:")
print_table(data, ['name', 'value'], text_fmt)

## Test CSV formatter
print("\nCSV Format:")
print_table(data, ['name', 'value'], csv_fmt)

## Test HTML formatter
print("\nHTML Format:")
print_table(data, ['name', 'value'], html_fmt)

Este código de prueba importa las funciones y clases necesarias del paquete structly, crea formateadores de cada tipo, define algunos datos de prueba y luego prueba cada formateador mostrando los datos en el formato correspondiente.

Guarda y cierra el archivo después de realizar estos cambios. Ahora ejecuta la prueba:

python test_tableformat.py

Deberías ver los mismos datos formateados de tres maneras diferentes (texto, CSV y HTML). Si ves la salida esperada, significa que la reorganización de tu código fue exitosa.

Resumen

En este laboratorio, has aprendido varias técnicas cruciales de organización de Python. Primero, dominaste el uso de la variable __all__ para definir explícitamente los símbolos exportados por un módulo. Segundo, creaste una interfaz más amigable para el usuario re - exportando símbolos de submódulos desde el paquete de nivel superior.

Estas técnicas son esenciales para crear paquetes de Python limpios, mantenibles y amigables para el usuario. Te permiten controlar la vista del usuario, simplificar el proceso y organizar lógicamente el código a medida que el proyecto se expande.