Más sobre Funciones

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

Aunque se introdujeron las funciones anteriormente, se proporcionaron muy pocos detalles sobre cómo funcionan en realidad a un nivel más profundo. Esta sección tiene como objetivo llenar algunos vacíos y discutir temas como convenciones de llamada, reglas de alcance y más.

Llamada a una función

Considere esta función:

def read_prices(filename, debug):
  ...

Puede llamar a la función con argumentos posicionales:

prices = read_prices('prices.csv', True)

O puede llamar a la función con argumentos de palabras clave:

prices = read_prices(filename='prices.csv', debug=True)

Argumentos predeterminados

A veces desea que un argumento sea opcional. Si es así, asigne un valor predeterminado en la definición de la función.

def read_prices(filename, debug=False):
 ...

Si se asigna un valor predeterminado, el argumento es opcional en las llamadas a la función.

d = read_prices('prices.csv')
e = read_prices('prices.dat', True)

Nota: Los argumentos con valores predeterminados deben aparecer al final de la lista de argumentos (todos los argumentos no opcionales van primero).

Prefiera los argumentos de palabras clave para los argumentos opcionales

Compare y contraste estos dos diferentes estilos de llamada:

parse_data(data, False, True) #?????

parse_data(data, ignore_errors=True)
parse_data(data, debug=True)
parse_data(data, debug=True, ignore_errors=True)

En la mayoría de los casos, los argumentos de palabras clave mejoran la claridad del código, especialmente para los argumentos que sirven como banderas o que están relacionados con características opcionales.

Mejores prácticas de diseño

Siempre dé nombres cortos, pero significativos a los argumentos de las funciones.

Alguien que use una función puede querer usar el estilo de llamada con palabras clave.

d = read_prices('prices.csv', debug=True)

Las herramientas de desarrollo de Python mostrarán los nombres en las características de ayuda y la documentación.

Devolviendo valores

La instrucción return devuelve un valor

def square(x):
    return x * x

Si no se da un valor de retorno o falta la instrucción return, se devuelve None.

def bar(x):
    statements
    return

a = bar(4)      ## a = None

## O

def foo(x):
    statements  ## No `return`

b = foo(4)      ## b = None

Varios valores de retorno

Las funciones solo pueden devolver un valor. Sin embargo, una función puede devolver varios valores devolviéndolos en una tupla.

def divide(a,b):
    q = a // b      ## Cociente
    r = a % b       ## Resto
    return q, r     ## Devuelve una tupla

Ejemplo de uso:

x, y = divide(37,5) ## x = 7, y = 2

x = divide(37, 5)   ## x = (7, 2)

Alcance de variables

Los programas asignan valores a variables.

x = value ## Variable global

def foo():
    y = value ## Variable local

Las asignaciones de variables se realizan fuera y dentro de las definiciones de funciones. Las variables definidas fuera son "globales". Las variables dentro de una función son "locales".

Variables locales

Las variables asignadas dentro de las funciones son privadas.

def read_portfolio(filename):
    portfolio = []
    for line in open(filename):
        fields = line.split(',')
        s = (fields[0], int(fields[1]), float(fields[2]))
        portfolio.append(s)
    return portfolio

En este ejemplo, filename, portfolio, line, fields y s son variables locales. Esas variables no se conservan ni son accesibles después de la llamada a la función.

>>> stocks = read_portfolio('portfolio.csv')
>>> fields
Traceback (most recent call last):
File "<stdin>", line 1, in?
NameError: name 'fields' is not defined
>>>

Las variables locales también no pueden entrar en conflicto con las variables encontradas en otros lugares.

Variables globales

Las funciones pueden acceder libremente a los valores de las variables globales definidas en el mismo archivo.

name = 'Dave'

def greeting():
    print('Hello', name)  ## Usando la variable global `name`

Sin embargo, las funciones no pueden modificar las variables globales:

name = 'Dave'

def spam():
  name = 'Guido'

spam()
print(name) ## imprime 'Dave'

Recuerde: Todas las asignaciones en funciones son locales.

Modificando variables globales

Si es necesario modificar una variable global, debe declararse como tal.

name = 'Dave'

def spam():
    global name
    name = 'Guido' ## Cambia la variable global name arriba

