Skip to content

Latest commit

 

History

History
695 lines (502 loc) · 19 KB

File metadata and controls

695 lines (502 loc) · 19 KB

Organización del código: paquetes y módulos

Prácticamente todos los lenguajes de programación poseen una forma de organizar el código en espacios de nombres (namespaces) para agrupar funcionalidad relacionada.

En Python, esta funcionalidad son los paquetes y módulos.

La sintáxis import

  1. Importa un módulo con import:

    import datetime

    La sentencia import, seguida de un nombre, enlaza ese nombre al módulo que acaba de importar. Los módulos también son objetos:

    id(datetime)
    type(datetime)
    datetime

    Observa la representación del valor del módulo que indica dónde está definido el módulo. ¿Dónde está?

    datetime.__file__

    Podemos indicar el nombre con el que queremos asociar un módulo mediante:

    import datetime as _dates
    type(_dates)
    _dates
  2. Muestra el contenido del módulo con dir:

    dir(datetime)
  3. Podemos acceder a los contenidos de un módulo con la sintaxis punto:

    datetime.date
    datetime.time
    datetime.datetime

    ¿Cuál es el tipo de estos elementos?

  4. Importa ahora el módulo collections:

    import collections

    ¿Qué tipo tiene collections? ¿Dónde está definido?

  5. ¿Qué tipo tiene el elemento collections.abc?

    type(collections.abc)

    ¿Dónde está definido?

  6. Muestra los contenidos de collections.abs con dir.

    dir(collections.abc)
  7. Podemos importar uno o varios elementos en particular, utilizando la siguiente sintáxis:

    from collections import deque, Counter, abc
    type(deque)
    type(Counter)

    Aunque esta notación es conveniente, el uso de espacios de nombres aumenta la legibilidad del código porque contextualiza el elemento al que preceden.

    También podemos indicar el nombre con el que se cargará cada elemento:

    from collections import deque as queue, Counter as counter, abc
    deque
    counter
  8. También podemos importar todo de un módulo, mediante:

    from collections import *
  9. La sintáxis from ... import ... importa primero el módulo que sucede a from sin enlazarlo a ningún nombre:

    from decimal import Decimal
    decimal

    ¿Qué tipo de error ocurre?

    Y luego enlaza cada elemento que sucede a import al identificador con el mismo nombre:

    type(Decimal)

    ¿Qué harías para importar el módulo decimal y el elemento Decimal?

  10. La sentencia import tiene una contrapartida programática:

    import importlib
    importlib.import_module('decimal')
  11. La función import_module no enlaza ningún nombre:

    importlib.import_module('decimal')
    decimal

    ¿Qué error se produce? ¿Cómo lo solucionarías sin hacer uso de la sintáxis import?

  12. Extraer un elemento de un módulo de forma programática puede hacerse con getattr:

    getattr(collections, 'namedtuple')

    La función getattr tampoco enlaza ningún nombre.

    Implementa la función dirtypes que toma un módulo y devuelve un diccionario cuyas claves son los elementos listados por dir y los respectivos valores son los tipos de cada elemento:

    def dirtypes(module):
        return {item:type(getattr(module, item)) for item in dir(module)}

Módulos

