Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

En este capítulo profundizaremos sobre el manejo de variables en Python, contrastándolo con lo que ya conocemos de Go y Java. Aunque los conceptos fundamentales de variables son universales, Python introduce matices importantes en su gestión, especialmente en lo que respecta a la inmutabilidad de ciertos tipos de datos, los ámbitos de ejecución y la poderosa característica de las clausuras.

1Variables y asignación

En Go y Java, la declaración de variables a menudo implica especificar explícitamente el tipo de dato (aunque Go ofrece inferencia de tipos). Python, por otro lado, es un lenguaje de tipado dinámico. Esto significa que no se declara el tipo de una variable; el tipo se infiere en tiempo de ejecución según el valor que se le asigna.

Una diferencia clave es que en Python, las variables son referencias a objetos en memoria. Cuando se reasigna una variable, simplemente esa referencia pasa a apuntar a un objeto diferente, en lugar de cambiar el valor (esto es crucial para entender la inmutabilidad de ciertos tipos).

Cada vez que se asigna un valor a una variable, Python sigue los siguientes pasos:

  1. Crea un objeto en memoria (si no existe ya).

  2. Asigna una referencia a ese objeto.

Python garantiza que los pasos anteriores son atómicos, es decir se ejecutan uno tras otro sin interrupciones, lo que asegura la consistencia del estado de las variables en un entorno multihilo.

Si la variable ya tenía una referencia a otro objeto, esa referencia se pierde (el objeto anterior puede ser desalojado de la memoria por el recolector de basura si no hay otras referencias a él).

Asignación de Variables

Asignación de Variables

Asignación de Variables

Asignación de Variables

Esto contrasta con Go y Java, donde la asignación de una variable puede implicar la creación de una copia del valor (especialmente para tipos primitivos).

2Tipos de datos y mutabilidad

En Python todo es un objeto, por lo tanto podemos pensar que todas las variables son referencias a objetos en el heap. La distinción importante es si un objeto es mutable o inmutable.

2.1Tipos inmutables (como los “primitivos” en Java/Go)

Cuando a una variable que ya tenía asignado un objeto inmutable, se le asigna otro valor, en realidad se crea un nuevo objeto inmutable en memoria y la referencia anterior se pierde.

s1 = "hola"
s2 = s1
s1 += " mundo"  # Esto crea una nueva cadena "hola mundo"
                # y s1 ahora referencia a ella

print(f"s1: {s1}, s2: {s2}")
Output
s1: hola mundo, s2: hola
Inmutabilidad (Strings)

Inmutabilidad (Strings)

Inmutabilidad (Strings)

Inmutabilidad (Strings)

En este fragmento, s1 y s2 inicialmente referencian al mismo objeto, la cadena "hola". Al modificar s1, se crea un nuevo objeto cadena "hola mundo", y s1 ahora apunta a este nuevo objeto, mientras que s2 sigue apuntando al antiguo objeto "hola".

2.2Tipos mutables (como los objetos en Java/Go)

Cuando se modifica un objeto mutable, se altera el objeto en su lugar. Si múltiples variables referencian al mismo objeto mutable, todas verán los cambios.

lista1 = [1, 2, 3]
lista2 = lista1  # lista1 y lista2 referencian a la misma lista
lista1.append(4)  # Modifica la lista original

print(f"lista1: {lista1}, lista2: {lista2}")
Output
lista1: [1, 2, 3, 4], lista2: [1, 2, 3, 4]
Mutabilidad (Listas)

Mutabilidad (Listas)

Mutabilidad (Listas)

Mutabilidad (Listas)

En este caso, lista1 y lista2 referencian al mismo objeto lista. Al modificar lista1, lista2 refleja el cambio porque ambas variables apuntan al mismo objeto en memoria.

3Visibilidad de variables

En Python no existe el concepto de público, privado o protegido como en Java. En cambio, se utiliza una convención de nomenclatura para indicar la visibilidad de las variables:

Variables públicas
Se definen sin guiones bajos al inicio del nombre. Son accesibles desde cualquier parte del código (ej. mi_variable).
Variables protegidas
Se definen con un guion bajo al inicio del nombre (ej. _variable). Indica que la variable es para uso interno del módulo o clase, pero aún es accesible desde fuera.
Variables privadas
Se definen con dos guiones bajos al inicio del nombre (ej. __variable). Esto activa el name mangling, lo que significa que el nombre de la variable se modifica internamente para evitar conflictos con nombres en subclases.
Variables especiales
Se definen con dos guiones bajos al inicio y al final del nombre, estos son conocidos en la comunidad Python como dunder methods (dunders es un acrónimo de double underscore). Estas son utilizadas por Python para definir métodos especiales y no deben ser modificadas directamente. Por ejemplo, el método __init__ es el constructor de una clase.

4Ámbitos de ejecución (scopes): La regla LEGB

Python define un sistema de ámbitos para resolver nombres (variables, funciones, clases, etc.). Este sistema se conoce comúnmente como la regla LEGB:

Local (L)
Nombres definidos dentro de una función.
Enclosing (E) / Clausura
Nombres en el ámbito de una función externa (función “envolvente”). Este ámbito define el contexto de ejecución de una función anidada.
Global (G)
Nombres definidos en el nivel superior de un módulo (archivo .py).
Built-in (B)
Nombres preasignados por Python (ej. open, range, print).

Cuando Python busca un nombre, sigue este orden: primero busca en el ámbito Local, luego en el Enclosing, después en el Global y finalmente en el Built-in.

Ámbitos de Ejecución

Ámbitos de Ejecución

Ámbitos de Ejecución

Ámbitos de Ejecución

4.1Ámbito Local (L)

Las variables definidas dentro de una función son locales a esa función. Esto significa que solo son accesibles dentro de la función y no pueden ser accedidas desde fuera de ella. Una vez que la función termina su ejecución, las variables locales se eliminan de la memoria.

Si hay variables globales definidas con el mismo nombre de las variables locales, entonces las locales ocultan las globales. Esto se conoce como Ocultamiento de variables (shadowing).

mensaje = "Hola desde el ámbito global"  # Variable global


def mi_funcion():
    mensaje = "Hola desde la función"  # Variable local
    print(mensaje)


mi_funcion()  # Llama a la función que imprime el mensaje local

print(mensaje)  # Acceso a la variable global
Output
Hola desde la función
Hola desde el ámbito global

La variable local mensaje dentro de mi_funcion oculta la variable global del mismo nombre. Cuando se llama a mi_funcion, imprime el mensaje local, mientras que fuera de la función se accede a la variable global.

Si se necesita modificar una variable global desde dentro de una función, se debe usar la palabra clave global para indicar que se quiere referenciar a la variable global.

mensaje = "Hola desde el ámbito global"  # Variable global


def mi_funcion():
    global mensaje  # Indica que se quiere usar la variable global
    mensaje = "Hola desde la función"  # Modifica la variable global
    print(mensaje)


mi_funcion()  # Llama a la función que modifica el mensaje global

print(mensaje)  # Acceso a la variable global modificada
Output
Hola desde la función
Hola desde la función

4.2Ámbito Enclosing (E) / Clausuras

En Python, las funciones son ciudadanos de primera clase, lo que significa que Python las trata como a un objeto más y por lo tanto se pueden asignar funciones a variables, pasarlas como argumentos y retornarlas desde otras funciones y también se pueden anidar, esto es definir funciones dentro de otras funciones.

Esta versatilidad de poder definir una función dentro de otra lleva a las clausuras, que son una característica poderosa y distintiva de Python. Las clausuras permiten que una función anidada acceda a variables del ámbito de su función envolvente, incluso después de que la función envolvente haya terminado su ejecución.

Para que una función sea una clausura, debe cumplir dos condiciones:

def fabrica_incrementos(y):
    def incrementar(x):
        return x + y  # y está encapsulada en la función interna

    return incrementar  # Retorna la función interna


incrementar_2 = fabrica_incrementos(2) # Crea una función que incrementa en 2

print(incrementar_2(5))
Output
7