La declaración global debe aparecer antes de su uso y la variable correspondiente debe existir en el mismo archivo que la función. Habiendo visto esto, sabe que se considera mala forma. De hecho, trate de evitar global por completo si puede. Si necesita que una función modifique algún tipo de estado fuera de la función, es mejor usar una clase en su lugar (más sobre esto más adelante).

Pasaje de argumentos

Cuando se llama a una función, las variables de argumento son nombres que se refieren a los valores pasados. Estos valores NO son copias. Si se pasan tipos de datos mutables (por ejemplo, listas, diccionarios), se pueden modificar in situ.

def foo(items):
    items.append(42)    ## Modifica el objeto de entrada

a = [1, 2, 3]
foo(a)
print(a)                ## [1, 2, 3, 42]

Punto clave: Las funciones no reciben una copia de los argumentos de entrada.

Reasignación vs Modificación

Asegúrese de entender la sutil diferencia entre modificar un valor y reasignar un nombre de variable.

def foo(items):
    items.append(42)    ## Modifica el objeto de entrada

a = [1, 2, 3]
foo(a)
print(a)                ## [1, 2, 3, 42]

## VS
def bar(items):
    items = [4,5,6]    ## Cambia la variable local `items` para que apunte a un objeto diferente

b = [1, 2, 3]
bar(b)
print(b)                ## [1, 2, 3]

Recordatorio: La asignación de variables nunca sobrescribe la memoria. El nombre simplemente se vincula a un nuevo valor.

Este conjunto de ejercicios le pide que implemente lo que, quizás, es la parte más poderosa y difícil del curso. Hay muchos pasos y muchos conceptos de ejercicios anteriores se ponen juntos de una vez. La solución final es de solo alrededor de 25 líneas de código, pero tome su tiempo y asegúrese de entender cada parte.

Una parte central de su programa report.py se centra en la lectura de archivos CSV. Por ejemplo, la función read_portfolio() lee un archivo que contiene filas de datos de cartera y la función read_prices() lee un archivo que contiene filas de datos de precios. En ambas funciones, hay muchos detalles "fastidiosos" de bajo nivel y características similares. Por ejemplo, ambas abren un archivo y lo envuelven con el módulo csv y ambas convierten varios campos en nuevos tipos.

Si estuviera haciendo mucha análisis de archivos en serio, probablemente querría limpiar algo de esto y hacerlo más general. Ese es nuestro objetivo.

Comience este ejercicio abriendo el archivo llamado fileparse.py. Aquí es donde estaremos trabajando.

Ejercicio 3.3: Lectura de archivos CSV

Para comenzar, centrémonos solo en el problema de leer un archivo CSV en una lista de diccionarios. En el archivo fileparse_3.3.py, defina una función que se vea así:

## fileparse_3.3.py
import csv

def parse_csv(filename):
    '''
    Analiza un archivo CSV en una lista de registros
    '''
    with open(filename) as f:
        rows = csv.reader(f)

        ## Lee los encabezados del archivo
        headers = next(rows)
        records = []
        for row in rows:
            if not row:    ## Omite las filas sin datos
                continue
            record = dict(zip(headers, row))
            records.append(record)

    return records

Esta función lee un archivo CSV en una lista de diccionarios mientras esconde los detalles de abrir el archivo, envolverlo con el módulo csv, ignorar las líneas en blanco, etc.

Prueba:

Pista: python3 -i fileparse_3.3.py.

>>> portfolio = parse_csv('/home/labex/project/portfolio.csv')
>>> portfolio
[{'name': 'AA','shares': '100', 'price': '32.20'}, {'name': 'IBM','shares': '50', 'price': '91.10'}, {'name': 'CAT','shares': '150', 'price': '83.44'}, {'name': 'MSFT','shares': '200', 'price': '51.23'}, {'name': 'GE','shares': '95', 'price': '40.37'}, {'name': 'MSFT','shares': '50', 'price': '65.10'}, {'name': 'IBM','shares': '100', 'price': '70.44'}]
>>>

Esto es bueno, excepto que no se pueden hacer ningún tipo de cálculo útil con los datos porque todo se representa como una cadena. Lo corregiremos pronto, pero sigamos construyendo sobre ello.

✨ Revisar Solución y Practicar