Un módulo es un fichero Python que suele contener objetos, funciones y definiciones de tipos. El fichero debe acabar en .py, .pyc o .so. El nombre del módulo es el nombre del fichero sin la extensión y tiene que ser un identificador válido de Python.

  1. Entra en tu carpeta dentro de alumni y abre un intérprete interactivo. Prueba a importar y listar alguno de los módulos que hay en esa carpeta:

    import fizzbuzz
    dir(fizzbuzz)
    fizzbuzz.fizzbuzz(15)
  2. Comprueba dónde está el módulo fizzbuzz y compáralo con la localización del módulo datetime. Obten la lista sys.path:

    import sys
    sys.path

    Busca las localizaciones de fizzbuzz y datetime dentro de sys.path.

  3. Sal del intérprete y crea tu propio módulo datetime.py dentro de tu carpeta en alumni. Añade el siguiente contenido:

    print('this is a fake datetime module')
  4. Lanza el intérprete oficial de Python e importa datetime.

    import datetime

    Observa lo que ocurre. ¿Cuál es la localización del módulo?

    La búsqueda de los módulos se produce en orden, en cada una de las rutas de la lista sys.path:

    import sys
    sys.path

    La cadena vacía representa el lugar desde el que se lanzó el intérprete de comandos.

    Observa bien lo que ha pasado. Se ha impreso un mensaje por pantalla. Esto ocurre porque el código en el interior del módulo se ha ejecutado. Cargar un módulo implica ejecutar el código contenido en su interior.

  5. Cuando importas un módulo, este se cachea. Sal del intérprete, vuelve a entrar y prueba a importar datetime dos veces:

    import datetime
    import datetime

    ¿Cuántas veces se imprime el mensaje?

  6. Los módulos cacheados se encuentran en sys.modules:

    import sys
    sys.modules
    sys.modules.get('datetime')

    Antes de cargar un módulo, Python comprueba que no exista ya en la caché. En caso de que exista, simplemente lo toma de ahí. Si no existe, lo crea, lo inserta en la caché y lo ejecuta, en ese orden.

  7. ¿Qué pasa si ejecutas esto?

    import importlib
    import datetime
    importlib.reload(datetime)

    ¿Cuántas veces se imprimer el texto esta vez?

  8. Sal del intérprete y borra tu módulo datetime.

Paquetes

El software complejo requiere de espacios de nombres más complejos, que a menudo contienen otros espacios de nombres en su interior.

En Python usaremos paquetes. Los paquetes son módulos que pueden contener otros módulos. Se implementam mediante directorios.

Por ejemplo, el servidor web de un blog podría tener la siguiente estructura de paquetes y módulos:

.
├── controllers
│   ├── __init__.py
│   ├── form_management.py
│   ├── post_management.py
│   └── tag_management.py
├── db
│   ├── __init__.py
│   ├── mongodb.py
│   └── postgredb.py
├── models
│   ├── __init__.py
│   ├── form.py
│   ├── post.py
│   └── tag.py
└── views
    ├── __init__.py
    ├── archives.py
    ├── contact.py
    └── posts.py

El fichero __init__.py en el interior de cada directorio era obligatorio para que Python reconociera el directorio como un paquete hasta la versión 3.2. Sin embargo, empezando en Python 3.3, el uso de este fichero es opcional aunque su ausencia afecta sútilmente al comportamiento del paquete.