Al ejecutar el fragmento anterior ocurre lo siguiente:

  1. Se define la función fabrica_incrementos que recibe un parámetro y. El código de la función se guarda en memoria (como si fuera una valor más en la memoria). El nombre de la función fabrica_incrementos se guarda en el ámbito global; esta será la referencia que permite acceder al objeto de tipo función.

  2. Luego se invoca fabrica_incrementos(2) y el resultado de esa operación (la función interna incrementar) se va a asignar a la variable incrementar_2. En este momento, y tiene el valor 2 y se guarda en la clausura de la función interna incrementar.

  3. El valor devuelto por fabrica_incrementos es una función que queda ligada a la variable incrementar_2. incrementar_2 contiene el valor de y, al momento de su creación, en su clausura. Esto significa que incrementar_2 “recuerda” el valor de y aunque fabrica_incrementos ya haya terminado su ejecución.

  4. Finalmente se ejecuta incrementar_2 con un parámetro x y retorna la suma de x más y. Si bien fabrica_incrementos ya ha terminado su ejecución y por lo tanto los valores de sus parámetros no están en la memoria, la referencia a y se mantiene en la clausura. La función realiza la operación 5 + 2, donde 5 es el valor ligado al parámetro x y 2 es el valor de y, al momento de la creación de incrementar_2 que se guardó en la clausura.

  5. incrementar_2(5) retorna 7 al ámbito global, y print lo muestra en la salida.

4.3Ámbito Global (G)

El ámbito global se refiere a las variables definidas en el nivel superior de un módulo. Estas variables son accesibles desde cualquier parte del módulo, incluidas las funciones.

Al declarar un módulo se puede incluir variables y constantes globales que pueden ser utilizadas en todo el código del módulo. A modo de ejemplo podemos ver las constantes matemáticas definidas en el módulo math, como math.pi o math.e.

import math  # Importa el módulo math

print("Constantes matemáticas:")
print(math.pi)  # Imprime el valor de pi
print(math.e)  # Imprime el valor de e
print(math.tau)  # Imprime el valor de tau
print(math.inf)  # Imprime el valor de infinito
print(math.nan)  # Imprime el valor de NaN (Not a Number)
Output
Constantes matemáticas:
3.141592653589793
2.718281828459045
6.283185307179586
inf
nan

Para definir un módulo propio se crea un archivo con extensión .py y se pueden definir variables y funciones que serán accesibles desde otros módulos al importarlos. El nombre del módulo es el nombre del archivo sin la extensión .py.

A modo de ejemplo, se muestra un módulo simple que implementa una pila (stack) utilizando una lista y objetos. Más adelante veremos en detalle la Programación Orientada a Objetos (POO) en Python, pero aquí se muestra un ejemplo de un módulo y como se documenta cada parte del código.

Click para ver el código
stack.py
"""
Módulo Stack - Implementación de Pila (LIFO)

Este módulo proporciona una implementación de la estructura de datos pila
(stack)
utilizando el principio LIFO (Last In, First Out - Último en entrar, primero
en salir).

La pila es una estructura de datos lineal que permite insertar y eliminar
elementos
únicamente desde un extremo llamado "cima" o "tope".

Ejemplo de uso:
    >>> from stack import Stack
    >>> pila = Stack()
    >>> pila.push(10)
    >>> pila.push(20)
    >>> print(pila.peek())  # 20
    >>> print(pila.pop())   # 20
    >>> print(pila.size())  # 1

Clases:
    Stack: Implementación de pila usando lista interna de Python.
"""

from stack_exception import StackException


