Convenciones de paso de argumentos de funciones

Beginner

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

Introducción

En este laboratorio, aprenderás sobre las convenciones de paso de argumentos de funciones en Python. También crearás una estructura reutilizable para clases de datos y aplicarás principios de diseño orientado a objetos para simplificar tu código.

Este ejercicio tiene como objetivo reescribir el archivo stock.py de una manera más organizada. Antes de comenzar, copia tu trabajo existente en stock.py a un nuevo archivo llamado orig_stock.py para tenerlo como referencia. Los archivos que crearás son structure.py y stock.py.

Comprender el paso de argumentos de funciones

En Python, las funciones son un concepto fundamental que te permite agrupar un conjunto de instrucciones para realizar una tarea específica. Cuando llamas a una función, a menudo necesitas proporcionarle algunos datos, que llamamos argumentos. Python ofrece diferentes formas de pasar estos argumentos a las funciones. Esta flexibilidad es increíblemente útil, ya que te ayuda a escribir un código más limpio y mantenible. Antes de comenzar a aplicar estas técnicas a nuestro proyecto, echemos un vistazo más detallado a estas convenciones de paso de argumentos.

Crear una copia de seguridad de tu trabajo

Antes de comenzar a hacer cambios en nuestro archivo stock.py, es una buena práctica crear una copia de seguridad. De esta manera, si algo sale mal durante nuestras pruebas, siempre podemos volver a la versión original. Para crear una copia de seguridad, abre una terminal y ejecuta el siguiente comando:

cp stock.py orig_stock.py

Este comando utiliza el comando cp (copiar) en la terminal. Toma el archivo stock.py y crea una copia de él llamada orig_stock.py. Al hacer esto, nos aseguramos de que nuestro trabajo original se conserve de manera segura.

Explorar el paso de argumentos de funciones

En Python, hay varias formas de llamar a funciones con diferentes tipos de argumentos. Exploremos cada uno de estos métodos en detalle.

1. Argumentos posicionales

La forma más sencilla de pasar argumentos a una función es por posición. Cuando defines una función, especificas una lista de parámetros. Cuando llamas a la función, proporcionas valores para estos parámetros en el mismo orden en que se definen.

Aquí tienes un ejemplo:

def calculate(x, y, z):
    return x + y + z

## Call with positional arguments
result = calculate(1, 2, 3)
print(result)  ## Output: 6

En este ejemplo, la función calculate toma tres parámetros: x, y y z. Cuando llamamos a la función con calculate(1, 2, 3), el valor 1 se asigna a x, 2 se asigna a y y 3 se asigna a z. La función luego suma estos valores y devuelve el resultado.

2. Argumentos de palabra clave

Además de los argumentos posicionales, también puedes especificar argumentos por sus nombres. Esto se llama usar argumentos de palabra clave. Cuando usas argumentos de palabra clave, no tienes que preocuparte por el orden de los argumentos.

Aquí tienes un ejemplo:

## Call with a mix of positional and keyword arguments
result = calculate(1, z=3, y=2)
print(result)  ## Output: 6

En este ejemplo, primero pasamos el argumento posicional 1 para x. Luego, usamos argumentos de palabra clave para especificar los valores de y y z. El orden de los argumentos de palabra clave no importa, siempre y cuando proporciones los nombres correctos.

3. Desempaquetar secuencias y diccionarios

Python proporciona una forma conveniente de pasar secuencias y diccionarios como argumentos utilizando la sintaxis * y **. Esto se llama desempaquetado.

Aquí tienes un ejemplo de desempaquetar una tupla en argumentos posicionales:

## Unpacking a tuple into positional arguments
args = (1, 2, 3)
result = calculate(*args)
print(result)  ## Output: 6

En este ejemplo, tenemos una tupla args que contiene los valores 1, 2 y 3. Cuando usamos el operador * antes de args en la llamada a la función, Python desempaqueta la tupla y pasa sus elementos como argumentos posicionales a la función calculate.

Aquí tienes un ejemplo de desempaquetar un diccionario en argumentos de palabra clave:

## Unpacking a dictionary into keyword arguments
kwargs = {'y': 2, 'z': 3}
result = calculate(1, **kwargs)
print(result)  ## Output: 6

En este ejemplo, tenemos un diccionario kwargs que contiene los pares clave-valor 'y': 2 y 'z': 3. Cuando usamos el operador ** antes de kwargs en la llamada a la función, Python desempaqueta el diccionario y pasa sus pares clave-valor como argumentos de palabra clave a la función calculate.

4. Aceptar argumentos variables

A veces, es posible que desees definir una función que pueda aceptar cualquier número de argumentos. Python te permite hacer esto utilizando la sintaxis * y ** en la definición de la función.

Aquí tienes un ejemplo de una función que acepta cualquier número de argumentos posicionales:

## Accept any number of positional arguments
def sum_all(*args):
    return sum(args)

print(sum_all(1, 2))           ## Output: 3
print(sum_all(1, 2, 3, 4, 5))  ## Output: 15

En este ejemplo, la función sum_all utiliza el parámetro *args para aceptar cualquier número de argumentos posicionales. El operador * recoge todos los argumentos posicionales en una tupla llamada args. La función luego utiliza la función incorporada sum para sumar todos los elementos de la tupla.

Aquí tienes un ejemplo de una función que acepta cualquier número de argumentos de palabra clave:

## Accept any number of keyword arguments
def print_info(**kwargs):
    for key, value in kwargs.items():
        print(f"{key}: {value}")

print_info(name="Python", year=1991)
## Output:
## name: Python
## year: 1991

En este ejemplo, la función print_info utiliza el parámetro **kwargs para aceptar cualquier número de argumentos de palabra clave. El operador ** recoge todos los argumentos de palabra clave en un diccionario llamado kwargs. La función luego itera sobre los pares clave-valor en el diccionario y los imprime.

Estas técnicas nos ayudarán a crear estructuras de código más flexibles y reutilizables en los siguientes pasos. Para familiarizarte más con estos conceptos, abramos el intérprete de Python y probemos algunos de estos ejemplos.

python3

Una vez que estés en el intérprete de Python, intenta ingresar los ejemplos anteriores. Esto te dará experiencia práctica con estas técnicas de paso de argumentos.

Crear una clase base de estructura

Ahora que tenemos una buena comprensión del paso de argumentos de funciones, vamos a crear una clase base reutilizable para estructuras de datos. Este paso es crucial porque nos ayuda a evitar escribir el mismo código una y otra vez cuando creamos clases simples que almacenan datos. Al utilizar una clase base, podemos optimizar nuestro código y hacerlo más eficiente.

El problema con el código repetitivo

En los ejercicios anteriores, definiste una clase Stock como se muestra a continuación:

class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

Observa detenidamente el método __init__. Notarás que es bastante repetitivo. Tienes que asignar manualmente cada atributo uno por uno. Esto puede volverse muy tedioso y consumir mucho tiempo, especialmente cuando tienes muchas clases con una gran cantidad de atributos.

Crear una clase base flexible

Vamos a crear una clase base Structure que pueda manejar automáticamente la asignación de atributos. Primero, abre el WebIDE y crea un nuevo archivo llamado structure.py. Luego, agrega el siguiente código a este archivo:

## structure.py

class Structure:
    """
    A base class for creating simple data structures.
    Automatically populates object attributes from _fields and constructor arguments.
    """
    _fields = ()

    def __init__(self, *args):
        ## Check that the number of arguments matches the number of fields
        if len(args) != len(self._fields):
            raise TypeError(f"Expected {len(self._fields)} arguments")

        ## Set the attributes
        for name, value in zip(self._fields, args):
            setattr(self, name, value)

Esta clase base tiene varias características importantes:

  1. Define una variable de clase _fields. Por defecto, esta variable está vacía. Esta variable contendrá los nombres de los atributos que tendrá la clase.
  2. Comprueba si el número de argumentos pasados al constructor coincide con el número de campos definidos en _fields. Si no coinciden, genera un TypeError. Esto nos ayuda a detectar errores temprano.
  3. Establece los atributos del objeto utilizando los nombres de los campos y los valores proporcionados como argumentos. La función setattr se utiliza para establecer dinámicamente los atributos.

