Ricardo Pérez López
IES Doñana, curso 2025/2026
A un bloque de sentencias que realiza una tarea específica se le puede dar un nombre.
De esta forma se crearía una única unidad de código empaquetado que actuaría bajo ese nombre como una caja negra, de manera que, para poder usarla, bastaría con llamarla invocando su nombre sin tener que conocer sus detalles internos de funcionamiento.
A este tipo de «bloques con nombre» se les denomina subrutinas, subprogramas o procedimientos.
Es el equivalente imperativo al concepto de función en programación funcional, solo que en lugar de estar formado por una expresión, está formado por una sentencia o bloque de sentencias.
Los procedimientos nos ayudan a:
Descomponer el problema principal en subproblemas más pequeños que se pueden resolver por separado de una forma más o menos independiente del resto.
Ocultar la complejidad de partes concretas de un programa bajo capas de abstracción con diferentes niveles de detalle.
Desarrollar el programa mediante sucesivos refinamientos de cada nivel de abstracción.
En definitiva, los procedimientos son abstracciones.
La llamada o invocación a un procedimiento es una sentencia simple pero que provoca la ejecución de un bloque de sentencias.
Por tanto, podría considerarse que un procedimiento es una sentencia compuesta que actúa como una sentencia simple.
La programación procedimental (procedural programming) es un paradigma de programación imperativa basada en los conceptos de procedimiento y llamada a procedimientos.
En este paradigma, un programa imperativo está compuesto principalmente por procedimientos (bloques de sentencias con nombre) que se llaman entre sí.
Los procedimientos pueden tener parámetros a través de los cuales reciben sus datos de entrada, caso de necesitarlos.
A su vez, los procedimientos pueden devolver un resultado, de ser necesario.
Los procedimientos, además, determinan su propio ámbito local y (dependiendo del lenguaje de programación usado) también podrían acceder a otros ámbitos no locales que contengan al suyo, como el ámbito global.
Durante el proceso de refinamiento sucesivo que acabamos de estudiar, se pueden ir creando procedimientos que representen diferentes niveles de detalle en el diseño descendente.
Recordemos que una estructura de control es una sentencia compuesta y, como tal, podemos estudiarla como si fuera una sola sentencia (con su entrada y su salida), sin tener que conocer el detalle de cómo funciona por dentro, es decir, sin tener que conocer qué sentencias más simples contiene.
De igual forma, una llamada a un procedimiento es una sentencia simple que actúa como una sentencia compuesta, formada por varias instrucciones (el cuerpo del procedimiento) que actúan como una sola.
En ese caso, el procedimiento actúa como un recurso abstracto en un determinado nivel (en ese nivel, se invoca al procedimiento aunque aún no exista) que luego se implementa en un nivel de mayor refinamiento.
El uso de procedimientos para escribir programas siguiendo un diseño descendente nos lleva a un código descompuesto en partes separadas en lugar de tener un único código enorme con todo el texto del programa escrito directamente al mismo nivel.
Esta forma de refinamiento y de diseño descendente está ya más relacionado con el concepto de programación modular, que estudiaremos posteriormente.
En un ordinograma, una llamada a un procedimiento se representa como un rectángulo con doble trazo superior.
El ejemplo anterior descompuesto en procedimientos sería:
El código escrito mediante descomposición en procedimientos tiene dos grandes ventajas:
Es más fácil de entender un código basado en abstracciones independientes y separadas que se llaman entre sí, antes que un código donde todo está en el mismo nivel de refinamiento formando un texto monolítico de principio a fin.
Es probable que los procedimientos así obtenidos puedan reutilizarse en otros programas con poca o ninguna variación, siempre y cuando sean lo suficientemente genéricos e independientes del resto del programa que los utiliza.
Estas ventajas nos están ya haciendo entender que puede resultar interesante diseñar un programa descomponiéndolo en partes separadas, asunto que veremos con más detalle al estudiar la programación modular.
Pero no debemos confundir la programación procedimental con la programación modular, que son términos relacionados pero diferentes.
Asimismo, la mayoría de los lenguajes estructurados permiten la creación de procedimientos, lo que a veces lleva a la confusión de creer que la programación estructurada y la procedimental son el mismo paradigma.
El diseño descendente también tiene inconvenientes:
Hasta que no se ha llegado al nivel más bajo, no se tiene algo que pueda probarse, ensamblarse y ejecutarse.
Por tanto, es un enfoque más difícil de aplicar si no se entienden bien los detalles prácticos ni se conocen perfectamente desde el principio los requisitos del programa que hay que constuir.
Puede ignorar la reutilización de procedimientos ya existentes.
Resulta fácil obtener procedimientos que sólo sean útiles para el programa actual y no se puedan generalizar para su reutilización en otras situaciones.
Cada lenguaje de programación procedimental establece sus propios mecanismos de creación de procedimientos.
En Python, los procedimientos son las denominadas funciones imperativas.
Estudiaremos ahora cómo crear y usar funciones imperativas en Python.
Al igual que ocurre en programación funcional, una función imperativa es una construcción sintáctica que acepta argumentos y produce un resultado.
Pero a diferencia de lo que ocurre en programación funcional, una función imperativa contiene sentencias.
Las funciones imperativas en Python conforman los bloques básicos que nos permiten descomponer un programa en partes que se combinan entre sí, lo que resulta el complemento perfecto para la programación estructurada.
Todavía podemos construir funciones mediante expresiones lambda, pero las funciones imperativas tienen ventajas:
Podemos escribir sentencias dentro de las funciones imperativas.
Podemos escribir funciones que no devuelvan ningún resultado porque su cometido sea provocar algún efecto lateral.
La definición de una función imperativa tiene la siguiente sintaxis:
def
⟨nombre⟩(
[⟨lista_parámetros⟩])
:
donde:
identificador
[,
identificador
]*Por ejemplo:
La definición de una función imperativa es una sentencia
compuesta, es decir, una estructura (como las
estructuras de control if
, while
,
etcétera).
Por tanto, puede aparecer en cualquier lugar del programa donde pueda haber una sentencia.
Como en cualquier otra estructura, las sentencias que contiene (las que van en el cuerpo de la función) van indentadas (o sangradas) dentro de la definición de la función.
Por tanto (de nuevo, como en cualquier otra estructura), el final de la función se deduce al encontrarse una sentencia menos indentada que el cuerpo, o bien el final del script.
La definición de una función es una sentencia ejecutable que, como cualquier otra definición, crea una ligadura entre un identificador (el nombre de la función) y una variable que almacenará una referencia a la función.
Esta función, como cualquier otro valor, se guardará en el montículo.
La definición de una función no ejecuta el cuerpo de la función. El cuerpo se ejecutará únicamente cuando se llame a la función, al igual que ocurría con las expresiones lambda.
Esa definición se ejecuta en un determinado ámbito (normalmente, el ámbito global) y, por tanto, su ligadura y su variable se almacenarán en el marco del ámbito donde se ha definido la función (normalmente, el marco global).
Asimismo, el cuerpo de una función imperativa determina un ámbito, al igual que ocurría con las expresiones lambda.
Nuestra gramática se vuelve a ampliar para incluir las definiciones de funciones imperativas como un caso más de sentencia compuesta:
def
⟨nombre⟩(
[⟨lista_parámetros⟩])
:
En la definición de un función, podemos indicar los tipos tanto de los parámetros como del valor de retorno de la función.
Esos tipos son, efectivamente, anotaciones de tipo (o type hints en inglés), y ya los hemos visto anteriormente, por ejemplo al escribir la signatura de una función.
En realidad, la primera línea de la definición de una función
(que técnicamente se denomina la cabecera de la
función) podría entenderse que es la signatura de la función, precedida
por la palabra clave def
.
Por ejemplo, la siguiente definición de una función que saluda a una persona a partir de su nombre y sus apellidos:
se podría anotar de la siguiente forma:
lo que nos lleva a que la función tiene la siguiente signatura:
\texttt{saluda(\(nombre\):\,str,\;\(apellidos\):\,str)\;->\;None}
La función anterior devuelve None
porque realmente
no devuelve ningún valor (su único cometido es escribir en la consola
usando print
).
Por tanto, nuestra sintaxis se puede ampliar para incluir las anotaciones de tipo en las definiciones de las funciones:
def
⟨nombre⟩(
[⟨lista_parámetros⟩])
[->
⟨tipo⟩]:
identificador
[[:
⟨tipo⟩]
[,
identificador
]*Es importante recordar que el intérprete no comprueba en ningún
momento las anotaciones de tipo para verificar que son correctas. Para
ello se utilizan herramientas externas como pylint
o
mypy
.
Sabemos que una llamada a cualquier función es una expresión que, como cualquier otra expresión, se evalúa en busca de su valor.
De la misma forma, una llamada a una función imperativa es una expresión que se evalúa para determinar su valor, pero, al hacerlo, se provoca la ejecución de las sentencias que forman el cuerpo de la misma.
Por eso, cuando llamamos a la función también decimos que estamos ejecutando la función, ya que estamos provocando la ejecución de las sentencias que contiene.
En concreto, cuando se llama a una función imperativa, ocurre lo siguiente (en este orden):
Como siempre que se llama a una función, se crea un nuevo marco en el entorno (que contiene las ligaduras y variables locales a su ámbito, incluyendo sus parámetros) y se almacena en la pila de control.
Se pasan los argumentos de la llamada a los parámetros de la función, de forma que los parámetros toman los valores de los argumentos correspondientes.
Recordemos que en Python se sigue el orden aplicativo (o evaluación estricta): primero se evalúan los argumentos y después se pasan a los parámetros correspondientes.
El flujo de control del programa se transfiere al bloque de sentencias que forman el cuerpo de la función y se empieza a ejecutar éste.
Cuando se termina de ejecutar el cuerpo de la función (o, dicho de otra forma, cuando se sale de la función):
Se genera su valor de retorno (en breve veremos cómo).
Se saca su marco de la pila.
Se devuelve el control de la ejecución a la sentencia que llamó a la función.
Se sustituye, en dicha sentencia, la llamada a la función por su valor de retorno.
Se continúa la ejecución del programa desde ese punto.
Como una expresión, por sí sola, es también una sentencia válida según la sintaxis de Python, podemos usar una llamada a una función como una sentencia simple allí donde se espera una sentencia cualquiera.
En consecuencia, podemos considerar que una sentencia formada únicamente por una llamada a una función es una sentencia simple que, en realidad, actúa como una sentencia compuesta: una estructura secuencial (o bloque), que es el cuerpo de la función y que se ejecuta cuando se llama a ésta.
Debemos tener en cuenta que, en ese caso, el valor de retorno se perdería ya que no se usaría.
Por ejemplo, si la función saluda
que vimos antes
devolviera el número de caracteres que tiene el nombre que recibe como
argumento, al llamar a la función como una sentencia propia:
esa llamada devolvería el valor 6
(la longitud de la
cadena 'Manuel'
)
pero dicho valor se perdería ya que nunca se usa dentro de ninguna
expresión ni como parte de otra sentencia.
Por ejemplo:
Produce la siguiente salida:
Hola Pepe
Encantado de saludarte
El gusto es mío
Hola Juan
Encantado de saludarte
Hasta luego, Lucas
Sayonara, baby
Una función puede llamar a otra.
Por ejemplo, este programa:
Produce la siguiente salida:
Hola Pepe
Me llamo Ricardo
Encantado de saludarte
El gusto es mío
Hola Juan
Me llamo Ricardo
Encantado de saludarte
Hasta luego, Lucas
Sayonara, baby
La función debe estar definida antes de poder llamarla.
Eso significa que el intérprete de Python debe ejecutar el def
de una
función antes de que el programa pueda llamar a esa función.
Por ejemplo, el siguiente programa lanzaría el error «NameError: name ‘hola’ is not defined» en la línea 1:
En cambio, este funcionaría perfectamente:
En el marco de la función llamada se almacenan, entre otras cosas, los parámetros de la función.
Al entrar en la función, los parámetros contendrán los valores de los argumentos que se hayan pasado a la función al llamar a la misma.
Existen distintos mecanismos de paso de argumentos, dependiendo del lenguaje de programación utilizado.
Los más conocidos son los llamados paso de argumentos por valor y paso de argumentos por referencia.
En Python existe un único mecanismo de paso de argumentos llamado paso de argumentos por asignación, que en la práctica resulta bastante sencillo:
Lo que hace el intérprete es asignar el argumento al
parámetro, como si hiciera internamente ⟨parámetro⟩ =
⟨argumento⟩, por lo que se aplica
todo lo relacionado con los alias de variables, mutabilidad,
etc.
Por ejemplo:
En la línea 5 se llama a saluda
asignándole al
parámetro persona
el valor 'Manolo'
.
En la línea 7 se llama a saluda
asignándole al
parámetro persona
el valor de
x
, como si se hiciera persona
=
x
, lo que sabemos que crea un
alias.
En este caso, la creación del alias no nos afectaría, ya que el valor pasado como argumento es una cadena y, por tanto, inmutable.
En caso de pasar un argumento mutable:
La función es capaz de cambiar el estado interno de la lista que se ha pasado como argumento porque:
Al llamar a la función, el argumento lista
se pasa a la función
asignándola al parámetro l
como si hubiera hecho l
=
lista
.
Eso hace que ambas variables sean alias una de la otra (se refieren al mismo objeto lista).
Por tanto, la función está modificando el valor de la variable
lista
que se ha pasado como
argumento.
Recordemos de nuevo que el intérprete no comprueba si los tipos de los argumentos coinciden con los esperados en la lista de parámetros cuando se usan anotaciones de tipos.
Por tanto, el intérprete de Python ejecutará el siguiente programa sin dar ningún error:
El intérprete no se quejará aunque en la llamada a la función le
hayamos pasado un argumento de tipo int
cuando
deberíamos haberle pasado uno de tipo str
, según se
indica en su lista de parámetros.
En este caso, además, la función print
tampoco
dará ningún error ya que puede imprimir valores de tipo entero.
return
Para devolver el resultado de la función al código que la llamó,
hay que usar una sentencia return
.
Cuando el intérprete encuentra una sentencia return
dentro
de una función, ocurre lo siguiente (en este orden):
Se genera el valor de retorno de la función, que será el valor de
la expresión que aparece en la sentencia return
.
Se finaliza la ejecución de la función, sacando su marco de la pila.
Se devuelve el control a la sentencia que llamó a la función.
En esa sentencia, se sustituye la llamada a la función por su valor de retorno (el calculado en el paso 1 anterior).
Se continúa la ejecución del programa desde ese punto.
Por ejemplo:
La función se define en las líneas 1–2. El intérprete lee la definición de la función pero no ejecuta las sentencias de su cuerpo en ese momento (lo hará cuando se llame a la función).
En la línea 6 se llama a la función suma
pasándole como argumentos los
valores de a
y b
, asignándolos a x
e y
, respectivamente.
Dentro de la función, en la sentencia return
se
calcula la suma x
+
y
y se finaliza la ejecución de
la función, devolviendo el control al punto en el que se la llamó (la
línea 6) y haciendo que su valor de retorno sea el valor calculado en la
suma anterior (el valor de la expresión que acompaña al return
).
El valor de retorno de la función sustituye a la llamada a la función en la expresión en la que aparece dicha llamada, al igual que ocurre con las expresiones lambda.
Por tanto, una vez finalizada la ejecución de la función, la línea 6 se reescribe sustituyendo la llamada a la función por su valor.
Si, por ejemplo, suponemos que el usuario ha introducido los
valores 5
y 7
en
las variables a
y b
, respectivamente, tras finalizar la
ejecución de la función tendríamos que la línea 6 quedaría:
y la ejecución del programa continuaría ejecutando la sentencia tal y como está ahora.
En resumen: la llamada a una función imperativa es una expresión que se evalúa ejecutando el cuerpo de la función (sustituyendo previamente los parámetros por los argumentos de la llamada), de forma que el valor de la expresión será el valor de retorno de la función llamada.
También es posible usar la sentencia return
sin
devolver ningún valor.
En ese caso, su utilidad es la de finalizar la ejecución de la función en algún punto intermedio de su código.
Pero en Python todas las funciones devuelven algún valor.
Lo que ocurre en este caso es que la función devuelve el valor
None
.
Por tanto, la sentencia return
sin
valor de retorno equivale a hacer return None
.
Cuando se alcanza el final del cuerpo de una función sin haberse
ejecutado antes ninguna sentencia return
, es como
si la última sentencia del cuerpo de la función fuese un return
sin
valor de retorno.
Por ejemplo:
equivale a:
Esa última sentencia return
nunca es
necesario ponerla, ya que la ejecución de una función termina
automáticamente (y retorna al punto donde se la llamó) cuando ya no
quedan más sentencias que ejecutar en su cuerpo.
La función suma
se podría
haber escrito así:
y el efecto final habría sido el mismo.
La variable res
que
aparece en el cuerpo de la función es una variable
local y sólo existe dentro de la función. Por tanto, esto sería
incorrecto:
Fuera de la función, la variable res
no está definida en el entorno (que
está formado sólo por el marco global) y por eso da error en la línea
6.
Eso significa que se crea un nuevo marco en el entorno que contendrá, al menos, los parámetros, las variables locales y las ligaduras locales a la función.
Ese marco es, por tanto, el espacio de nombres donde se almacenará todo lo que sea local a la función.
suma
Al igual que pasa con las expresiones lambda, las definiciones de funciones generan un nuevo ámbito.
Tanto los parámetros como las variables y las ligaduras que se crean en el cuerpo de la función son locales a ella, y por tanto sólo existen dentro de ella.
Su ámbito es el cuerpo de la función a la que pertenecen.
Los parámetros se pueden usar libremente en cualquier parte del cuerpo de la función porque ya se les ha asignado un valor.
En cambio, se produce un error UnboundLocalError
si se intenta usar una variable local antes de
asignarle un valor:
Desde dentro de una función es posible usar variables globales, ya que se encuentran en el entorno de la función.
Se puede consultar el valor de una variable global directamente:
Pero para poder cambiar una variable global es necesario que la función la declare previamente como global.
De no hacerlo así, el intérprete supondría que el programador quiere crear una variable local que tiene el mismo nombre que la global:
Como en Python no existen las declaraciones de variables, el intérprete tiene que averiguar por sí mismo qué ámbito tiene una variable.
Lo hace con una regla muy sencilla:
Si hay una asignación a una variable en cualquier lugar dentro de una función, esa variable se considera local a la función.
El siguiente código genera un error «UnboundLocalError: local variable ‘x’ referenced before assignment». ¿Por qué?
Como la función prueba
asigna un valor a x
, Python
considera que x
es local a la
función.
Pero en la expresión x + 4
,
la variable x
aún no tiene ningún
valor asignado, por lo que genera un error «variable local
x
referenciada antes de ser asignada».
global
Para informar al intérprete que una determinada variable es
global, se usa la sentencia global
:
La sentencia «global x
» es
una declaración que informa al intérprete de que la
variable x
debe buscarla únicamente en el marco global y
que, por tanto, debe saltarse los demás marcos que haya en el
entorno.
Si la variable global no existe en el momento de realizar la
asignación, se crea. Por tanto, una función puede crear nuevas variables
globales usando global
:
Las reglas básicas de uso de la sentencia global
en
Python son:
Cuando se crea una variable dentro de una función (asignándole un valor), por omisión es local.
Cuando se crea una variable fuera de una
función, por omisión es global (no hace falta usar la
sentencia global
).
Se usa la sentencia global
para
cambiar el valor de una variable global dentro de una función
(si la variable global no existía previamente, se crea durante la
asignación).
El uso de la sentencia global
fuera de una función no tiene ningún efecto.
La sentencia global
debe
aparecer antes de que se use la variable global
correspondiente.
Cambiar el estado de una variable global es uno de los ejemplos más claros y conocidos de los llamados efectos laterales.
Recordemos que una función tiene (o provoca) efectos laterales cuando provoca cambios de estado observables desde el exterior de la función, más allá de calcular y devolver su valor de retorno.
Por ejemplo:
Cuando cambia el valor de una variable global.
Cuando cambia un argumento mutable.
Cuando realiza una operación de entrada/salida.
Cuando llama a otras funciones que provocan efectos laterales.
Los efectos laterales hacen que el comportamiento de un programa sea más difícil de predecir.
La pureza o impureza de una función tienen mucho que ver con los efectos laterales.
Una función es pura si, desde el punto de vista de un observador externo, el único efecto que produce es calcular su valor de retorno, el cual sólo depende del valor de sus argumentos.
Por tanto, una función es impura si cumple al menos una de las siguientes condiciones:
Provoca efectos laterales, porque está haciendo algo más que calcular su valor de retorno.
Su valor de retorno depende de algo más que de sus argumentos (p. ej., de una variable global).
En una expresión, no podemos sustituir libremente una llamada a una función impura por su valor de retorno.
Por tanto, decimos que una función impura no cumple la transparencia referencial.
El siguiente es un ejemplo de función impura, ya
que, además de calcular su valor de retorno, provoca el efecto
lateral de ejecutar una operación de
entrada/salida (la función print
):
Cualquiera que desee usar la función suma
, pero no sepa cómo está construida
internamente, podría pensar que lo único que hace es calcular la suma de
dos números, pero resulta que también imprime un mensaje en la
salida, por lo que el resultado que se obtiene al ejecutar el
siguiente programa no es el que cabría esperar:
No podemos sustituir libremente en una expresión las llamadas a
la función suma
por sus valores
de retorno correspondientes.
Es decir, no es lo mismo hacer:
que hacer:
porque en el primer caso se imprimen cosas por pantalla y en el segundo no.
Por tanto, la función suma
es impura porque no cumple la transparencia
referencial, y no la cumple porque provoca un
efecto lateral.
Si una función necesita consultar el valor de una variable global, también pierde la transparencia referencial, ya que la convierte en impura porque su valor de retorno puede depender de algo más que de sus argumentos (en este caso, del valor de la variable global).
En consecuencia, la función podría producir resultados distintos en momentos diferentes ante los mismos argumentos:
En este caso, la función es impura porque,
aunque no provoca efectos laterales, sí puede verse afectada por los
efectos laterales que provocan otras partes del programa cuando
modifican el valor de la variable global z
.
Igualmente, el uso de la sentencia global
supone otra forma más de perder transparencia
referencial porque, gracias a ella, una función puede cambiar
el valor de una variable global, lo que la convertiría en
impura ya que provoca un efecto
lateral (la modificación de la variable global).
En consecuencia, esa misma función podría producir resultados distintos en momentos diferentes ante los mismos argumentos:
O también podría afectar a otras funciones que dependan del valor de la variable global.
En ese caso, ambas funciones serían impuras: la que provoca el efecto lateral y la que se ve afectada por ella.
Por ejemplo, las siguientes dos funciones son impuras, cada una por un motivo:
def cambia(x):
global z
z += x # efecto lateral: cambia una variable global
def suma(x, y):
return x + y + z # impureza: depende del valor de una variable global
z = 0
print(suma(4, 3)) # imprime 7
cambia(2) # provoca un efecto lateral
print(suma(4, 3)) # ahora imprime 9
cambia
provoca un efecto lateral y suma
depende de una variable global.
Aunque los efectos laterales resultan indeseables en general, a veces es precisamente el efecto que deseamos.
Por ejemplo, podemos diseñar una función que modifique los elementos de una lista en lugar de devolver una lista nueva:
Si la función no pudiera cambiar el interior de la lista que recibe como argumento, tendría que crear una lista nueva, lo que resultaría menos eficiente en tiempo y espacio:
En Python también podemos definir funciones dentro de funciones:
Cuando definimos una función g
dentro de otra
función f
, decimos que:
g
es un función local o
interna de f
.f
es la función externa de
g
.También se dice que:
g
es una función anidada dentro de
f
.f
contiene a g
.Como g
se define dentro de
f
, sólo es visible dentro de f
, ya que el
ámbito de g
es el cuerpo de
f
.
El uso de funciones locales evita la superpoblación de funciones en un espacio de nombres cuando esa función sólo tiene sentido usarla en un ámbito más local.
Por ejemplo:
La función fact_iter
es
local a la función fact
.
Por tanto, no se puede usar fuera de fact
, ya que
sólo existe en el ámbito de la función fact
(es decir, en
el cuerpo de la función fact
).
Como fact_iter
sólo existe para ser usada como
función auxiliar de fact
, tiene sentido definirla como una
función local de fact
.
De esta forma, no contaminaremos el espacio de nombres global con
el nombre fact_iter
, que es el nombre de una función que
sólo debe ser usada y conocida por fact
, y que queda oculta
dentro de fact
.
Tampoco se puede usar fact_iter
dentro de
fact
antes de definirla:
Esto ocurre porque la sentencia def
de la línea
3 crea una ligadura entre fact_iter
y una variable que
apunta a la función que se está definiendo, pero esa ligadura y esa
variable sólo empiezan a existir cuando se ejecuta la sentencia def
en la línea
3, y no antes.
Por tanto, en la línea 2 aún no existe la función
fact_iter
y, por tanto, no se puede usar ahí, dando un
error UnboundLocalError
.
Esto puede verse como una extensión a la regla que vimos anteriormente sobre cuándo considerar a una variable como local, cambiando «asignación» por «definición» y «variable» por «función».
Como ocurre con cualquier otra función, las funciones locales también determinan un ámbito.
Ese ámbito, como siempre ocurre, estará anidado dentro del ámbito en el que se define la función.
En este caso, el ámbito de fact_iter
está anidado
dentro del ámbito de fact
.
Asimismo, como ocurre con cualquier otra función, cuando la
ejecución del programa entre en el ámbito de fact_iter
se
creará un nuevo marco en el entorno.
Y, como siempre, ese nuevo marco apuntará al marco del ámbito que lo contiene, es decir, el marco de la función que contiene a la función local.
En este caso, el marco de fact_iter
apuntará al marco de
fact
, el cual a su vez apuntará al marco global.
nonlocal
Una función local puede acceder al valor de las variables locales a la función que la contiene, ya que se encuentran dentro de su ámbito (aunque en otro marco).
En cambio, cuando una función local quiere
cambiar mediante una asignación el valor de una
variable local a la función que la contiene, deberá declararla
previamente como no local con la sentencia nonlocal
.
De lo contrario, al intentar cambiar el valor de la variable, el intérprete crearía una nueva variable local a la función actual, que haría sombra a la variable que queremos modificar y que pertenece a otra función.
Es algo similar a lo que ocurre con la sentencia global
y las
variables globales, pero en ámbitos intermedios.
La sentencia «nonlocal n
» es
una declaración que informa al intérprete de que la
variable n
debe buscarla en el entorno saltándose el marco
de la función actual y el marco global.
def fact(n):
def fact_iter(acc):
nonlocal n
if n == 0:
return acc
else:
acc *= n
n -= 1
return fact_iter(acc)
return fact_iter(1)
print(fact(5))
La función fact_iter
puede consultar el valor de la
variable n
, ya que es una variable local a la función
fact
y, por tanto, está en el entorno de
fact_iter
(para eso no hace falta declararla como
no local).
Como, además, n
está declarada no
local en fact_iter
(en la línea 3), la función
fact_iter
también puede modificar esa variable y no hace
falta que la reciba como argumento.
Esa instrucción le indica al intérprete que, a la hora de buscar
n
en el entorno de fact_iter
, debe saltarse el
marco de fact_iter
y el marco global y, por tanto, debe
empezar a buscar en el marco de fact
.
Las funciones genéricas se definen de la siguiente forma:
En esta definición, T
es una variable de
tipo, es decir, un identificador que representa a un
tipo cualquiera que en este momento no está
determinado.
Las variables de tipo sirven, por ejemplo, para indicar que todos los elementos de una colección son del mismo tipo.
Por otra parte, [T]
representa un parámetro
de tipo, que sirve para expresar el hecho de que la función que
estamos definiendo es genérica y funciona con valores de muchos tipos
distintos.
Esta forma de expresar funciones describe un cierto tipo de polimorfismo llamado polimorfismo paramétrico, donde la misma función puede actuar sobre valores de tipos muy diversos. Por eso, a esas funciones las podemos llamar funciones polimórficas.
Por ejemplo, la función que devuelve el máximo elemento de una lista, se puede anotar de la siguiente forma:
y podríamos llamarla con cualquier tipo de lista, siempre que todos los elementos de la lista sean del mismo tipo:
Según la signatura de la función, no sería correcto pasarle una lista con elementos de diferentes tipos, pero el intérprete no detectaría el error ya que no hace comprobación de tipos. De todas formas, la función probablemente no funcionaría bien en ese caso, ya que es una violación de su especificación:
La cadena de documentación (docstring) de una función es un literal de tipo cadena que aparece como primera sentencia de la función.
Las docstrings son comentarios que tienen la finalidad de documentar la función correspondiente.
Por convenio, las docstrings siempre se delimitan
mediante triples dobles comillas ("""
).
La función help
muestran
la docstring de la función para el que se solicita la
ayuda.
Internamente, la docstring se almacena en el atributo
__doc__
de la función, ya que
para Python una función también es un objeto.
def saluda(nombre):
"""Devuelve un saludo.
Args:
nombre (str): El nombre de la persona a la que saluda.
Returns:
str: El saludo.
"""
return "¡Hola, " + nombre + "!"
Existen dos formas distintas de docstrings:
De una sola línea (one-line): para casos muy obvios que necesiten poca explicación.
De varias líneas (multi-line): para casos donde se necesita una explicación más detallada.
Lo que hace básicamente la función help(
función)
es acceder al contenido del atributo
__doc__
de la función y mostrarlo
de forma legible.
Siempre podemos acceder directamente al atributo __doc__
para recuperar la
docstring original usando función.__doc__
:
Esta información también es usada por otras herramientas de
documentación externa, como pydoc
.
Docstrings de una sola línea:
Más apropiada para funciones sencillas.
Las comillas de apertura y cierre deben aparecer en la misma línea.
No hay líneas en blanco antes o despues de la docstring.
Debe ser una frase acabada en punto que describa el efecto de la función («Hace esto», «Devuelve aquello»…).
No debe ser una signatura, así que lo siguiente está mal:
Esto está mejor:
Docstrings de varias líneas:
Toda la docstring debe ir indentada al mismo nivel que las comillas de apertura.
La primera línea debe ser un resumen informativo y caber en 80 columnas.
Puede ir en la misma línea que las comillas de apertura, o en la línea siguiente.
A continuación, debe ir una línea en blanco, seguida de una descripción más detallada.
La docstring de un módulo debe enumerar los elementos que exporta, con una línea resumen para cada uno.
La docstring de una función debe resumir su comportamiento y documentar sus argumentos, valores de retorno, efectos laterales, excepciones que lanza y precondiciones (si tiene).
pydoc
El módulo pydoc
es un generador automático de
documentación para programas Python.
La documentación generada se puede presentar en forma de páginas de texto en la consola, enviada a un navegador web o guardada en archivos HTML.
Dicha documentación se genera a partir de los docstrings de los elementos que aparecen en el código fuente del programa.
La función help
llama al
sistema de ayuda en línea del intérprete interactivo, el cual usa
pydoc
para generar su documentación en forma de texto para
la la consola.
En la línea de órdenes del sistema operativo, se puede usar
pydoc
pasándole el nombre de una función, módulo o
atributo:
Si no se indican más opciones, se visualizará en pantalla la documentación del objeto indicado:
Con la opción -w
se genera un archivo HTML:
Con la opción -b
se arranca un servidor HTTP y se
abre el navegador para visualizar la documentación:
doctest
doctest
es una herramienta que permite realizar
pruebas de forma automática sobre una función.
Para ello, se usa la docstring de la función.
En ella, se escribe una simulación de una pretendida ejecución de la función desde el intérprete interactivo de Python.
La herramienta comprueba si la salida obtenida coincide con la esperada según dicta la docstring de la función.
De esta forma, la docstring cumple dos funciones:
Documentación de la función.
Especificación de casos de prueba de la función.
# ejemplo.py
def factorial(n):
"""Devuelve el factorial de n, un número entero >= 0.
>>> [factorial(n) for n in range(6)]
[1, 1, 2, 6, 24, 120]
>>> factorial(30)
265252859812191058636308480000000
>>> factorial(-1)
Traceback (most recent call last):
...
ValueError: n debe ser >= 0
"""
import math
if not n >= 0:
raise ValueError("n debe ser >= 0")
result = 1
factor = 2
while factor <= n:
result *= factor
factor += 1
return result
$ python -m doctest ejemplo.py
$ python -m doctest ejemplo.py -v
Trying:
[factorial(n) for n in range(6)]
Expecting:
[1, 1, 2, 6, 24, 120]
ok
Trying:
factorial(30)
Expecting:
265252859812191058636308480000000
ok
Trying:
factorial(-1)
Expecting:
Traceback (most recent call last):
...
ValueError: n debe ser >= 0
ok
1 items had no tests:
ejemplo
1 items passed all tests:
3 tests in ejemplo.factorial
3 tests in 2 items.
3 passed and 0 failed.
Test passed.
pytest
pytest
es una herramienta que permite realizar
pruebas automáticas sobre una función o programa Python, pero de una
manera más general que con doctest
.
La forma más sencilla de usarla es crear una función llamada
test_
⟨nombre⟩ por
cada función ⟨nombre⟩ que
queramos probar.
Esa función test_
⟨nombre⟩ será la encargada de probar
automáticamente el funcionamiento correcto de la función ⟨nombre⟩.
Dentro de la función test_
⟨nombre⟩, usaremos la orden assert
para
comprobar si se cumple una determinada condición.
En caso de que no se cumpla, se entenderá que la función ⟨nombre⟩ no ha superado dicha prueba.
En Python 3, la herramienta se llama pytest-3
y se
instala mediante:
$ pytest-3
======================== test session starts =========================
platform linux -- Python 3.8.5, pytest-4.6.9, py-1.8.1, pluggy-0.13.0
rootdir: /home/ricardo/python
collected 1 item
test_ejemplo.py F [100%]
============================== FAILURES ==============================
___________________________ test_respuesta ___________________________
def test_respuesta():
> assert inc(3) == 5
E assert 4 == 5
E + where 4 = inc(3)
test_ejemplo.py:7: AssertionError
====================== 1 failed in 2.48 seconds ======================
pytest
sigue la siguiente estrategia a la hora de
localizar pruebas:
Si no se especifica ningún argumento, empieza a buscar recursivamente empezando en el directorio actual.
En esos directorios, busca todos los archivos
test_*.py
o *_test.py
.
En esos archivos, localiza todas las funciones cuyo nombre
empiece por test
.
Considérese la siguiente fórmula (debida a Herón de Alejandrı́a), que expresa el valor de la superficie S de un triángulo cualquiera en función de sus lados, a, b y c: S = \sqrt{\frac{a+b+c}{2}\left(\frac{a+b+c}{2}-a\right)\left(\frac{a+b+c}{2}-b\right)\left(\frac{a+b+c}{2}-c\right)}
Escribir una función que obtenga el valor S a partir de a, b y c, evitando el cálculo repetido del semiperímetro, sp = \frac{a+b+c}{2}, y almacenando el resultado finalmente en la variable S.
Escribir tres funciones que impriman las siguientes salidas en
función de la cantidad de líneas que se desean (␣
es un
espacio en blanco):
***** ␣* ␣␣␣␣*␣␣␣␣
***** ␣␣* ␣␣␣***␣␣␣
***** ␣␣␣* ␣␣*****␣␣
***** ␣␣␣␣* ␣*******␣
***** ␣␣␣␣␣* ␣␣␣␣*␣␣␣␣
Escribir una función para hallar \binom{n}{k}, donde n y k son datos enteros positivos,
mediante la fórmula \frac{n!}{(n-k)!k!}
mediante la fórmula \frac{n(n-1)\cdots(k+1)}{(n-k)!}
¿Qué ventajas presenta la segunda con respecto a la primera?