Funciones en Python

En las matemáticas, una función es un mapeo de valores en un dominio determinado (variable independiente) con valores en un rango (variable dependiente), de forma tal que para cada valor en el dominio de entrada, la función genera como resultado un valor único en el rango de valores de salida. Este concepto resulta útil cuanto vamos a analizar las funciones en Python, donde las funciones, además de mapear los argumentos de entrada concretos con valores de retorno únicos, nos permitirán asignar nombres genéricos a porciones de código cuyo objetivo se puede resumir en el nombre asignado.

Las funciones son un elemento de suma importancia para la mejor organización, legibilidad y reusabilidad del código. Un fragmento de código que se repite más de una vez, debería ser convertido en una función que pueda ser llamada desde todos los lugares en que sea necesario emplear este fragmento de código. De esta forma mejoraremos la mantenibilidad y sobre todo la reusabilidad de nuestra base de código.

Uno de los primeros pasos en la escritura de funciones es la selección de un nombre adecuado, que refleje el propósito específico de la función, también debemos ser cuidadosos a la hora de seleccionar los nombres de los parámetros de la función, pues estos pueden brindarnos información extra para comprender el funcionamiento del código. Ejemplos de esto los podemos encontrar en el propio Python, con funciones como print(), type(), len(), etc.

Funciones y Métodos

Las funciones simples se definen generalmente a nivel de módulo (aunque también pueden definirse embebidas en otras funciones) y pueden ser empleadas directamente sin necesidad de crear algún objeto antes. Los métodos simples no son más que funciones con características especiales; primero: se definen siempre en el interior o cuerpo de una clase y segundo: deberán incluir un primer parámetro, que en Python acostumbramos a denominar self (aunque esto es solo una convención, recomendamos apegarse siempre a ella, para que nuestro código luzca cómo código Python a los ojos de otros programadores), y que representa al objeto implícito de la clase. De esta forma, para poder acceder a un método determinado, primeramente deberemos crear un objeto de la clase donde se ha definido el método en cuestión.

Existen además, otros tipos de métodos, como por ejemplo, los métodos estáticos y los de métodos de clase, que aunque siempre se definen en una clase determinada, no incluyen a self como su primer parámetro, sino que prescinden totalmente de este (métodos estáticos) o incluyen a la propia clase como primer parámetro (métodos de clase). En estos casos, podemos acceder a ellos a través de la propia clase o a través de una instancia u objeto de esta.

Definición de Funciones

En Python, las funciones se definen empleando la palabra clave def para iniciar la cabecera de la función, que continúa con el nombre de función, un listado de parámetros (0 o más) separados por coma “,” y entre paréntesis “()”, y concluye con el carácter de dos puntos “:” como toda cabecera de sentencia compuesta. Seguidamente se incluye el cuerpo o bloque de la función, que deberá ubicarse en el próximo nivel de indentación.

def function_name([param0, param1, ...]):

    # Cuerpo de la función en el siguiente nivel de indentación

    # Aquí se procesan los parámetros y se computa el resultado (result)

    [return result] # Valor de retorno o resultado de la función

En este caso los corchetes no son parte de la sintaxis, solo indican que la lista de parámetros es opcional, es decir, la función puede tener o no, una lista de parámetros de entrada. También es opcional el valor de retorno que no es más que el valor resultante de la ejecución de la función.

Es importante aclarar que al definir una función lo que hacemos es asociar un nombre único al fragmento de código que conforma la función. Este nombre estará disponible en nuestro espacio de nombres, de forma tal que podremos ejecutar el código más tarde a través de la llamada a la función. Esto significa que el código contenido en la función solo se ejecuta si llamamos a la función explícitamente. Ahora bien, para llamar a una función solo necesitamos conocer su nombre y tener una lista válida de argumentos para pasarle, es decir:

function_name(value_for_param0, value_for_param1, …)

La lista de parámetros de una función suele llamarse firma o signature.

Valor de Retorno

Las funciones en Python siempre tendrán un valor de retorno, ya sea explícito o implícito. Los valores de retorno explícitos son aquellos que devolvemos empleando la sentencia return en el cuerpo de la función y no son más que el resultado final de procesar los parámetros de entrada. Por ejemplo:

>>> def square(base): # Cabecera que define la función square con parámetro base

... return base ** 2 # Calcula el cuadrado de base y lo devuelve explícitamente

...

>>> square(4) # Llamada de función

16 # Valor retornado