Ejercicio 3.4: Construyendo un selector de columnas

En muchos casos, solo estás interesado en columnas seleccionadas de un archivo CSV, no en todos los datos. Modifica la función parse_csv() de modo que opcionalmente permita que se seleccionen columnas especificadas por el usuario de la siguiente manera:

>>> ## Lee todos los datos
>>> portfolio = parse_csv('/home/labex/project/portfolio.csv')
>>> portfolio
[{'name': 'AA','shares': '100', 'price': '32.20'}, {'name': 'IBM','shares': '50', 'price': '91.10'}, {'name': 'CAT','shares': '150', 'price': '83.44'}, {'name': 'MSFT','shares': '200', 'price': '51.23'}, {'name': 'GE','shares': '95', 'price': '40.37'}, {'name': 'MSFT','shares': '50', 'price': '65.10'}, {'name': 'IBM','shares': '100', 'price': '70.44'}]

>>> ## Lee solo algunos de los datos
>>> shares_held = parse_csv('/home/labex/project/portfolio.csv', select=['name','shares'])
>>> shares_held
[{'name': 'AA','shares': '100'}, {'name': 'IBM','shares': '50'}, {'name': 'CAT','shares': '150'}, {'name': 'MSFT','shares': '200'}, {'name': 'GE','shares': '95'}, {'name': 'MSFT','shares': '50'}, {'name': 'IBM','shares': '100'}]
>>>

Un ejemplo de selector de columnas se dio en el Ejercicio 2.23. Sin embargo, aquí está una forma de hacerlo:

## fileparse_3.4.py
import csv

def parse_csv(filename, select=None):
    '''
    Analiza un archivo CSV en una lista de registros
    '''
    with open(filename) as f:
        rows = csv.reader(f)

        ## Lee los encabezados del archivo
        headers = next(rows)

        ## Si se dio un selector de columnas, encuentra los índices de las columnas especificadas.
        ## También restringe el conjunto de encabezados utilizado para los diccionarios resultantes
        if select:
            indices = [headers.index(colname) for colname in select]
            headers = select
        else:
            indices = []

        records = []
        for row in rows:
            if not row:    ## Omite las filas sin datos
                continue
            ## Filtra la fila si se seleccionaron columnas específicas
            if indices:
                row = [ row[index] for index in indices ]

            ## Crea un diccionario
            record = dict(zip(headers, row))
            records.append(record)

    return records

Hay varios aspectos complicados en esta parte. Probablemente el más importante es la asignación de las selecciones de columnas a los índices de fila. Por ejemplo, suponga que el archivo de entrada tuviera los siguientes encabezados:

>>> headers = ['name', 'date', 'time','shares', 'price']
>>>

Ahora, suponga que las columnas seleccionadas fueran las siguientes:

>>> select = ['name','shares']
>>>

Para realizar la selección adecuada, tienes que asignar los nombres de columnas seleccionados a los índices de columna en el archivo. Eso es lo que hace este paso:

>>> indices = [headers.index(colname) for colname in select ]
>>> indices
[0, 3]
>>>

En otras palabras, "name" es la columna 0 y "shares" es la columna 3. Cuando lees una fila de datos del archivo, los índices se utilizan para filtrarla:

>>> row = ['AA', '6/11/2007', '9:50am', '100', '32.20' ]
>>> row = [ row[index] for index in indices ]
>>> row
['AA', '100']
>>>
✨ Revisar Solución y Practicar

Ejercicio 3.5: Realizar conversión de tipos

Modifica la función parse_csv() en el directorio /home/labex/project/fileparse_3.5.py de modo que opcionalmente permita que se apliquen conversiones de tipos a los datos devueltos. Por ejemplo:

>>> portfolio = parse_csv('/home/labex/project/portfolio.csv', types=[str, int, float])
>>> portfolio
[{'name': 'AA','shares': 100, 'price': 32.2}, {'name': 'IBM','shares': 50, 'price': 91.1}, {'name': 'CAT','shares': 150, 'price': 83.44}, {'name': 'MSFT','shares': 200, 'price': 51.23}, {'name': 'GE','shares': 95, 'price': 40.37}, {'name': 'MSFT','shares': 50, 'price': 65.1}, {'name': 'IBM','shares': 100, 'price': 70.44}]

