Reglas y trucos de alcance (scoping)

PythonPythonBeginner
Practicar Ahora

This tutorial is from open-source community. Access the source code

💡 Este tutorial está traducido por IA desde la versión en inglés. Para ver la versión original, puedes hacer clic aquí

Introducción

En este laboratorio, aprenderás sobre las reglas de alcance (scoping) de Python y explorarás técnicas avanzadas para trabajar con el alcance. Comprender el alcance en Python es fundamental para escribir código limpio y mantenible, y ayuda a evitar comportamientos inesperados.

Los objetivos de este laboratorio incluyen comprender en detalle las reglas de alcance de Python, aprender técnicas prácticas de alcance para la inicialización de clases, implementar un sistema flexible de inicialización de objetos y aplicar técnicas de inspección de marcos (frame inspection) para simplificar el código. Trabajarás con los archivos structure.py y stock.py.


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL python(("Python")) -.-> python/FunctionsGroup(["Functions"]) python(("Python")) -.-> python/ObjectOrientedProgrammingGroup(["Object-Oriented Programming"]) python/FunctionsGroup -.-> python/function_definition("Function Definition") python/FunctionsGroup -.-> python/arguments_return("Arguments and Return Values") python/FunctionsGroup -.-> python/scope("Scope") python/ObjectOrientedProgrammingGroup -.-> python/classes_objects("Classes and Objects") python/ObjectOrientedProgrammingGroup -.-> python/constructor("Constructor") subgraph Lab Skills python/function_definition -.-> lab-132510{{"Reglas y trucos de alcance (scoping)"}} python/arguments_return -.-> lab-132510{{"Reglas y trucos de alcance (scoping)"}} python/scope -.-> lab-132510{{"Reglas y trucos de alcance (scoping)"}} python/classes_objects -.-> lab-132510{{"Reglas y trucos de alcance (scoping)"}} python/constructor -.-> lab-132510{{"Reglas y trucos de alcance (scoping)"}} end

Comprender el problema con la inicialización de clases

En el mundo de la programación, las clases son un concepto fundamental que te permite crear tipos de datos personalizados. En ejercicios anteriores, es posible que hayas creado una clase Structure. Esta clase es una herramienta útil para definir fácilmente estructuras de datos. Una estructura de datos es una forma de organizar y almacenar datos para que se puedan acceder y utilizar de manera eficiente. La clase Structure, como clase base, se encarga de inicializar atributos basados en una lista predefinida de nombres de campos. Los atributos son variables que pertenecen a un objeto, y los nombres de campos son los nombres que le damos a estos atributos.

Echemos un vistazo más detallado a la implementación actual de la clase Structure. Para hacer esto, necesitamos abrir el archivo structure.py en el editor de código. Este archivo contiene el código de la clase Structure. Aquí están los comandos para navegar al directorio del proyecto y abrir el archivo:

cd ~/project
code structure.py

La clase Structure proporciona un marco básico para definir estructuras de datos simples. Cuando creamos una subclase, como la clase Stock, podemos definir los campos específicos que queremos para esa subclase. Una subclase hereda las propiedades y métodos de su clase base, en este caso, la clase Structure. Por ejemplo, en la clase Stock, definimos los campos name, shares y price:

class Stock(Structure):
    _fields = ('name', 'shares', 'price')

Ahora, abramos el archivo stock.py para ver cómo se implementa la clase Stock en el contexto del código general. Es probable que este archivo contenga el código que utiliza la clase Stock e interactúa con ella. Utiliza el siguiente comando para abrir el archivo:

code stock.py

Aunque este enfoque de usar la clase Structure y sus subclases funciona, tiene varias limitaciones. Para identificar estos problemas, ejecutaremos el intérprete de Python y exploraremos cómo se comporta la clase Stock. El siguiente comando importará la clase Stock y mostrará su información de ayuda:

python3 -c "from stock import Stock; help(Stock)"

Cuando ejecutes este comando, notarás que la firma mostrada en la salida de ayuda no es muy útil. En lugar de mostrar los nombres reales de los parámetros como name, shares y price, solo muestra *args. Esta falta de nombres de parámetros claros dificulta que los usuarios comprendan cómo crear correctamente una instancia de la clase Stock.

Intentemos también crear una instancia de Stock utilizando argumentos de palabra clave. Los argumentos de palabra clave te permiten especificar los valores de los parámetros por sus nombres, lo que puede hacer el código más legible. Ejecuta el siguiente comando:

python3 -c "from stock import Stock; s = Stock(name='GOOG', shares=100, price=490.1); print(s)"