class Stack:
    """
    Clase Stack - Implementación de una pila (stack) usando lista interna.

    Una pila es una estructura de datos que sigue el principio LIFO
    (Last In, First Out). Los elementos se agregan y eliminan desde
    la misma posición llamada "cima" o "tope".

    Atributos:
        _items (list): Lista interna que almacena los elementos de la pila.
                      Se usa convención de nombre privado con underscore.

    Operaciones principales:
        - push: Agregar elemento a la cima
        - pop: Eliminar y retornar elemento de la cima
        - peek/top: Ver elemento de la cima sin eliminarlo
        - is_empty: Verificar si está vacía
        - size: Obtener número de elementos

    Complejidad temporal:
        - Todas las operaciones son O(1) - tiempo constante

    Ejemplo:
        >>> stack = Stack()
        >>> stack.push(1)
        >>> stack.push(2)
        >>> stack.push(3)
        >>> print(stack.pop())  # 3
        >>> print(stack.peek()) # 2
    """

    def __init__(self):
        """
        Inicializa una pila vacía.

        Crea una nueva instancia de Stack con una lista interna vacía
        para almacenar los elementos.

        Complejidad temporal: O(1)
        Complejidad espacial: O(1)

        Ejemplo:
            >>> stack = Stack()
            >>> print(stack.is_empty())  # True
            >>> print(stack.size())      # 0
        """
        self._items = []

    def push(self, item):
        """
        Agrega un elemento a la cima de la pila.

        Inserta el elemento dado en la posición superior de la pila.
        Esta operación siempre es exitosa y no tiene restricciones
        de capacidad (limitada solo por la memoria disponible).

        Args:
            item: El elemento a agregar a la pila. Puede ser de cualquier tipo.

        Returns:
            None

        Complejidad temporal: O(1)
        Complejidad espacial: O(1)

        Ejemplo:
            >>> stack = Stack()
            >>> stack.push(10)
            >>> stack.push("texto")
            >>> stack.push([1, 2, 3])
            >>> print(stack.size())  # 3
        """
        self._items.append(item)

    def pop(self):
        """
        Elimina y retorna el elemento de la cima de la pila.

        Remueve el elemento que está en la posición superior de la pila
        y lo retorna. La pila se reduce en un elemento.

        Returns:
            El elemento que estaba en la cima de la pila.

        Raises:
            StackException: Si se intenta hacer pop en una pila vacía.

        Complejidad temporal: O(1)
        Complejidad espacial: O(1)

        Ejemplo:
            >>> stack = Stack()
            >>> stack.push(10)
            >>> stack.push(20)
            >>> elemento = stack.pop()
            >>> print(elemento)      # 20
            >>> print(stack.size())  # 1

            # Caso de error:
            >>> stack_vacia = Stack()
            >>> stack_vacia.pop()    # Lanza StackException
        """
        if self.is_empty():
            raise StackException("pop from empty stack")
        return self._items.pop()

    def peek(self):
        """
        Retorna el elemento de la cima sin eliminarlo.

        Permite ver el elemento que está en la posición superior
        de la pila sin modificar la estructura. También conocido
        como operación 'top'.

        Returns:
            El elemento que está en la cima de la pila.

        Raises:
            StackException: Si se intenta hacer peek en una pila vacía.

        Complejidad temporal: O(1)
        Complejidad espacial: O(1)

        Ejemplo:
            >>> stack = Stack()
            >>> stack.push(10)
            >>> stack.push(20)
            >>> elemento = stack.peek()
            >>> print(elemento)      # 20
            >>> print(stack.size())  # 2 (no se eliminó)

            # Caso de error:
            >>> stack_vacia = Stack()
            >>> stack_vacia.peek()   # Lanza StackException
        """
        if self.is_empty():
            raise StackException("peek from empty stack")
        return self._items[-1]

    def is_empty(self):
        """
        Verifica si la pila está vacía.

        Determina si la pila no contiene ningún elemento.
        Es útil para validar antes de realizar operaciones
        que requieren elementos (como pop o peek).

        Returns:
            bool: True si la pila está vacía, False en caso contrario.

        Complejidad temporal: O(1)
        Complejidad espacial: O(1)

        Ejemplo:
            >>> stack = Stack()
            >>> print(stack.is_empty())  # True
            >>> stack.push(10)
            >>> print(stack.is_empty())  # False
            >>> stack.pop()
            >>> print(stack.is_empty())  # True
        """
        return len(self._items) == 0

    def size(self):
        """
        Retorna el número de elementos en la pila.

        Obtiene la cantidad actual de elementos almacenados
        en la pila. Es útil para conocer el estado de la
        estructura de datos.

        Returns:
            int: Número de elementos en la pila (>= 0).

        Complejidad temporal: O(1)
        Complejidad espacial: O(1)

        Ejemplo:
            >>> stack = Stack()
            >>> print(stack.size())  # 0
            >>> stack.push(10)
            >>> stack.push(20)
            >>> print(stack.size())  # 2
            >>> stack.pop()
            >>> print(stack.size())  # 1
        """
        return len(self._items)

    def __str__(self):
        """
        Representación en string de la pila.

        Proporciona una representación legible de la pila
        mostrando los elementos desde la base hasta la cima.

        Returns:
            str: Representación en cadena de la pila.

        Ejemplo:
            >>> stack = Stack()
            >>> stack.push(1)
            >>> stack.push(2)
            >>> stack.push(3)
            >>> print(stack)  # Stack: [1, 2, 3] (cima: 3)
        """
        if self.is_empty():
            return "Stack: [] (vacía)"
        return f"Stack: {self._items} (cima: {self._items[-1]})"

    def __repr__(self):
        """
        Representación técnica de la pila.

        Proporciona una representación que puede usarse
        para recrear el objeto.

        Returns:
            str: Representación técnica del objeto.
        """
        return f"Stack({self._items})"


