Capítulo 3
Funciones, módulos y control
de entrada y salida
En los primeros dos capítulos se escribieron muchas instrucciones. Algunas
consistieron en cálculos, otras en encontrar información sobre una serie de
datos, como el promedio de unas mediciones, y otras en cortos algoritmos,
como el cálculo de los números de Fibonacci. Como estas tareas eran pequeñas,
bastó con escribir una instrucción tras otra en un solo bloque de código.
En la práctica, sin embargo, los problemas son más complejos. Piénsese
en las tareas de evaluar la calidad de la energía a partir de formas de onda,
de evaluar la estabilidad de un sistema eléctrico simulando contingencias o de
predecir la demanda a partir de enormes bancos de mediciones de potencia.
En casos así, en vez de resolver la tarea completa con una sola secuencia de
instrucciones, conviene resolver tareas más pequeñas y luego orquestar dichas
soluciones. En otras palabras, conviene programar de forma modular.
Este capítulo expone una de las técnicas de programación modular más
populares: la programación con funciones. Estos objetos relacionan, como
en las matemáticas, un valor de entrada con uno de salida, de forma muy
precisa. Su utilidad radica en que una única implementación puede ser reusada
innumerables veces. Así, se alivia la tarea de programación y se reduce el riesgo
de errores. El capítulo termina con la escritura y lectura de archivos.
3.1. Funciones con y sin valores de retorno
Una función es una regla que asocia elementos de dos conjuntos, llamados
dominio y codominio. Específicamente, una función f asocia a cada elemento
1
2 Funciones, módulos y control de entrada y salida
del dominio A un elemento del subdominio B. Esto se escribe formalmente
como f : A → B.
El concepto de función es inherente al ser humano. Es fácil enumerar
ejemplos de funciones en la vida cotidiana. Uno es el hecho de que todas
las personas tengan un número de identificación I: esta es, en esencia, una
función del conjunto «personas» (P ) al conjunto de los números naturales (N);
o sea, es una función I : P → N. Otro ejemplo es la función «madre» M : a
cada elemento del conjunto «personas» (P ) se asocia un elemento «madre»
que también pertenece a dicho conjunto; es decir, es una función M : P → P .
Cuando se habla de la velocidad instantánea de un vehículo se está hablando,
en esencia, de una función temporal: a cada instante de tiempo se asocia un
número, que es la velocidad del vehículo.
Las funciones en Python son igual de generales. Tanto su entrada como
su salida pueden tener cualquier valor que pueda ser asignado a una variable.
Esto incluye los valores booleanos True y False, los números como -5 o 3.14,
las cadenas de caracteres, las listas, las listas de listas... Incluso es posible que
una función reciba o devuelva otras funciones.
Considérese primero una función de la forma f : R → R, como lo es
f (x) = x2 . Su implementación sería
def f(x):
return x^2
La palabra reservada def marca el inicio de la función y es seguida con el
nombre, que en este caso es f. Lo que está entre paréntesis es una variable que
adquirirá el valor dado a la función, conocido como argumento. Así, una vez
que se escriba, por ejemplo,
f(2)
la variable x adquirirá el valor de 2. Después de los argumentos se encuentran
los dos puntos (:), que son los que marcan la definición de dicha función. Al
igual que en los condicionales, el texto que sigue a los dos puntos debe tener
un nivel de sangría hacia la derecha. Una vez que dicha sangría se revierte un
nivel a la izquierda, Python lo interpreta como el final de la definición. En este
caso, la definición consta de una sola sentencia: return x**2. Esto es lo que f
devuelve al pasarle otros argumentos:
>>> def f(x):
... return x**2
...
3.1. Funciones con y sin valores de retorno 3
>>> f(1)
1
>>> f(2)
4
>>> f(3)
9
Dado que la variable x solo existe dentro de la función (su alcance o scope es
local), bien se podría sustituir por cualquier otra variable:
>>> def f(number):
... return number**2
...
>>> f(1)
1
>>> f(2)
4
>>> f(3)
9
Esto demuestra que lo importante de las funciones es únicamente su estructura
interna.
Las funciones no necesariamente deben recibir argumentos. La siguiente es
una función constante:
>>> def g():
... return 1
...
>>> g()
1
Incluso si la función no recibe argumentos, es necesario que después de su
nombre se escriban los paréntesis. De otro modo, Python detecta un error de
sintaxis:
>>> def g:
File "<stdin>", line 1
def g:
^
SyntaxError: invalid syntax
>>> return 1
File "<stdin>", line 1
4 Funciones, módulos y control de entrada y salida
return 1
^
IndentationError: unexpected indent
Tampoco es estrictamente necesario que una función devuelva un valor. (En
este sentido, las funciones en Python difieren de las funciones matemáticas.)
>>> def h():
... print('Hola, mundo.')
...
>>> h()
Hola, mundo.
Si bien una función que no devuelva valores puede parecer inutil, es normal
que esto paso, como cuando se desea que una función opere sobre un archivo.
En ese caso, serían más similares a las subrutinas de otros lenguajes. De este
tipo fue la función que imprimía los números de Fibonacci hasta cierto límite
arbitrario, presentada en la introducción:
>>> def fib(n):
... a, b = 0, 1
... while a < n:
... print(a, end=' ')
... a, b = b, a + b
...
>>> fib(1000)
0 1 1 2 3 5 8 13 21 34 55 89 144 233 377 610 987
Cuando una función no devuelve nada, es equivalente a que tuviera la sentencia
return None al final de su definición.
La función f presentada antes (que devolvía el cuadrado de un número)
luce como una fórmula. Sin embargo, como lo ilustra fib, el contenido de una
función puede ser cualquier secuencia de instrucciones como las que se han
escrito hasta ahora. Es importante que dichas instrucciones hagan referencia a
variables que sean «conocidas» para la función (como sus argumentos) y que
las instrucciones se encuentren con el nivel de sangría correcto. La función que
sigue, por ejemplo, devuelve el valor absoluto de su argumento:
>>> def myabs(x):
... if x > 0:
... return x
... else:
3.1. Funciones con y sin valores de retorno 5
... return -x
...
>>> myabs(3)
3
>>> myabs(-3)
3
La implementación puede ser, por supuesto, más compleja. La siguiente función
determina si un número entre 1 y 100 es primo o no:
def es_primo(n):
if not 1 <= n <= 100:
print('El número debe estar entre 1 y 100')
return None
elif n == 1:
return True
else:
for i in range(2, n):
if n % i == 0:
return False
return True
Que esta función está correcta se puede verificar escribiendo, por ejemplo,
es_primo(10) y es_primo(11); estas llamadas a la función devuelven False
y True, respectivamente.
Conviene analizar la lógica anterior. Primero, se evalúa la expresión boolea-
na not 1 <= n <= 100. Si su valor es True, entonces se imprime una adver-
tencia y la función devuelve None. Si su valor es False, entonces n se encuentra
entre 1 y 100. Como el 1 se encuentra en una condición especial, se le trata
por separado con la sentencia elif y se conviene que siempre se devuelva
True. Finalmente, el resto de posibilidades, que son los números entre 2 y
100, son tratados por igual: se itera por todos los posibles divisores i usando
un bucle for y si en algún momento el número n es divisible por alguno,
entonces la función devuelve False (el número no es primo). Cabe también
observar que i nunca llega a ser igual a n, dando que range siempre itera hasta
n-1. Finalmente, si la función nunca devolvió False, entonces el bucle for
agota todas las posibilidades para i y la funcion devuelve, finalmente, True
(el número es primo).
La implementación funciona porque Python trata de forma especial a la
palabra return. Apenas la encuentra en una función, devuelve el valor que
6 Funciones, módulos y control de entrada y salida
sigue a dicha palabra y sale inmediatamente de la función. Así, cuando se
encuentre return False, nunca llegará al final del bucle y, por lo tanto, no
ejecutará return True.
Las funciones pueden combinarse con las estructuras de datos y de control
de flujo estudiadas hasta ahora, lo cual es sumamente poderoso. Supóngase
que se tienen mediciones de tensiones en pu y se quiere determinar cuántas de
ellas están fuera del intervalo 0,95 pu–1,05 pu. Las mediciones están guardadas
en una lista voltages:
>>> voltages = [0.93, 0.94, 0.99, 1.00, 1.03, 1.06, 1.07]
Para empezar, se puede definir una función que determine si una sola tensión
está fuera de ese rango:
>>> def is_outside(v):
... return False if 0.95 <= v <= 1.05 else True
...
(Aquí se ha aprovechado el azúcar sintáctico que permitía escribir condicionales
en una sola línea.) Después, usando la comprensión de listas unida con los
condicionales, se filtran las tensiones que devuelven True.
>>> wrong_voltages = [v for v in voltages if is_outside(v)]
En el condicional se pudo haber escrito if is_outside(v) == True, pero esto
es innecesario: ya la función is_outside devuelve, de por sí, un valor booleano.
Finalmente, se calcula la longitud de la lista filtrada:
>>> len(wrong_voltages)
4
Con la práctica, es natural prescindir de las variables intermedias (como
wrong_voltages) y que simplemente se escriba todo en una sola sentencia:
>>> outliers = len([v for v in voltages if is_outside(v)])
>>> outliers
4
Si bien algunas funciones son tan sencillas que su lógica puede leerse desde
el código fuente, la mayoría, lamentablemente, no lo son. Para mejorar la
legibilidad, es una muy buena práctica incluir comentarios. Esta sería, de
nuevo, la función es_primo:
3.1. Funciones con y sin valores de retorno 7
def es_primo(n):
'''
Determinar si n es primo o no.
El primer argumento es un entero ubicado entre 1 y 100. Si
no pertenece a ese rango, entonces se devuelve None, caso contrario,
se devuelve True o False.
'''
# Tratar casos fuera del rango
if not 1 <= n <= 100:
print('El número debe estar entre 1 y 100')
return None
# Tratar el 1 (caso especial)
elif n == 1:
return True
# Tratar los demás números (caso genérico)
else:
for i in range(2, n):
if n % i == 0:
return False
# Si la evaluación de la función llegó hasta aquí,
# no se encontraron divisores, así que el número es primo
return True
Además de los comentarios marcados con # , se han utilizado cadenas de
caracteres multilínea, que son las delimitadas por '''. Cuando estas son
utilizadas dentro de funciones, se les llama docstrings. Este nombre se debe a
que su propósito es documentar toda la función: explicar qué recibe, qué hace y
qué devuelve. Es importante que las docstrings sean delimitadas con ''' para
que Python las pueda interpretar como tales. Así, es posible imprimir dicha
cadena con el atributo __doc__ de las funciones. Dada la definición anterior,
print(es_primo.__doc__) resulta en
Determinar si n es primo o no.
El primer argumento es un entero ubicado entre 1 y 100. Si
no pertenece a ese rango, entonces se devuelve None, caso contrario,
se devuelve True o False.
8 Funciones, módulos y control de entrada y salida
La forma recomendada de acceder a dicha docstring es utilizar la función
help(). Hacer help(es_primo) resuta en
Help on function es_primo in module __main__:
es_primo(n)
Determinar si n es primo o no.
El primer argumento es un entero ubicado entre 1 y 100. Si
no pertenece a ese rango, entonces se devuelve None, caso contrario,
se devuelve True o False.
Como los desarrolladores de los módulos estándar suelen incluir docstrings, es
posible hacer help() cuando se desconoce qué hace una función:
>>> import math
>>> help(math.hypot)
Help on built-in function hypot in module math:
hypot(...)
hypot(*coordinates) -> value
Multidimensional Euclidean distance from the origin to a point.
Roughly equivalent to:
sqrt(sum(x**2 for x in coordinates))
For a two dimensional point (x, y), gives the hypotenuse
using the Pythagorean theorem: sqrt(x*x + y*y).
For example, the hypotenuse of a 3/4/5 right triangle is:
>>> hypot(3.0, 4.0)
5.0
3.2. Lectura y escritura de archivos
Otras operaciones que involucran la noción de entrada y salida son la
lectura y escritura de archivos. Ambas operaciones son llevadas a cabo con la
3.2. Lectura y escritura de archivos 9
Cuadro 3.1. Modos de apertura de un archivo.
Modo Significado
'r' abrir para leer (por defecto)
'w' abrir para (sobre)escribir, creando el archivo si no existe
'x' abrir para crear (falla si el archivo ya existe)
'a' abrir para escribir, anexando al final del archivo si ya existe
'b' modo binario
't' modo de texto (por defecto)
'+' abrir un archivo del disco para actualizarlo (leer y escribir)
misma función, que es open. Además, esta función suele utilizarse en conjunto
con la palabra reservada with, dado lugar a la siguiente sintaxis:
with open('ejemplo.txt', 'w') as f:
f.write('Hola, mundo\n')
<demás código ejecutado mientras el archivo está abierto>
La combinación with open marca la apertura de un archivo, que en este caso
es 'ejemplo.txt'. Este archivo puede existir o no, según el modo en que se le
abra. El modo es el segundo argumento de open y en este caso es el carácter
'w'; este corresponde al modo de escritura. En dicho modo, si el archivo no
existe, entonces es creado primero. Otro modo, 'r', que es lectura, arrojaría
un error si el archivo todavía no existiese. Los modos en que se puede abrir un
archivo son mostrados en el cuadro 3.1.
Después de la llamada a open se asocia una variable al archivo recién
abierto: f. Por supuesto, esta variable pudo haber adquirido cualquier nombre,
como file o archivo. Con ella, es posible operar sobre el archivo. Dichas
operaciones deben incluirse dentro del cuerpo de la sentencia with: todas las
instrucciones que se ubiquen un nivel de sangría a la derecha. Una vez que se
haya salido de dicho cuerpo, Python cerrará automáticamente el archivo.
La operación que se especificó en el ejemplo, permitida en el modo 'w'
pero no en el 'r', es la de escritura. Esta se lleva a cabo con el método write.
Al escribir f.write(<string>), se escribirá la cadena <string> en el archivo.
Es importante que el argumento de write siempre sea una cadena, pues si no
el resultado será un error. Dicha cadena será escrita inmediatamente después
del último carácter escrito en el archivo. Esto es ilustrado con las instrucciones
>>> with open('Ejemplo 1.txt', 'w') as f:
... f.write('Primera línea')
10 Funciones, módulos y control de entrada y salida
... f.write('Segunda línea')
...
13
13
que producen un archivo 'Ejemplo 1.txt' que contiene
Primera líneaSegunda línea
Para evitar que todo el contenido quede en una sola línea, suele utilizarse el
carácter '\n'. Así,
>>> with open('Ejemplo 1.txt', 'w') as f:
... f.write('Primera línea\n')
... f.write('Segunda línea')
...
14
13
produciría
Primera línea
Segunda línea
Otro modo muy utilizado es el de lectura. Las siguientes instrucciones
leerían el archivo recién creado:
>>> with open('Ejemplo 1.txt', 'r') as f:
... for line in f:
... print(line)
...
Primera línea
Segunda línea
En esencia, la lectura de archivos siempre se reduce a iterar sobre f. Esto
equivale a iterar sobre los renglones del archivo. Para procesar el contenido
del archivo, se pueden usar todos los métodos estudiados antes. Las siguientes
instrucciones, por ejemplo, se deshacen de los saltos de línea y guardan todas
las palabras en una lista.
>>> all_words = []
>>> with open('Ejemplo 1.txt', 'r') as f:
3.2. Lectura y escritura de archivos 11
... for line in f:
... words = line.strip('\n').split(' ')
... all_words += words
...
>>> all_words
['Primera', 'línea', 'Segunda', 'línea']