Deberías obtener un mensaje de error como este:

TypeError: __init__() got an unexpected keyword argument 'name'

Este error se produce porque nuestro método __init__ actual, que es responsable de inicializar objetos de la clase Stock, no maneja argumentos de palabra clave. Solo acepta argumentos posicionales, lo que significa que debes proporcionar los valores en un orden específico sin usar los nombres de los parámetros. Esta es una limitación que queremos solucionar en este laboratorio.

En este laboratorio, exploraremos diferentes enfoques para hacer que nuestra clase Structure sea más flexible y amigable para el usuario. Al hacerlo, podemos mejorar la usabilidad de la clase Stock y otras subclases de Structure.

Usar locals() para acceder a los argumentos de una función

En Python, comprender los ámbitos de las variables es crucial. El ámbito de una variable determina en qué parte del código se puede acceder a ella. Python proporciona una función incorporada llamada locals() que es muy útil para que los principiantes comprendan el ámbito. La función locals() devuelve un diccionario que contiene todas las variables locales en el ámbito actual. Esto puede ser extremadamente útil cuando se desea inspeccionar los argumentos de una función, ya que ofrece una vista clara de qué variables están disponibles en una parte específica del código.

Creemos un experimento sencillo en el intérprete de Python para ver cómo funciona esto. Primero, necesitamos navegar al directorio del proyecto e iniciar el intérprete de Python. Puedes hacer esto ejecutando los siguientes comandos en tu terminal:

cd ~/project
python3

Una vez que estés en la shell interactiva de Python, definiremos una clase Stock. Una clase en Python es como un modelo para crear objetos. En esta clase, usaremos el método especial __init__. El método __init__ es un constructor en Python, lo que significa que se llama automáticamente cuando se crea un objeto de la clase. Dentro de este método __init__, usaremos la función locals() para imprimir todas las variables locales.

class Stock:
    def __init__(self, name, shares, price):
        print(locals())

Ahora, creemos una instancia de esta clase Stock. Una instancia es un objeto real creado a partir del modelo de la clase. Pasaremos algunos valores para los parámetros name, shares y price.

s = Stock('GOOG', 100, 490.1)

Cuando ejecutes este código, deberías ver una salida similar a:

{'self': <__main__.Stock object at 0x...>, 'name': 'GOOG', 'shares': 100, 'price': 490.1}

Esta salida muestra que locals() nos da un diccionario que contiene todas las variables locales en el método __init__. La referencia self es una variable especial en las clases de Python que se refiere a la instancia de la clase misma. Las otras variables son los valores de los parámetros que pasamos al crear el objeto Stock.

Podemos usar esta funcionalidad de locals() para inicializar automáticamente los atributos de un objeto. Los atributos son variables asociadas con un objeto. Definamos una función auxiliar y modifiquemos nuestra clase Stock.

def _init(locs):
    self = locs.pop('self')
    for name, val in locs.items():
        setattr(self, name, val)

class Stock:
    def __init__(self, name, shares, price):
        _init(locals())

La función _init toma el diccionario de variables locales obtenido de locals(). Primero, elimina la referencia self del diccionario usando el método pop. Luego, itera sobre los pares clave - valor restantes en el diccionario y usa la función setattr para establecer cada variable como un atributo en el objeto.

Ahora, probemos esta implementación con argumentos posicionales y de palabra clave. Los argumentos posicionales se pasan en el orden en que se definen en la firma de la función, mientras que los argumentos de palabra clave se pasan con los nombres de los parámetros especificados.

## Test with positional arguments
s1 = Stock('GOOG', 100, 490.1)
print(s1.name, s1.shares, s1.price)

## Test with keyword arguments
s2 = Stock(name='AAPL', shares=50, price=125.3)
print(s2.name, s2.shares, s2.price)

¡Ambos enfoques deberían funcionar ahora! La función _init nos permite manejar tanto los argumentos posicionales como los de palabra clave sin problemas. También conserva los nombres de los parámetros en la firma de la función, lo que hace que la salida de help() sea más útil. La función help() en Python proporciona información sobre funciones, clases y módulos, y tener intactos los nombres de los parámetros hace que esta información sea más significativa.

Cuando hayas terminado de experimentar, puedes salir del intérprete de Python ejecutando el siguiente comando:

exit()

Explorando la inspección de marcos de pila (stack frames)