Con frecuencia la función que estamos implementando no tiene como objetivo retornar un valor computado, sino que se limita a realizar una secuencia de operaciones lógicas que no devuelven valor alguno. En muchos lenguajes este tipo de función se denomina procedimiento para diferenciarlo de las funciones puras. En Python, solo tenemos funciones, por tanto, siempre tendrán un valor de retorno. Si en el cuerpo de la función no declaramos un valor de retorno concreto, Python se encarga de suministrarlo y este siempre será None. Veamos un ejemplo:

>>> def no_explicit_return():

... print('This function do not have explicit return value')

...

>>> rv = no_explicit_return()

This function do not have explicit return value

>>> print(rv)

None

Como vemos, aunque no hemos devuelto ningún valor de manera explícita, recibimos None como resultado, de lo cual se ha encargado Python internamente.

Es preciso notar que la palabra clave return tiene dos propósitos fundamentales, el primero es finalizar la ejecución de la función y el segundo, retornar el valor que resulte de la ejecución de la función. En ocasiones necesitamos interrumpir la ejecución de una función si se cumple determinada condición, dejando de procesar parte del cuerpo de la función. En estos casos, es común emplear la sentencia return sin expresión o valor de retorno explicito, con lo cual estaremos truncando la ejecución de la función y por demás, obtendremos None como valor de retorno por defecto.

El valor de retorno de una función puede ser único (return result) o puede ser una tupla que contenga varios elementos (return result0, result1, ...), lo cual resulta válido para asignación múltiple.

Parámetros, Argumentos y Asignación

Con el ánimo de aclarar términos, debemos decir que, para los fines que nos ocupan, los parámetros son los nombres de variables que listamos en la cabecera de definición de las funciones y que de una forma u otra empleamos directamente en el interior de la función. Por otro lado, los argumentos son objetos concretos que empleamos en la llamada a funciones. En Python, los argumentos se pasan siempre por asignación, es decir, el argumento es asignado al nombre de parámetro correspondiente en el momento de la llamada. Por tanto, no existe en Python, el paso de argumentos por valor y/o por referencia, como en otros lenguajes.

Otro elemento a destacar es que la asignación de nuevos valores a los parámetros dentro de la función, no afecta al código cliente a no ser que se trate de argumentos de tipo mutable y la función solo modifique sus componentes internos. Por ejemplo, si definimos una función como:

def func(param):

    …

y en el cuerpo de la función hacemos algo como:

    param = 'New value'

estaremos asignado un nuevo valor a la variable local param y perderemos la referencia al valor originalmente asignado a param por el código cliente o llamante. Si por otro lado, el objeto asignado a param por el llamante es de tipo mutable (Ej. una lista) y hacemos algo como:

    param[1] = 'New value'

entonces, estaremos modificando el objeto concreto que el llamante pasó a la función y por tanto, el llamante se verá impactado por la modificación. Este tipo de proceder es poco recomendable, pues puede conducir a comportamientos confusos (objetos modificados por varias funciones en momentos distintos, lo que hace difícil el seguimiento del estado actual del objeto) y atentará directamente contra la mantenibilidad del código.

>>> def func(param):

... param[1] = 'New value'

...

>>> l = [1, 2]

>>> func(l)

>>> l

[1, 'New value']

Para evitar este comportamiento, podemos optar por dos posibles soluciones:

  1. Copiar el objeto mutable en la llamada, de la forma:

>>> func(l[:])

  1. Reasignar una copia del objeto mutable en el cuerpo de la función:

>>> def func(param):

param = param[:]

param[1] = 'New value'

Argumentos Posicionales

La forma por defecto para aceptar argumentos de entrada en una función es a través de los llamados argumentos posicionales. La asignación de argumentos concretos a los parámetros se hace a partir de su posición relativa en la lista de parámetros declarada en la definición de la función y siempre debemos suministrar tantos argumentos como parámetros hemos definido. Por ejemplo, si tenemos una función declarada como def func(param0, param1): y la llamamos de la forma func(10, 15) estaremos asignando el valor 10 al parámetro param0, y 15, a param1. Si bien esta es la forma más elemental de lidiar con parámetros en una función, tiende a ser confusa cuando tenemos funciones con muchos parámetros de entrada. Por esta razón, es recomendable emplear siempre los llamados argumentos keyword en la llamada a funciones. Esta práctica tiene un impacto inmediato en la legibilidad del código, y además, agrega flexibilidad y nos evita tener que recordar la posición exacta de los parámetros en la cabecera de la función. Digamos, por ejemplo, que tenemos la función siguiente:

def new_email(to, subject):

    # procesar nuevo e-mail aquí...

podemos llamarla de la forma:

new_email(“ej@ejemplo.cu“, “Ejemplo”) # Llamada con argumentos posicionales

o de la forma:

new_email(to=“ej@ejemplo.cu“, subject=“Ejemplo”) # Llamada con argumentos keyword

Esta última variante, además de ser más legible, nos permite cambiar el orden de los argumentos, es decir, también podríamos escribir:

new_email(subject=“Ejemplo”, to=“ej@ejemplo.cu“)

Esto es posible, puesto que en este caso el intérprete no debe “decidir” basándose en la posición relativa, a qué parámetro asignarle qué argumento, sino que ya nosotros mismos le estamos informando directamente cómo debe proceder. En este caso se dice que la asignación es por nombre.

Parámetros Opcionales o Por Defecto

Python nos permite asignar valores por defecto a los parámetros. Esto se logra, asignado el valor deseado en la propia definición de la función, de la forma def func(param=0):. Al suministrar valores por defecto a los parámetros de una función, estamos abriendo la posibilidad de omitir los argumentos correspondientes en la llamada a la función.

>>> def hello(target='Mundo!'):

... print('Hola', target)

...

>>> hello() # Llamamos a la función sin argumentos

Hola Mundo!

>>> hello('Python!') # Ahora con argumentos

Hola Python!

Como podemos apreciar en el ejemplo anterior, cuando definimos una función con parámetros por defecto, podemos prescindir de argumentos para estos parámetros en la llamada, pues Python automáticamente les asigna el valor por defecto. De esta forma los parámetros se vuelven opcionales.

Python ofrece un sinnúmero de ejemplos de funciones definidas con parámetros por defecto u opcionales, tanto funciones integradas (built-ins), como en la librería estándar. Por ejemplo, la función print() se define como:

print(*objects, sep=' ', end='\n', file=sys.stdout, flush=False)

Lo que implica que el separador por defecto es el espacio, la salida termina con el carácter de nueva línea por defecto y el archivo de salida por defecto es la pantalla, representada en este caso por sys.stdout y así sucesivamente.

Es necesario resaltar que no debemos confundir la sintaxis name=value en la cabecera y en la llamada a funciones. En la primera, nos sirve para definir parámetros opcionales o por defecto, mientras que en la segunda denotan el uso de argumentos keyword o asignación por nombre.

Por último, y a modo de recomendación, aconsejamos evitar el empleo de tipos de datos mutables como parámetro por defecto, pues este proceder puede llevar a comportamientos erráticos del código. Por tanto, evitemos el empleo de listas, diccionarios, conjuntos y cualquier otro tipo mutable, como valores por defecto.

Cantidad Variable de Argumentos Posicionales (*args)

Otra opción que nos ofrece Python es la de poder realizar llamadas a una función con un número variable e indefinido de argumentos posicionales. Esto es posible empleando el prefijo *, seguido de un identificador genérico, que por convención casi siempre se denomina args, aunque puede tomar cualquier nombre. Python agrupa en args (que no es más que una tupla) a todos los argumentos posicionales extra que le pasemos a la función en la llamada. Veamos cómo funciona esto:

>>> def total(*args):

... return sum(args)

...

>>> total(15, 25, 32, 44) # Llamada con cuatro argumentos

116

>>> total(77, 2, 21) # Llamada con tres argumentos

100

Cantidad Variable de Argumentos keyword (**kwargs)

Igualmente, es posible llamar a una función con un número indefinido de argumentos keyword, lo que se puede lograr con el prefijo **, seguido de un identificador que por convención llamaremos kwargs, pero que al igual que args, puede tomar cualquier nombre. Python agrupa en kwargs (que representa un diccionario) todos los argumentos keyword extra que le pasemos a la función. Al kwargs quedar representado por un diccionario, es posible procesarlo como cualquier diccionario estándar.

>>> def func(**kwargs):

... print(kwargs)

...

>>> func(name='Juan', last_name='Pérez', age=25)

{'name': 'Juan', 'age': 25, 'last_name': 'Pérez'}

Argumentos keyword Exclusivos (Python 3.x)

Finalmente, es posible obligar al código cliente, o llamante, a emplear siempre llamadas con argumentos keyword o pasados por nombre. Para esto debemos emplear la sintaxis siguiente:

>>> def hello(*, target):

... print('Hola', target)

...

>>> hello(target='Python!') # Llamada correcta usando argumento keyword

Hola Python!

>>> hello('Python!') # Llamada incorrecta sin argumento keyword

Traceback (most recent call last):

File "<stdin>", line 1, in <module>

TypeError: hello() takes 0 positional arguments but 1 was given