Probar nuestra clase base de estructura

Ahora, vamos a crear algunas clases de ejemplo que heredan de la clase base Structure. Agrega el siguiente código a tu archivo structure.py:

## Example classes using Structure
class Stock(Structure):
    _fields = ('name', 'shares', 'price')

class Point(Structure):
    _fields = ('x', 'y')

class Date(Structure):
    _fields = ('year', 'month', 'day')

Para probar si nuestra implementación funciona correctamente, crearemos un archivo de prueba llamado test_structure.py. Agrega el siguiente código a este archivo:

## test_structure.py
from structure import Stock, Point, Date

## Test Stock class
s = Stock('GOOG', 100, 490.1)
print(f"Stock name: {s.name}, shares: {s.shares}, price: {s.price}")

## Test Point class
p = Point(3, 4)
print(f"Point coordinates: ({p.x}, {p.y})")

## Test Date class
d = Date(2023, 11, 9)
print(f"Date: {d.year}-{d.month}-{d.day}")

## Test error handling
try:
    s2 = Stock('AAPL', 50)  ## Missing price argument
    print("This should not print")
except TypeError as e:
    print(f"Error correctly caught: {e}")

Para ejecutar la prueba, abre tu terminal y ejecuta el siguiente comando:

python3 test_structure.py

Deberías ver la siguiente salida:

Stock name: GOOG, shares: 100, price: 490.1
Point coordinates: (3, 4)
Date: 2023-11-9
Error correctly caught: Expected 3 arguments

Como puedes ver, nuestra clase base está funcionando como se esperaba. Ha hecho que sea mucho más fácil definir nuevas estructuras de datos sin tener que escribir el mismo código de plantilla repetidamente.

Mejorar la representación de objetos

Nuestra clase Structure es útil para crear y acceder a objetos. Sin embargo, actualmente no tiene una buena forma de representarse como una cadena. Cuando imprimes un objeto o lo visualizas en el intérprete de Python, quieres ver una presentación clara e informativa. Esto te ayuda a entender qué es el objeto y cuáles son sus valores.

Comprender la representación de objetos en Python

En Python, hay dos métodos especiales que se utilizan para representar objetos de diferentes maneras. Estos métodos son importantes porque te permiten controlar cómo se muestran tus objetos.

  • __str__ - Este método es utilizado por la función str() y la función print(). Proporciona una representación legible por humanos del objeto. Por ejemplo, si tienes un objeto Stock, el método __str__ podría devolver algo como "Stock: GOOG, 100 shares at $490.1".
  • __repr__ - Este método es utilizado por el intérprete de Python y la función repr(). Da una representación más técnica y sin ambigüedades del objeto. El objetivo de __repr__ es proporcionar una cadena que se pueda utilizar para recrear el objeto. Por ejemplo, para un objeto Stock, podría devolver "Stock('GOOG', 100, 490.1)".

Vamos a agregar un método __repr__ a nuestra clase Structure. Esto hará que sea más fácil depurar nuestro código porque podemos ver claramente el estado de nuestros objetos.

Implementar una buena representación

Ahora, necesitas actualizar tu archivo structure.py. Agregarás el método __repr__ a la clase Structure. Este método creará una cadena que represente el objeto de una manera que se pueda utilizar para recrearlo.

def __repr__(self):
    """
    Return a representation of the object that can be used to recreate it.
    Example: Stock('GOOG', 100, 490.1)
    """
    ## Get the class name
    cls_name = type(self).__name__

    ## Get all the field values
    values = [getattr(self, name) for name in self._fields]

    ## Format the fields and values
    args_str = ', '.join(repr(value) for value in values)

    ## Return the formatted string
    return f"{cls_name}({args_str})"

Esto es lo que hace este método paso a paso:

  1. Obtiene el nombre de la clase utilizando type(self).__name__. Esto es importante porque te dice de qué tipo de objeto estás tratando.
  2. Recupera todos los valores de los campos de la instancia. Esto te da los datos que el objeto contiene.
  3. Crea una representación en cadena con el nombre de la clase y los valores. Esta cadena se puede utilizar para recrear el objeto.