Durante el curso, incluiremos el fichero __init__.py y, a continuación, veremos para qué sirve:

  1. En el interior de tu carpeta, en alumni, crea un nuevo directorio llamado exercises y en su interior, crea un fichero __init__.py con el siguiente contenido:

    print('This module will contain the course exercises')
  2. Ahora lanza el intérprete de Python desde tu carpeta y ejecuta:

    import exercises
    exercises

    Fíjate que un paquete cumple un papel doble: por un lado es un contenedor de otros módulos y de ahí que usemos una carpeta. Pero por otro lado también es un módulo. El fichero especial __init__.py representa el contenido del módulo.

  3. Modifica el fichero __init__.py para incluir la siguiente definición:

    def f():
        ...
  4. Ahora lanza el intérprete de Python y lista el contenido del módulo:

    import exercises
    dir(exercises)

    Comprueba que el nombre 'f' esá entre los elementos de la lista.

  5. Vamos a hacer el contenido del paquete más interesante: traslada los módulos en el interior de tu carpeta de alumno dentro de exercises y lanza el intérprete de nuevo:

    import exercises
    dir(exercises)

    ¿Notas algo extraño?

    Por defecto, Python no carga los contenidos de un paquete. Ni siquiera los lista como elementos del módulo pero eso no quita que no estén ahí:

    from exercises import fizzbuzz
    fizzbuzz
    import exercises
    dir(exercises)

    ¿Qué pasa ahora?

  6. De hecho, relanza el intérprete y prueba a importar todo con:

    from exercises import *

    Comprueba que no se ha añadido fizzbuzz pero sí la función f.

    Por defecto, Python no conoce los módulos contenidos en un paquete y, por tanto, no puede importarlos.

  7. La única forma de hacer que esto funcione es proporcionar los contenidos del paquete de forma explícita. Modifica __init__.py para añadir:

    __all__ = ['fizzbuzz']
  8. Ahora lanza el intérprete interactivo y lista los contenidos del módulo:

    import exercises
    dir(exercises)

    ¿Qué ocurre? La variable __all__ no sirve para listar los contenidos, sino para que "importar todo" funcione:

    from exercises import *
    fizzbuzz
    f

    ¿Qué ha pasado? ¿Cómo lo corregirías?

  9. Relanza el intérprete interactivo y prueba la siguiente forma de importar el módulo fizzbuzz:

    import exercises.fizzbuzz
    exercises.fizzbuzz
    dir(exercises)
    dir(exercises.fizzbuzz)

    El uso de paquetes hace necesario el uso de la "notación punto" para acceder a los módulos contenidos en su interior.

    El nombre del módulo precedido de todos los paquetes padre, separados por punto y hasta la raíz, se denomina nombre plenamente caracterizado (o fully qualified name).

  10. A veces, un paquete tiene que acceder al contenido de un submódulo. Por ejemplo porque está interesado en exponer parte de la funcionalidad directamente. Dentro de exercises/__init__.py, añade:

    from . import fizzbuzz

    El punto sucediendo al from indica una importación relativa. Un sólo punto indica que el módulo que lo sucede debe buscarse a partir del paquete actual.

  11. Lanza un intérprete y fíjate en los contenidos del módulo ahora:

    import exercises
    dir(exercises)

    Comprueba que existe el nombre 'fizzbuzz'.

  12. Elimina la importación relativa. Hablaremos de importaciones relativas más adelante.

El nombre de un módulo

En su implementación, un módulo y un script no se diferencian en nada: ambos son ficheros de Python acabados en .py. Sin embargo, durante la ejecución, un módulo y un script se ejecutan de forma ligeramente distinta.

En particular, el valor de una variable especial llamada __name__ cambia según se esté importando el fichero o ejecutando como un script_.

  1. Añade una línea al comienzo de exercises/fizzbuzz.py que diga así:

    print(f'The name of fizzbuzz.py is {__name__}')
  2. Lanza un intérprete interactivo e importa el módulo fizzbuzz:

    import exercises.fizzbuzz

    ¿Qué nombre aparece?

  3. Ahora ejecuta fizzbuzz.py como un script:

    $ python exercises/fizzbuzz.py

    ¿Qué nombre aparece ahora?

    Podemos distinguir si un fichero se está cargando como parte de una importación o ejecutando como un script consultado la variable name. Así, podemos añadir funcionalidad "de aplicación", a un módulo cualquiera.

  4. Para hacer lo mismo en un paquete, podemos usar el fichero especial __main__.py. Por ejemplo, crea este fichero dentro de la carpeta exercises y añade el siguiente contenido:

    import pkgutil
    import exercises
    print(f'List of my exercises:')
    for _, name, is_package in pkgutil.iter_modules(exercises.__path__):
        print(f'\t{name} (is package: {is_package})')

    ¿Puedes corregir la salida?

  5. Compara una importación con una ejecución. Para "ejecutar" la carpeta necesitarás pasar el parámetro -m al intérprete de Python.

Importaciones relativas de ancestros y nombres

Igual que un punto . se refiere al paquete que contiene al módulo en ejecución (el paquete actual), dos puntos seguidos .. se refieren al paquete padre. Cada punto extra implica un nivel superior adicional.

Para resolver el nombre del módulo al que va a accederse, se cuenta el número de puntos en el nombre del paquete actual. Así, si el nombre es pkgA.pkgB.moduleC, un punto . se refiere a pkgB y .. se refiere a pkgA. Tres puntos ... estaría "más allá" del módulo raíz y sería un error.