En el caso de los prefijos * y **, aclaramos que en la cabecera de la función estos prefijos recolectan los argumentos en exceso y los agrupan en una tupla o un diccionario respectivamente. En la llamada, un argumento prefijado con *, desempaca una secuencia en varios argumentos posicionales y un argumento con el prefijo **, desempaca un diccionario en un conjunto de argumentos keyword.

Reglas de Asignación de Argumentos

Las reglas generales que emplea Python para la asignación de argumentos a parámetros en la llamada de funciones son:

  1. Asigna todos los argumentos posicionales a los parámetros de izquierda a derecha

  2. Si hay más argumentos posicionales que parámetros y:

  1. Si existen un parámetro con el prefijo *, se asigna a este un tupla con los argumentos restantes

  2. Si no existe un parámetro con el prefijo *, se lanza la excepción TypeError

  1. Asigna todos los argumentos keyword por su nombre. En caso de que existan argumentos ya asignados como posicionales y se repitan como keyword, se lanza la excepción TypeError

  2. Si hay más argumentos keyword que parámetros y:

  1. Si existe un parámetro con el prefijo **, se asigna a este un diccionario con el resto de los argumentos keyword

  2. Si no existe un parámetro con el prefijo **, se lanza una excepción de tipo TypeError

  1. Aplicar los valores por defecto a los parámetros que falten en la llamada si están definidos como opcionales en la cabecera de la función

  2. En caso de que queden parámetros sin valor asignado, se lanza la excepción TypeError

  3. Todos los parámetros que aparezcan a continuación del prefijo *, serán asignados exclusivamente por nombre, de lo contrario se lanza una excepción TypeError.

Orden de Parámetros y Argumentos

Para lograr un empleo sintácticamente correcto de parámetros y argumentos, estos deben seguir un orden de aparición específico, tanto en la cabecera, como en la llamada de funciones. Ese orden debe ser:

  • Parámetros en la cabecera. En primer lugar los posicionales, seguidos de los parámetros opcionales o por defecto, luego los prefijados con *, seguidos de cualquier parámetro de tipo keyword exclusivo, y finalmente los prefijados con **

  • Argumentos en la llamada. Primeramente los posicionales, seguidos de los keyword, luego los prefijados con *, y finalmente los prefijados con **.

Espacio de Nombres Local

Siempre que estamos lidiando con funciones, debemos tener en cuenta que cada función que definamos enmarca un espacio de nombres local. Esto significa que los nombre que creemos dentro de una definición de función, son locales a esta (incluidos los parámetros) y dejarán de existir inmediatamente después que concluya la ejecución de la función.

Por otro lado, si necesitamos modificar una variable global en el interior de una función, debemos emplear la palabra clave global (o nonlocal si la función se ha definido entro de otra función y esta contiene la variable a modificar), de lo contrario cuando tratemos de asignar un nuevo valor a la variable global en realidad estaremos creado una nueva variable local.

>>> x = 10

>>> def func():

... x = 15

... print(x)

...

>>> func()

15

>>> x

10

>>> def func1():

... global x

... x = 15

... print(x)

...

>>> func1()

15

>>> x

15

En este ejemplo, la función func() no modifica la variable global x, sino que crea una variable x local a la función. En func1() por otro lado, le estamos diciendo a la función que usaremos la variable global x y que la modificaremos asignándole un nuevo valor (15). Este tipo de proceder, aunque es perfectamente válido en Python, no es recomendable y puede crearnos serios problemas y llevarnos a errores difíciles de identificar y corregir. Por tanto, recomendamos evitar la modificación de variables globales desde el interior de funciones, es más saludable emplear las funciones de manera tradicional, es decir, pasarle un conjunto de argumentos de entrada y recibir el valor resultante.

Lecturas Recomendadas

Para profundizar en el estudio de estos temas recomendamos la lectura del Capítulo 7. “Basic Function Definitions” del libro “Python Essentials” por Steven F. Lott, publicado por la Editorial “Pakt Publishing”. Además, el Capítulo 18. “Arguments” del libro “Learning Python” por Mark Lutz, publicado por la Editorial “O’Reilly”.

Muy bien, esto es todo por ahora, si este artículo te resultó interesante y/o útil, compártelo para que otros también puedan acceder a él. Déjanos tus comentarios y podremos mejorar nuestros contenidos.

Gracias de antemano,

lpozo

2 comentarios

    • James on 6 marzo, 2018 at 4:35 pm

    Muchas gracias, me has refrescado los conocimientos

    1. Por nada, esa es la intención…
      saludos,
      lpozo

Los comentarios han sido desactivados.