Probar la representación mejorada

Vamos a probar nuestra implementación mejorada. Crea un nuevo archivo llamado test_repr.py. Este archivo creará algunas instancias de nuestras clases e imprimirá sus representaciones.

## test_repr.py
from structure import Stock, Point, Date

## Create instances
s = Stock('GOOG', 100, 490.1)
p = Point(3, 4)
d = Date(2023, 11, 9)

## Print the representations
print(repr(s))
print(repr(p))
print(repr(d))

## Direct printing also uses __repr__ in the interpreter
print(s)
print(p)
print(d)

Para ejecutar la prueba, abre tu terminal y escribe el siguiente comando:

python3 test_repr.py

Deberías ver la siguiente salida:

Stock('GOOG', 100, 490.1)
Point(3, 4)
Date(2023, 11, 9)
Stock('GOOG', 100, 490.1)
Point(3, 4)
Date(2023, 11, 9)

Esta salida es mucho más informativa que antes. Cuando ves Stock('GOOG', 100, 490.1), inmediatamente sabes qué representa el objeto. Incluso podrías copiar esta cadena y usarla para recrear el objeto en tu código.

El beneficio de buenas representaciones

Una buena implementación de __repr__ es muy útil para la depuración. Cuando estás mirando objetos en el intérprete o registrando información durante la ejecución del programa, una representación clara facilita la identificación rápida de problemas. Puedes ver el estado exacto del objeto y entender qué podría estar saliendo mal.

Restringir nombres de atributos

Actualmente, nuestra clase Structure permite establecer cualquier atributo en sus instancias. Para los principiantes, esto puede parecer conveniente al principio, pero en realidad puede causar muchos problemas. Cuando trabajas con una clase, esperas que ciertos atributos estén presentes y se utilicen de una manera específica. Si los usuarios escriben mal el nombre de los atributos o intentan establecer atributos que no forman parte del diseño original, puede causar errores difíciles de encontrar.

La necesidad de restringir atributos

Veamos un escenario simple para entender por qué necesitamos restringir los nombres de los atributos. Considera el siguiente código:

s = Stock('GOOG', 100, 490.1)
s.shares = 50      ## Correct attribute name
s.share = 60       ## Typo in attribute name - creates a new attribute instead of updating

En la segunda línea, hay un error tipográfico. En lugar de shares, escribimos share. En Python, en lugar de generar un error, simplemente creará un nuevo atributo llamado share. Esto puede causar errores sutiles porque es posible que pienses que estás actualizando el atributo shares, pero en realidad estás creando uno nuevo. Esto puede hacer que tu código se comporte de manera inesperada y sea muy difícil de depurar.

Implementar la restricción de atributos

Para resolver este problema, podemos sobrescribir el método __setattr__. Este método se llama cada vez que intentas establecer un atributo en un objeto. Al sobrescribirlo, podemos controlar qué atributos se pueden establecer y cuáles no.

Actualiza tu clase Structure en structure.py con el siguiente código:

def __setattr__(self, name, value):
    """
    Restrict attribute setting to only those defined in _fields
    or attributes starting with underscore (private attributes).
    """
    if name.startswith('_'):
        ## Allow setting private attributes (starting with '_')
        super().__setattr__(name, value)
    elif name in self._fields:
        ## Allow setting attributes defined in _fields
        super().__setattr__(name, value)
    else:
        ## Raise an error for other attributes
        raise AttributeError(f'No attribute {name}')

Así es cómo funciona este método:

  1. Si el nombre del atributo comienza con un guión bajo (_), se considera un atributo privado. Los atributos privados se utilizan a menudo con fines internos en una clase. Permitimos que se establezcan estos atributos porque forman parte de la implementación interna de la clase.
  2. Si el nombre del atributo está en la lista _fields, significa que es uno de los atributos definidos en el diseño de la clase. Permitimos que se establezcan estos atributos porque forman parte del comportamiento esperado de la clase.
  3. Si el nombre del atributo no cumple con ninguna de estas condiciones, generamos un AttributeError. Esto le dice al usuario que está intentando establecer un atributo que no existe en la clase.