El nombre de un módulo varía dependiendo de el lugar dónde lancemos el intérprete.

  1. Lanza el intérprete desde tu carpeta de alumno e importa fizzbuzz. Comprueba el nombre:

    import exercises.fizzbuzz
    exercises.fizzbuzz.__name__
  2. Ahora haz lo mismo desde la carpeta alumni:

    import delapuente.exercises.fizzbuzz
    delapuente.exercises.fizzbuzz.__name__
  3. Crea un módulo al mismo nivel que exercises con el nombre extra_math.py y el siguiente contenido:

    def is_divisible(value, divisor):
        return value % divisor == 0
  4. En fizzbuzz.py, utiliza un import relativo para ascender un nivel, importar extra_math y utilizar su función is_divisible.

    from ..extra_math import is_divisible

    Reemplaza el uso de _is_divisible por is_divisible.

  5. En base a lo que sabes del nombre, trata de predecir el resultado de las siguiente ejecuciones:

    • Trata de ejecutar fizzbuzz.py como si fuera un script.
    • Desde la carpeta alumni, lanza un intérprete e importa el módulo fizzbuzz
    • Desde tu carpeta de alumno, lanza un intérprete e importa el módulo fizzbuzz
  6. Descarta los cambios de esta sección.

Prácticas comunes

Es conveniente que los scripts incluyan, como primera línea, un shebang que se refiera al intérprete de Python como:

```python
#!/usr/bin/env python3
```

De esta forma, si damos permisos de ejecución al script, el sistema operativo tratará de usar ese programa para ejecutar el script.

En todo Python 2, la codificación esperada por el intérprete era latin-1 pero la mayoría de los editores guardaban los ficheros en utf-8. Por tanto era necesario indicar como primera línea la codificación del fichero como:

# -*- encoding: utf-8 -*-

En Python 3, la codificación esperada es utf-8 pero seguiremos encontrando el indicador de codificación.

Lo siguiente que suele aparecer es la licencia del fichero, en comentarios:

# {{ project }}
# Copyright (C) {{ year }}  {{ organization }}
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <http://www.gnu.org/licenses/>.Copyright (C) 2018

Lo siguiente que suele aparecer es la documentación del módulo, encerrada entre comillas triples """.

"""
Contain the exercises of the course.

..moduleauthor: Salvador de la Puente <[email protected]>
"""

Es normal que la documentación incluya el autor del módulo.

La documentación puede inspeccionarse con la función help:

import datetime
help(datetime)

Lo siguiente suelen ser las importaciones de otros módulos. Un orden normal es:

  1. Funcionalidad de la biblioteca estándar.
  2. Funcionalidad de aplicaciones de terceros.
  3. Funcionalidad de la aplicación en desarrollo.

Los módulos pueden versionarse siguiendo el convenio definido en el PEP-396:

__version__ = '1.2.3'

Las definiciones privadas suelen comenzar por guión bajo _:

def _main():
    ...

if name == '__main__':
    _main()

Si el módulo puede ejecutarse como un script, la lógica se suele escribir en una función privada.

La ejecución de un módulo puede tener efectos más allá de definiciones. Por ejemplo, podría lanzar una excepción pero no suele tener prints. Si es necesario emitir alguna información, se suele usar un sistema de registro (logging).

Como ejercicio, intenta que tus módulos sigan estas prácticas. Luego añade los cambios a tu respositorio y pide que sean integrados en el repositorio principal del curso.

Importaciones circulares

  1. Crea un módulo a.py con el siguiente contenido:

    import b
    
    def f():
        return 42
  2. Crea un módulo b.py con el siguiente contenido:

    from a import f
    
    def g():
        return f()
  3. Lanza un intérprete y trata de importar a:

    import a

    ¿Qué ocurre?

  4. La solución trivial es retrasar la importación hasta justo antes del momento en el que se usará el módulo. Por ejemplo, cambiando b.py para que contenga:

    def g():
        from a import f
        return f()

    Otra solución es utilizar una importación sin from:

    import a
    
    def g():
        return a.f()

Las sentencias import pueden aparecer en cualquier bloque aunque se recomienda que sólo aparezcan a nivel de módulo. Una dependencia circular suele ser un síntoma de mal diseño. Si la función f sólo va a ser utilizado en el módulo b, debería estar definida ahí. Si puede aparecer en ambos, quizá convenga extraerla a otro módulo que importen tanto a como b.