El enfoque _init(locals()) que hemos estado utilizando es funcional, pero tiene un inconveniente. Cada vez que definimos un método __init__, tenemos que llamar explícitamente a locals(). Esto puede volverse un poco tedioso, especialmente cuando se trabajan con múltiples clases. Afortunadamente, podemos hacer que nuestro código sea más limpio y eficiente utilizando la inspección de marcos de pila. Esta técnica nos permite acceder automáticamente a las variables locales del llamador sin tener que llamar explícitamente a locals().

Comencemos a explorar esta técnica en el intérprete de Python. Primero, abre tu terminal y navega al directorio del proyecto. Luego, inicia el intérprete de Python. Puedes hacer esto ejecutando los siguientes comandos:

cd ~/project
python3

Ahora que estamos en el intérprete de Python, necesitamos importar el módulo sys. El módulo sys proporciona acceso a algunas variables utilizadas o mantenidas por el intérprete de Python. Lo usaremos para acceder a la información del marco de pila.

import sys

A continuación, definiremos una versión mejorada de nuestra función _init(). Esta nueva versión accederá directamente al marco del llamador, eliminando la necesidad de pasar locals() explícitamente.

def _init():
    ## Get the caller's frame (1 level up in the call stack)
    frame = sys._getframe(1)

    ## Get the local variables from that frame
    locs = frame.f_locals

    ## Extract self and set other variables as attributes
    self = locs.pop('self')
    for name, val in locs.items():
        setattr(self, name, val)

En este código, sys._getframe(1) recupera el objeto de marco de la función llamadora. El 1 como argumento significa que estamos buscando un nivel más arriba en la pila de llamadas. Una vez que tenemos el objeto de marco, podemos acceder a sus variables locales utilizando frame.f_locals. Esto nos da un diccionario de todas las variables locales en el ámbito del llamador. Luego extraemos la variable self y establecemos las variables restantes como atributos del objeto self.

Ahora, probemos esta nueva función _init() con una nueva versión de nuestra clase Stock.

class Stock:
    def __init__(self, name, shares, price):
        _init()  ## No need to pass locals() anymore!

## Test it
s = Stock('GOOG', 100, 490.1)
print(s.name, s.shares, s.price)

## Also works with keyword arguments
s = Stock(name='AAPL', shares=50, price=125.3)
print(s.name, s.shares, s.price)

Como puedes ver, el método __init__ ya no necesita pasar locals() explícitamente. Esto hace que nuestro código sea más limpio y fácil de leer desde la perspectiva del llamador.

Cómo funciona la inspección de marcos de pila

Cuando llamas a sys._getframe(1), Python devuelve el objeto de marco que representa el marco de ejecución del llamador. El argumento 1 significa "un nivel más arriba del marco actual" (la función llamadora).

Un objeto de marco contiene información importante sobre el contexto de ejecución. Esto incluye la función actual que se está ejecutando, las variables locales en esa función y el número de línea que se está ejecutando actualmente.

Al acceder a frame.f_locals, obtenemos un diccionario de todas las variables locales en el ámbito del llamador. Esto es similar a lo que locals() devolvería si se llamara directamente desde ese ámbito.

Esta técnica es bastante poderosa, pero debe usarse con cautela. Generalmente se considera una característica avanzada de Python y puede parecer un poco "mágica" porque se sale de los límites normales de ámbito de Python.

Una vez que hayas terminado de experimentar con la inspección de marcos de pila, puedes salir del intérprete de Python ejecutando el siguiente comando:

exit()

Implementando la inicialización avanzada en la estructura

Acabamos de aprender dos técnicas poderosas para acceder a los argumentos de una función. Ahora, usaremos estas técnicas para actualizar nuestra clase Structure. Primero, entendamos por qué estamos haciendo esto. Estas técnicas harán que nuestra clase sea más flexible y fácil de usar, especialmente cuando se trate de diferentes tipos de argumentos.

Abre el archivo structure.py en el editor de código. Puedes hacer esto ejecutando los siguientes comandos en la terminal. El comando cd cambia el directorio al directorio del proyecto, y el comando code abre el archivo structure.py en el editor de código.

cd ~/project
code structure.py

Reemplaza el contenido del archivo con el siguiente código. Este código define una clase Structure con varios métodos. Analicemos cada parte para entender lo que hace.

import sys

class Structure:
    _fields = ()

    @staticmethod
    def _init():
        ## Get the caller's frame (the __init__ method that called this)
        frame = sys._getframe(1)

        ## Get the local variables from that frame
        locs = frame.f_locals

        ## Extract self and set other variables as attributes
        self = locs.pop('self')
        for name, val in locs.items():
            setattr(self, name, val)

    def __repr__(self):
        values = ', '.join(f'{name}={getattr(self, name)!r}' for name in self._fields)
        return f'{type(self).__name__}({values})'

    def __setattr__(self, name, value):
        if name.startswith('_') or name in self._fields:
            super().__setattr__(name, value)
        else:
            raise AttributeError(f'{type(self).__name__!r} has no attribute {name!r}')