Probar la restricción de atributos

Ahora que hemos implementado la restricción de atributos, probémosla para asegurarnos de que funciona como se espera. Crea un archivo llamado test_attributes.py con el siguiente código:

## test_attributes.py
from structure import Stock

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

## This should work - valid attribute
print("Setting shares to 50")
s.shares = 50
print(f"Shares is now: {s.shares}")

## This should work - private attribute
print("\nSetting _internal_data")
s._internal_data = "Some data"
print(f"_internal_data is: {s._internal_data}")

## This should fail - invalid attribute
print("\nTrying to set an invalid attribute:")
try:
    s.share = 60  ## Typo in attribute name
    print("This should not print")
except AttributeError as e:
    print(f"Error correctly caught: {e}")

Para ejecutar la prueba, abre tu terminal y escribe el siguiente comando:

python3 test_attributes.py

Deberías ver la siguiente salida:

Setting shares to 50
Shares is now: 50

Setting _internal_data
_internal_data is: Some data

Trying to set an invalid attribute:
Error correctly caught: No attribute share

Esta salida muestra que nuestra clase ahora evita errores accidentales de atributos. Permite establecer atributos válidos y atributos privados, pero genera un error cuando intentamos establecer un atributo inválido.

El valor de la restricción de atributos

Restringir los nombres de los atributos es muy importante para escribir código robusto y mantenible. He aquí por qué:

  1. Ayuda a detectar errores tipográficos en los nombres de los atributos. Si cometemos un error al escribir el nombre de un atributo, el código generará un error en lugar de crear un nuevo atributo. Esto facilita encontrar y corregir errores temprano en el proceso de desarrollo.
  2. Evita intentos de establecer atributos que no existen en el diseño de la clase. Esto asegura que la clase se utilice como se pretendió y que el código se comporte de manera predecible.
  3. Evita la creación accidental de nuevos atributos. La creación de nuevos atributos puede causar un comportamiento inesperado y dificultar la comprensión y el mantenimiento del código.

Al restringir los nombres de los atributos, hacemos que nuestro código sea más confiable y fácil de manejar.

Reescribir la clase Stock

Ahora que tenemos una clase base Structure bien definida, es hora de reescribir nuestra clase Stock. Al utilizar esta clase base, podemos simplificar nuestro código y hacerlo más organizado. La clase Structure proporciona un conjunto de funcionalidades comunes que podemos reutilizar en nuestra clase Stock, lo cual es una gran ventaja para la mantenibilidad y legibilidad del código.

Crear la nueva clase Stock

Comencemos creando un nuevo archivo llamado stock.py. Este archivo contendrá nuestra clase Stock reescrita. Aquí está el código que debes poner en el archivo stock.py:

## stock.py
from structure import Structure

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

    @property
    def cost(self):
        """
        Calculate the cost as shares * price
        """
        return self.shares * self.price

    def sell(self, nshares):
        """
        Sell a number of shares
        """
        self.shares -= nshares

Analicemos lo que hace esta nueva clase Stock:

  1. Hereda de la clase Structure. Esto significa que la clase Stock puede utilizar todas las características proporcionadas por la clase Structure. Una de las ventajas es que no necesitamos escribir un método __init__ nosotros mismos porque la clase Structure se encarga de la asignación de atributos automáticamente.
  2. Definimos _fields, que es una tupla que especifica los atributos de la clase Stock. Estos atributos son name, shares y price.
  3. Se define la propiedad cost para calcular el costo total de las acciones. Multiplica el número de shares por el price.
  4. El método sell se utiliza para reducir el número de acciones. Cuando se llama a este método con un número de acciones a vender, resta ese número del número actual de acciones.

Probar la nueva clase Stock

Para asegurarnos de que nuestra nueva clase Stock funcione como se espera, necesitamos crear un archivo de prueba. Creemos un archivo llamado test_stock.py con el siguiente código:

## test_stock.py
from stock import Stock

## Create a stock
s = Stock('GOOG', 100, 490.1)

