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.
-
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
-
Muestra el contenido del módulo con
dir
:dir(datetime)
-
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?
-
Importa ahora el módulo
collections
:import collections
¿Qué tipo tiene
collections
? ¿Dónde está definido? -
¿Qué tipo tiene el elemento
collections.abc
?type(collections.abc)
¿Dónde está definido?
-
Muestra los contenidos de
collections.abs
condir
.dir(collections.abc)
-
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
-
También podemos importar todo de un módulo, mediante:
from collections import *
-
La sintáxis
from ... import ...
importa primero el módulo que sucede afrom
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 elementoDecimal
? -
La sentencia
import
tiene una contrapartida programática:import importlib importlib.import_module('decimal')
-
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
? -
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 pordir
y los respectivos valores son los tipos de cada elemento:def dirtypes(module): return {item:type(getattr(module, item)) for item in dir(module)}
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.
-
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)
-
Comprueba dónde está el módulo
fizzbuzz
y compáralo con la localización del módulodatetime
. Obten la listasys.path
:import sys sys.path
Busca las localizaciones de
fizzbuzz
ydatetime
dentro desys.path
. -
Sal del intérprete y crea tu propio módulo
datetime.py
dentro de tu carpeta enalumni
. Añade el siguiente contenido:print('this is a fake datetime module')
-
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.
-
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?
-
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.
-
¿Qué pasa si ejecutas esto?
import importlib import datetime importlib.reload(datetime)
¿Cuántas veces se imprimer el texto esta vez?
-
Sal del intérprete y borra tu módulo
datetime
.
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:
-
En el interior de tu carpeta, en
alumni
, crea un nuevo directorio llamadoexercises
y en su interior, crea un fichero__init__.py
con el siguiente contenido:print('This module will contain the course exercises')
-
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. -
Modifica el fichero
__init__.py
para incluir la siguiente definición:def f(): ...
-
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. -
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?
-
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ónf
.Por defecto, Python no conoce los módulos contenidos en un paquete y, por tanto, no puede importarlos.
-
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']
-
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?
-
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).
-
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. -
Lanza un intérprete y fíjate en los contenidos del módulo ahora:
import exercises dir(exercises)
Comprueba que existe el nombre
'fizzbuzz'
. -
Elimina la importación relativa. Hablaremos de importaciones relativas más adelante.
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_.
-
Añade una línea al comienzo de
exercises/fizzbuzz.py
que diga así:print(f'The name of fizzbuzz.py is {__name__}')
-
Lanza un intérprete interactivo e importa el módulo
fizzbuzz
:import exercises.fizzbuzz
¿Qué nombre aparece?
-
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.
-
Para hacer lo mismo en un paquete, podemos usar el fichero especial
__main__.py
. Por ejemplo, crea este fichero dentro de la carpetaexercises
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?
-
Compara una importación con una ejecución. Para "ejecutar" la carpeta necesitarás pasar el parámetro
-m
al intérprete de Python.
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.
-
Lanza el intérprete desde tu carpeta de alumno e importa
fizzbuzz
. Comprueba el nombre:import exercises.fizzbuzz exercises.fizzbuzz.__name__
-
Ahora haz lo mismo desde la carpeta
alumni
:import delapuente.exercises.fizzbuzz delapuente.exercises.fizzbuzz.__name__
-
Crea un módulo al mismo nivel que
exercises
con el nombreextra_math.py
y el siguiente contenido:def is_divisible(value, divisor): return value % divisor == 0
-
En
fizzbuzz.py
, utiliza un import relativo para ascender un nivel, importarextra_math
y utilizar su funciónis_divisible
.from ..extra_math import is_divisible
Reemplaza el uso de
_is_divisible
poris_divisible
. -
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ódulofizzbuzz
- Desde tu carpeta de alumno, lanza un intérprete e importa el módulo
fizzbuzz
- Trata de ejecutar
-
Descarta los cambios de esta sección.
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:
- Funcionalidad de la biblioteca estándar.
- Funcionalidad de aplicaciones de terceros.
- 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.
-
Crea un módulo
a.py
con el siguiente contenido:import b def f(): return 42
-
Crea un módulo
b.py
con el siguiente contenido:from a import f def g(): return f()
-
Lanza un intérprete y trata de importar
a
:import a
¿Qué ocurre?
-
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
.
- Everything is a (Python) module
- El sistema de importación de Python es totalmente personalizable.