>>> shares_held = parse_csv('/home/labex/project/portfolio.csv', select=['name','shares'], types=[str, int])
>>> shares_held
[{'name': 'AA','shares': 100}, {'name': 'IBM','shares': 50}, {'name': 'CAT','shares': 150}, {'name': 'MSFT','shares': 200}, {'name': 'GE','shares': 95}, {'name': 'MSFT','shares': 50}, {'name': 'IBM','shares': 100}]
>>>

Ya exploraste esto en el Ejercicio 2.24. Necesitarás insertar el siguiente fragmento de código en tu solución:

...
if types:
    row = [func(val) for func, val in zip(types, row) ]
...
✨ Revisar Solución y Practicar

Ejercicio 3.6: Trabajar sin encabezados

Algunos archivos CSV no incluyen ninguna información de encabezado. Por ejemplo, el archivo prices.csv se ve así:

"AA",9.22
"AXP",24.85
"BA",44.85
"BAC",11.27
...

Modifica la función parse_csv() en /home/labex/project/fileparse_3.6.py para que pueda trabajar con tales archivos creando una lista de tuplas en lugar de eso. Por ejemplo:

>>> prices = parse_csv('/home/labex/project/prices.csv', types=[str,float], has_headers=False)
>>> prices
[('AA', 9.22), ('AXP', 24.85), ('BA', 44.85), ('BAC', 11.27), ('C', 3.72), ('CAT', 35.46), ('CVX', 66.67), ('DD', 28.47), ('DIS', 24.22), ('GE', 13.48), ('GM', 0.75), ('HD', 23.16), ('HPQ', 34.35), ('IBM', 106.28), ('INTC', 15.72), ('JNJ', 55.16), ('JPM', 36.9), ('KFT', 26.11), ('KO', 49.16), ('MCD', 58.99), ('MMM', 57.1), ('MRK', 27.58), ('MSFT', 20.89), ('PFE', 15.19), ('PG', 51.94), ('T', 24.79), ('UTX', 52.61), ('VZ', 29.26), ('WMT', 49.74), ('XOM', 69.35)]
>>>

Para hacer este cambio, necesitarás modificar el código para que la primera línea de datos no se interprete como una línea de encabezado. Además, necesitarás asegurarte de no crear diccionarios ya que ya no hay nombres de columna para usar como claves.

✨ Revisar Solución y Practicar

Ejercicio 3.7: Elegir un separador de columnas diferente

Aunque los archivos CSV son bastante comunes, también es posible que encuentres un archivo que use un separador de columnas diferente, como una tabulación o un espacio. Por ejemplo, el archivo portfolio.dat se ve así:

name shares price
"AA" 100 32.20
"IBM" 50 91.10
"CAT" 150 83.44
"MSFT" 200 51.23
"GE" 95 40.37
"MSFT" 50 65.10
"IBM" 100 70.44

La función csv.reader() permite especificar un separador de columnas diferente de la siguiente manera:

rows = csv.reader(f, delimiter=' ')

Modifica tu función parse_csv() en /home/labex/project/fileparse_3.7.py para que también permita cambiar el separador.

Por ejemplo:

>>> portfolio = parse_csv('/home/labex/project/portfolio.dat', types=[str, int, float], delimiter=' ')
>>> portfolio
[{'name': 'AA','shares': 100, 'price': 32.2}, {'name': 'IBM','shares': 50, 'price': 91.1}, {'name': 'CAT','shares': 150, 'price': 83.44}, {'name': 'MSFT','shares': 200, 'price': 51.23}, {'name': 'GE','shares': 95, 'price': 40.37}, {'name': 'MSFT','shares': 50, 'price': 65.1}, {'name': 'IBM','shares': 100, 'price': 70.44}]
>>>
✨ Revisar Solución y Practicar

Comentario

Si has llegado hasta aquí, has creado una función de biblioteca muy útil. Puedes usarla para analizar archivos CSV arbitrarios, seleccionar columnas de interés, realizar conversiones de tipo, sin tener que preocuparte demasiado por el funcionamiento interno de los archivos o el módulo csv.

Resumen

¡Felicidades! Has completado el laboratorio Más sobre Funciones. Puedes practicar más laboratorios en LabEx para mejorar tus habilidades.