def demo_stack():
    """
    Función de demostración del uso de la clase Stack.

    Muestra ejemplos de todas las operaciones disponibles
    y casos de uso típicos de una pila.
    """
    print("=== Demostración de la clase Stack ===\n")

    # Crear pila vacía
    stack = Stack()
    print(f"Pila creada: {stack}")
    print(f"¿Está vacía?: {stack.is_empty()}")
    print(f"Tamaño: {stack.size()}\n")

    # Agregar elementos
    print("Agregando elementos (push):")
    elementos = [10, 20, 30, "Python", [1, 2, 3]]
    for elemento in elementos:
        stack.push(elemento)
        print(f"  Push {elemento} -> {stack}")

    print(f"\nTamaño final: {stack.size()}")
    print(f"Elemento en la cima (peek): {stack.peek()}\n")

    # Eliminar elementos
    print("Eliminando elementos (pop):")
    while not stack.is_empty():
        elemento = stack.pop()
        print(f"  Pop -> {elemento}, pila restante: {stack}")

    print(f"\n¿Está vacía?: {stack.is_empty()}")

    # Demostrar manejo de errores
    print("\n=== Manejo de errores ===")
    try:
        stack.pop()
    except StackException as e:
        print(f"Error al hacer pop en pila vacía: {e}")

    try:
        stack.peek()
    except StackException as e:
        print(f"Error al hacer peek en pila vacía: {e}")


if __name__ == "__main__":
    demo_stack()

En la función demo_stack se muestra cómo se puede utilizar el módulo stack para crear una pila, agregar elementos y eliminarlos.

Es común que los módulos tengan un bloque de código al final que se ejecuta solo si el módulo se ejecuta directamente, no cuando se importa. Esto se logra utilizando la siguiente estructura:

if __name__ == "__main__":
    demo_stack()

Si el módulo se importa desde otro módulo, el bloque if __name__ == "__main__": no se ejecuta, lo que permite que el código de demostración no interfiera con el uso del módulo como biblioteca.

4.4Ámbito Built-in (B)

El ámbito built-in contiene nombres predefinidos por Python, como funciones y excepciones que están disponibles en todos los módulos (sin necesidad de importarlos). Estos nombres son parte del núcleo del lenguaje y se pueden utilizar directamente en cualquier parte del código. Algunos ejemplos son print, len, range, int, str, entre otros.

Si se intenta redefinir un nombre built-in, se creará una variable local o global que ocultará temporalmente el nombre built-in, pero no se eliminará del ámbito built-in.

print(len("Hola"))  # Llama a la función built-in len


def mi_funcion():
    len = 4
    print(len("Mundo"))  # Error


mi_funcion()  # Llama a la función que imprime la longitud de "Mundo"
4
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
Cell In[7], line 9
      5     len = 4
      6     print(len("Mundo"))  # Error
      7 
      8 
----> 9 mi_funcion()  # Llama a la función que imprime la longitud de "Mundo"

Cell In[7], line 6, in mi_funcion()
      4 def mi_funcion():
      5     len = 4
----> 6     print(len("Mundo"))  # Error

TypeError: 'int' object is not callable

5Recursos para profundizar