Esto es lo que hemos hecho en el código:

  1. Eliminamos el antiguo método __init__(). Dado que las subclases definirán sus propios métodos __init__, ya no necesitamos el antiguo.
  2. Añadimos un nuevo método estático _init(). Este método utiliza la inspección de marcos para capturar y establecer automáticamente todos los parámetros como atributos. La inspección de marcos nos permite acceder a las variables locales del método llamador.
  3. Mantenimos el método __repr__(). Este método proporciona una buena representación en cadena del objeto, lo cual es útil para la depuración y la impresión.
  4. Añadimos un método __setattr__(). Este método aplica la validación de atributos, asegurando que solo se puedan establecer atributos válidos en el objeto.

Ahora, actualicemos la clase Stock. Abre el archivo stock.py utilizando el siguiente comando:

code stock.py

Reemplaza su contenido con el siguiente código:

from structure import Structure

class Stock(Structure):
    _fields = ('name', 'shares', 'price')

    def __init__(self, name, shares, price):
        self._init()  ## This magically captures and sets all parameters!

    @property
    def cost(self):
        return self.shares * self.price

    def sell(self, nshares):
        self.shares -= nshares

El cambio clave aquí es que nuestro método __init__ ahora llama a self._init() en lugar de establecer manualmente cada atributo. El método _init() utiliza la inspección de marcos para capturar y establecer automáticamente todos los parámetros como atributos. Esto hace que el código sea más conciso y fácil de mantener.

Probemos nuestra implementación ejecutando las pruebas unitarias. Las pruebas unitarias nos ayudarán a asegurarnos de que nuestro código funciona como se espera. Ejecuta los siguientes comandos en la terminal:

cd ~/project
python3 teststock.py

Deberías ver que todas las pruebas pasan, incluyendo la prueba para argumentos de palabra clave que fallaba antes. Esto significa que nuestra implementación está funcionando correctamente.

Veamos también la documentación de ayuda para nuestra clase Stock. La documentación de ayuda proporciona información sobre la clase y sus métodos. Ejecuta el siguiente comando en la terminal:

python3 -c "from stock import Stock; help(Stock)"

Ahora deberías ver una firma adecuada para el método __init__, mostrando todos los nombres de los parámetros. Esto hace que sea más fácil para otros desarrolladores entender cómo usar la clase.

Finalmente, probemos de forma interactiva que los argumentos de palabra clave funcionan como se espera. Ejecuta el siguiente comando en la terminal:

python3 -c "from stock import Stock; s = Stock(name='GOOG', shares=100, price=490.1); print(s)"

Deberías ver que el objeto Stock se crea correctamente con los atributos especificados. Esto confirma que nuestro sistema de inicialización de clase admite argumentos de palabra clave.

Con esta implementación, hemos logrado un sistema de inicialización de clase mucho más flexible y fácil de usar que:

  1. Preserva las firmas de función adecuadas en la documentación, lo que hace que sea más fácil para los desarrolladores entender cómo usar la clase.
  2. Admite tanto argumentos posicionales como de palabra clave, proporcionando más flexibilidad al crear objetos.
  3. Requiere un código de plantilla mínimo en las subclases, reduciendo la cantidad de código que debes escribir.
✨ Revisar Solución y Practicar

Resumen

En este laboratorio, has aprendido sobre las reglas de alcance (scoping) de Python y algunas técnicas poderosas para manejar el alcance. Primero, exploraste cómo usar la función locals() para acceder a todas las variables locales dentro de una función. Segundo, aprendiste a inspeccionar los marcos de pila (stack frames) utilizando sys._getframe() para acceder a las variables locales del llamador.

También aplicaste estas técnicas para crear un sistema de inicialización de clase flexible. Este sistema captura y establece automáticamente los parámetros de la función como atributos del objeto, mantiene las firmas de función adecuadas en la documentación y admite tanto argumentos posicionales como de palabra clave. Estas técnicas demuestran la flexibilidad y las capacidades de introspección de Python. Aunque la inspección de marcos es una técnica avanzada que debe usarse con cuidado, puede reducir eficazmente el código repetitivo (boilerplate code) cuando se utiliza adecuadamente. Comprender las reglas de alcance y estas técnicas avanzadas te proporciona más herramientas para escribir código Python más limpio y mantenible.