## Check the attributes
print(f"Stock: {s}")
print(f"Name: {s.name}")
print(f"Shares: {s.shares}")
print(f"Price: {s.price}")
print(f"Cost: {s.cost}")

## Sell some shares
print("\nSelling 20 shares...")
s.sell(20)
print(f"Shares after selling: {s.shares}")
print(f"Cost after selling: {s.cost}")

## Try to set an invalid attribute
print("\nTrying to set an invalid attribute:")
try:
    s.prices = 500  ## Invalid attribute (should be 'price')
    print("This should not print")
except AttributeError as e:
    print(f"Error correctly caught: {e}")

En este archivo de prueba, primero importamos la clase Stock del archivo stock.py. Luego creamos una instancia de la clase Stock con el nombre 'GOOG', 100 acciones y un precio de 490.1. Imprimimos los atributos de la acción para verificar si se han establecido correctamente. Después, vendemos 20 acciones e imprimimos el nuevo número de acciones y el nuevo costo. Finalmente, intentamos establecer un atributo inválido prices (debería ser price). Si nuestra clase Stock funciona correctamente, debería generar un AttributeError.

Para ejecutar la prueba, abre tu terminal y escribe el siguiente comando:

python3 test_stock.py

La salida esperada es la siguiente:

Stock: Stock('GOOG', 100, 490.1)
Name: GOOG
Shares: 100
Price: 490.1
Cost: 49010.0

Selling 20 shares...
Shares after selling: 80
Cost after selling: 39208.0

Trying to set an invalid attribute:
Error correctly caught: No attribute prices

Ejecutar pruebas unitarias

Si tienes pruebas unitarias de ejercicios anteriores, puedes ejecutarlas contra tu nueva implementación. En tu terminal, escribe el siguiente comando:

python3 teststock.py

Ten en cuenta que algunas pruebas pueden fallar. Esto puede deberse a que esperan comportamientos o métodos específicos que aún no hemos implementado. ¡No te preocupes! Continuaremos construyendo sobre esta base en ejercicios futuros.

Revisión de nuestro progreso

Tomemos un momento para revisar lo que hemos logrado hasta ahora:

  1. Creamos una clase base Structure reutilizable. Esta clase:

    • Maneja automáticamente la asignación de atributos, lo que nos ahorra escribir mucho código repetitivo.
    • Proporciona una buena representación en cadena, lo que facilita imprimir y depurar nuestros objetos.
    • Restringe los nombres de los atributos para prevenir errores, lo que hace nuestro código más robusto.
  2. Reescribimos nuestra clase Stock. Ella:

    • Hereda de la clase Structure para reutilizar la funcionalidad común.
    • Solo define los campos y métodos específicos del dominio, lo que mantiene la clase enfocada y limpia.
    • Tiene un diseño claro y sencillo, lo que la hace fácil de entender y mantener.

Este enfoque tiene varios beneficios para nuestro código:

  • Es más mantenible porque hay menos repetición. Si necesitamos cambiar algo en la funcionalidad común, solo necesitamos cambiarlo en la clase Structure.
  • Es más robusto debido a la mejor comprobación de errores proporcionada por la clase Structure.
  • Es más legible porque las responsabilidades de cada clase son claras.

En ejercicios futuros, continuaremos construyendo sobre esta base para crear un sistema de gestión de carteras de acciones más sofisticado.

Resumen

En este laboratorio, has aprendido sobre las convenciones de paso de argumentos de funciones en Python y las has aplicado para construir una base de código más organizada y mantenible. Has explorado los flexibles mecanismos de paso de argumentos de Python, creado una clase base Structure reutilizable para objetos de datos y mejorado la representación de objetos para una mejor depuración.

También has agregado validación de atributos para prevenir errores comunes y reescrito la clase Stock utilizando la nueva estructura. Estas técnicas demuestran principios clave de diseño orientado a objetos, como la herencia para la reutilización de código, la encapsulación para la integridad de los datos y el polimorfismo a través de interfaces comunes. Al aplicar estos principios, puedes desarrollar un código más robusto y mantenible con menos repetición y menos errores.