3. Organización de un Programa

En este modulo nos empapamos de más detalles sobre la composición de funciones, el manejo de errores y la introducción de modulos. Al final seremos capaces de escribir programas que son subdivididos en funciones esparcidas en varios archivos. Veremos algunas plantillas de código que les será útil para la creación de programas.

3.1 Scripting

En esta parte, veremos más de cerca la práctica de escribir scripts en Python.

3.1.1 ¿Qué es un guión / script?

Un script es un programa que ejecuta una serie de declaraciones y se detiene.

# programa.py 
declaracion1
declaracion2
declaracion3
...

Hasta ahora, vale destacar que hemos estado escribiendo scripts.

3.1.2 Un problema

Si escribe un script útil, crecerá en características y funcionalidad. Es posible que desee aplicarlo a otros problemas relacionados. Con el tiempo, podría convertirse en una aplicación crítica. Sin embargo, podría convertirse en un gran enredado si no tiene cuidado. Entonces, es importante tener algún tipo de organización.

3.1.3 Definiendo cosas

Los nombres siempre deben definirse antes de que se utilicen más tarde.

def square(x):
    return x*x

a = 42
b = a + 2     # requiere que `a` esté definida 
z = square(b) # require que `square` y `b` esten definidas 

El orden es importante. Casi siempre colocas las definiciones de variables y funciones cerca de la parte superior.

3.1.4 Definición de funciones

Es una buena idea poner todo el código relacionado con una sola tarea en un solo lugar. Para esto, haga uso de una función.

def read_prices(filename):
    prices = {}
    with open(filename) as f:
        f_csv = csv.reader(f)
        for row in f_csv:
            prices[row[0]] = float(row[1])
    return prices

Una función también simplifica las operaciones repetidas.

oldprices = read_prices('oldprices.csv')
newprices = read_prices('newprices.csv')

3.1.5 ¿Qué es una función?

Una función es una secuencia de declaraciones con nombre.

def funcname(args):
  declaracion
  declaracion
  ...
  return result

Cualquier declaración de Python se puede utilizar dentro.

def foo():
    import math
    print(math.sqrt(2))
    help(math)

No hay declaraciones especiales en Python (lo que hace que sea fácil de recordar).

3.1.6 Definición de función

Las funciones se pueden definir en cualquier orden.

def foo(x):
    bar(x)

def bar(x):
    declaracion

# OR 
def bar(x):
    declaracion

def foo(x):
    bar(x)

Las funciones solo deben definirse antes de ser utilizadas (o llamadas) durante la ejecución del programa.

foo(3)        # foo debió haber estado definida 

Estilísticamente, probablemente sea más común ver las funciones definidas de abajo hacia arriba.

3.1.7 Estilo de abajo hacia arriba

Las funciones se tratan como bloques de construcción. Los bloques más pequeños / simples van primero.

# myprogram.py 
def foo(x):
    ...

def bar(x):
    ...
    foo(x)          # Definida anteriormente
    ...

def spam(x):
    ...
    bar(x)          # Definida anteriormente
    ...

spam(42)            # Código que utiliza las funciones esta definido anteriormente 

Las funciones posteriores se basan en funciones anteriores. Nuevamente, esto es solo una cuestión de estilo. Lo único que importa en el programa anterior es que la llamada spam(42) sea ​​la última.

3.1.8 Diseño de funciones

Idealmente, las funciones deberían ser una caja negra . Solo deben operar con entradas pasadas y evitar variables globales y efectos secundarios misteriosos. Sus principales objetivos: modularidad y previsibilidad.

3.1.9 Documentación en cadenas

La documentación en cadenas, comunmente conocida en inglés como doc strings, es una buena práctica que incluye un breve resumen de una oración de lo que hace la función. Si se necesita más información, se incluye un breve ejemplo de uso junto con una descripción más detallada de los argumentos. Las mismas se escriben inmediatamente después del nombre de la función y alimentan a help(), el IDE (ambiente integrado de desarrollo) y otras herramientas.

def read_prices(filename):
    '''Lee precios de un archivo CSV file de nombre, precio, y data'''
    prices = {}
    with open(filename) as f:
        f_csv = csv.reader(f)
        for row in f_csv:
            prices[row[0]] = float(row[1])
    return prices

3.1.10 Tipo de anotaciones

También puede agregar sugerencias de tipo opcionales a las definiciones de funciones.

def read_prices(filename: str) -> dict:
    '''Lee precios de un archivo CSV file de nombre, precio, y data'''
    prices = {}
    with open(filename) as f:
        f_csv = csv.reader(f)
        for row in f_csv:
            prices[row[0]] = float(row[1])
    return prices

Las sugerencias no hacen nada operativamente. Son puramente informativos. Sin embargo, pueden ser utilizados por IDE, verificadores de código y otras herramientas para hacer más.

3.1.11 Ejercicios

En la sección 2, escribió un programa llamado report.pyque imprimía un informe que mostraba el rendimiento de una cartera de acciones. Este programa constaba de algunas funciones. Por ejemplo:

# report.py 
import csv

def read_portfolio(filename):
    '''Lee un archivo de cartera de acciones en una lista de diccionarios con 
    la clave siendo nombre, acciones y precio.'''
    portfolio = []
    with open(filename) as f:
        rows = csv.reader(f)
        headers = next(rows)

        for row in rows:
            record = dict(zip(headers, row))
            stock = {
                'name' : record['name'],
                'shares' : int(record['shares']),
                'price' : float(record['price'])
            }
            portfolio.append(stock)
    return portfolio
...

Sin embargo, también hubo partes del programa que solo realizaron una serie de cálculos con guión. Este código apareció cerca del final del programa. Por ejemplo:

...

# Salida/output del reporte
headers = ('Name', 'Shares', 'Price', 'Change')
print('%10s %10s %10s %10s'  % headers)
print(('-' * 10 + ' ') * len(headers))
for row in report:
    print('%10s %10d %10.2f %10.2f' % row)
...

En este ejercicio, tomaremos el programa anterior y lo organizaremos con un poco más de fuerza en torno al uso de funciones.

Ejercicio 3.1: Estructurar un programa como una colección de funciones

Modifique su programa report.py para que todas las operaciones principales, incluyendo los cálculos y la salida, se lleven a cabo mediante una colección de funciones.

Específicamente:

  • Cree una función print_report(report) que imprima el informe.
  • Cambie la última parte del programa para que no sea más que una serie de llamadas a funciones y ningún otro cálculo.

Ejercicio 3.2: Creación de una función de nivel superior para la ejecución del programa

Tome la última parte de su programa y empaquelo en una sola función portfolio_report(portfolio_filename, prices_filename). Haga que la función funcione para que la siguiente llamada de función cree el informe como antes:

portfolio_report('Data/portfolio.csv', 'Data/prices.csv')

En esta versión final, su programa no será más que una serie de definiciones de funciones seguidas de una única llamada de función portfolio_report() final al (que ejecuta todos los pasos involucrados en el programa).

Al convertir su programa en una sola función, es fácil ejecutarlo en diferentes entradas. Por ejemplo, pruebe estas declaraciones de forma interactiva después de ejecutar su programa:

>>> portfolio_report('Data/portfolio2.csv', 'Data/prices.csv')
>>> # mire la salida de la linea anterior
>>> files = ['Data/portfolio.csv', 'Data/portfolio2.csv']
>>> for name in files:
        print(f'{name:-^43s}')
        portfolio_report(name, 'Data/prices.csv')
        print()

>>> # mire la salida del ciclo anterior
>>>

Comentario

Python hace que sea muy fácil escribir código de scripting relativamente no estructurado en el que solo tiene un archivo con una secuencia de declaraciones. En el panorama general, casi siempre es mejor utilizar funciones cuando pueda. En algún momento, ese script crecerá y deseará tener un poco más de organización. Además, un hecho poco conocido es que Python se ejecuta un poco más rápido si usa funciones.

3.2 Más sobre funciones

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

3.2.1 Llamar 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)

3.2.2 Argumentos predeterminados

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

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

Si se asigna un valor predeterminado, el argumento es opcional en las llamadas a funciones.

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).

3.2.3 Prefiera argumentos de palabras clave vs argumentos opcionales

Compare y contraste estos dos estilos de llamadas diferentes:

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 argumentos que sirven como indicadores o que están relacionados con características opcionales.

3.2.4 Mejores prácticas de diseño

Siempre proporcione nombres cortos pero significativos a los argumentos de las funciones.

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

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

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

3.2.5 Devolución de valores

La declaración return devuelve un valor

def square(x):
    return x * x

Si no se proporciona ningún valor de retorno o el return falta, se devuelve None.

def bar(x):
    ...declaraciones
    return

a = bar(4)      # a = None 

# O
def foo(x):
    ...declaraciones  # Sin `return` 

b = foo(4)      # b = None 

3.2.6 Múltiples 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       # Remanente     
    return q, r     # Retorna una tuple 

Ejemplo de uso:

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

3.2.7 Alcance de una variable

Los programas asignan valores a las variables.

x = value # variable Global  
def foo():
    y = value # variable Local  

Las asignaciones de variables ocurren fuera y dentro de las definiciones de funciones. Las variables definidas en el exterior son "globales". Las variables dentro de una función son "locales".

3.2.8 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 se puede acceder a ellas 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
>>>

Los locales tampoco pueden entrar en conflicto con las variables que se encuentran en otros lugares.

3.2.9 Variables globales

Las funciones pueden acceder libremente a los valores de globales definidos en el mismo archivo.

name = 'Dave'

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

Sin embargo, las funciones no pueden modificar los globales:

name = 'Dave'

def spam():
  name = 'Guido'

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

Recuerde: todas las asignaciones en funciones son locales.

3.2.10 Modificar variables globales

Si debe modificar una variable global, debe declararla como tal.

name = 'Dave'

def spam():
    global name
    name = 'Guido' # cambia el nombre global anterior

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, sepa que se considera de mala forma. De hecho, trate de evitarlo global completo si puede. Si necesita una función para modificar 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).

3.2.11 Transferencia de argumentos

Cuando llama a una función, las variables de argumento son nombres que hacen referencia a los valores pasados. Estos valores NO son copias (consulte la sección 2.7). Si se pasan tipos de datos mutables (por ejemplo, listas, diccionarios), se pueden modificar en el lugar.

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.

3.2.12 Reasignación vs modificación

Asegúrese de comprender 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]    # Reasigna variable local `items` a otro objeto 

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

Recordatorio: la asignación de variables nunca sobrescribe la memoria. El nombre simplemente está vinculado a un nuevo valor.

3.2.13 Ejercicios

Este conjunto de ejercicios le permite implementar lo que es, quizás, la parte más poderosa y difícil del curso. Hay muchos pasos y muchos conceptos de ejercicios anteriores que se juntan todos a la vez. La solución final es solo de unas 25 líneas de código, pero tómese su tiempo y asegúrese de comprender 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 muchas partes "complicadas" de bajo nivel y características similares. Por ejemplo, ambos abren un archivo y lo envuelven con el módulo csv y ambos convierten varios campos en nuevos tipos.

Si estuvieses haciendo mucho análisis de archivos, probablemente queras limpiar algo de esto y hacerlo más general. Ese es nuestro objetivo.

Comience este ejercicio abriendo un archivo llamado fileparse.py. Aquí es donde estaremos haciendo nuestro trabajo.

Ejercicio 3.3: lectura de archivos CSV

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

# fileparse.py 
import csv

def parse_csv(filename):
    ''' Convierte un archivo CSV a una lista de registro '''
    with open(filename) as f:
        rows = csv.reader(f)

        headers = next(rows)  # Lee la primera fila con el encabezado
        records = []
        for row in rows:
            if not row:    # Omite filas sin data                 
                continue
            record = dict(zip(headers, row))
            records.append(record)

    return records

Esta función lee un archivo CSV en una lista de diccionarios mientras oculta los detalles de la apertura del archivo, lo envuelve con el módulo csv, ignora las líneas en blanco, etc.

Pruébelo:

Sugerencia: $ python3 -i fileparse.py.

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

Esto es bueno, excepto que no puede hacer ningún tipo de cálculo útil con los datos porque todo está representado como una cadena. Arreglaremos esto en breve, pero sigamos mejorandolo.

Ejercicio 3.4: Creación de un selector de columnas

En muchos casos, solo le interesan las columnas seleccionadas de un archivo CSV, no todos los datos. Modifique la función parse_csv() para que, opcionalmente, permita que las columnas especificadas por el usuario se seleccionen de la siguiente manera:

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

>>> # Lee solo parte de la data 
>>> shares_held = parse_csv('Data/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'}]
>>>

En el ejercicio 2.23 se dio un ejemplo de selector de columnas. Sin embargo, aquí hay una forma de hacerlo:

# fileparse.py 
import csv

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

        # Read the file headers         
        headers = next(rows)

        # Si se proporcionó un selector de columna, busque los índices de las 
        # columnas especificadas.
        # También reduzca el conjunto de encabezados utilizados 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:    # Omitir filas sin data                
                continue
            # Filtrar la fila si se seleccionaron columnas específicas          
            if indices:
                row = [ row[index] for index in indices ]

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

    return records

Hay una serie de partes complicadas en esta parte. Probablemente lo más importante es el mapeo de las columnas seleccionadas a los índices de filas. Por ejemplo, suponga que el archivo de entrada tiene 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, debe asignar los nombres de las columnas seleccionadas a los índices de las columnas en el archivo. Eso es lo que está haciendo este paso:

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

En otras palabras, "nombre" es la columna 0 y "recursos compartidos" es la columna 3. Cuando lee 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']
>>>

Ejercicio 3.5: Realización de conversión de tipos

Modifique la función parse_csv() para que, opcionalmente, permita que se apliquen conversiones de tipo a los datos devueltos. Por ejemplo:

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

>>> shares_held = parse_csv('Data/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 exploró esto en el ejercicio 2.24. Deberá insertar el siguiente fragmento de código en su solución:

...
if types:
    row = [func(val) for func, val in zip(types, row) ]
...

Ejercicio 3.6: Trabajar sin encabezados

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

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

Modifique la parse_csv() función para que pueda trabajar con dichos archivos creando una lista de tuplas. Por ejemplo:

>>> prices = parse_csv('Data/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 realizar este cambio, deberá modificar el código para que la primera línea de datos no se interprete como una línea de encabezado. Además, deberá asegurarse de no crear diccionarios, ya que ya no hay nombres de columna para usar como claves.

Ejercicio 3.7: elegir un delimitador de columna diferente

Aunque los archivos CSV son bastante comunes, también es posible que encuentre un archivo que use un separador de columna diferente, como una sangría (tab) o un espacio. Por ejemplo, el archivo se Data/portfolio.datve 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 dar un delimitador de columna diferente de la siguiente manera:

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

Modifique su función parse_csv() para que también permita cambiar el delimitador.

Por ejemplo:

>>> portfolio = parse_csv('Data/portfolio.dat', types=[str, int, float], delimiter=' ')
>>> portfolio
[{'price': '32.20', 'name': 'AA', 'shares': '100'}, {'price': '91.10', 'name': 'IBM', 'shares': '50'}, {'price': '83.44', 'name': 'CAT', 'shares': '150'}, {'price': '51.23', 'name': 'MSFT', 'shares': '200'}, {'price': '40.37', 'name': 'GE', 'shares': '95'}, {'price': '65.10', 'name': 'MSFT', 'shares': '50'}, {'price': '70.44', 'name': 'IBM', 'shares': '100'}]
>>>

Comentario

Si ha llegado hasta aquí, ha creado una agradable función de biblioteca que es realmente útil. Puede usarla para analizar archivos CSV arbitrarios, seleccionar columnas de interés, realizar conversiones de tipos, sin tener que preocuparse demasiado por el funcionamiento interno de los archivos o el módulo csv.

3.3 Comprobación de errores

Aunque las excepciones se introdujeron anteriormente, esta sección incluye algunos detalles adicionales sobre la verificación de errores y el manejo de excepciones.

3.3.1 Cómo fallan los programas

Python no realiza ninguna verificación ni validación de los tipos o valores de los argumentos de la función. Una función funcionará con cualquier dato que sea compatible con las declaraciones de la función.

def add(x, y):
    return x + y

add(3, 4)               # 7 
add('Hello', 'World')   # 'HelloWorld' 
add('3', '4')           # '34' 

Si hay errores en una función, aparecen en tiempo de ejecución (como excepción).

def add(x, y):
    return x + y

>>> add(3, '4')
Traceback (most recent call last):
...
TypeError: unsupported operand type(s) for +:
    'int' and 'str'
>>>

Para verificar el código, hay un fuerte énfasis en las pruebas (se tratará más adelante).

3.3.2 Excepciones

Las excepciones se utilizan para señalar errores. Para plantear una excepción usted mismo, haga uso de la declaración raise.

if name not in authorized:
    raise RuntimeError(f'{name} not authorized')

Para detectar una excepción, utilice try-except.

try:
    authenticate(username)
except RuntimeError as e:
    print(e)

3.3.3 Manejo de excepciones

Las excepciones se propagan a la primera coincidencia except.

def grok():
    ...
    raise RuntimeError('Whoa!')   # Excepción se alza aquí 

def spam():
    grok()                        # Esto alzrá la excepción 

def bar():
    try:
       spam()
    except RuntimeError as e:     # La excepción se atrapa aquí         
        ...

def foo():
    try:
         bar()
    except RuntimeError as e:     # La excepción no llega aquí
        ...

foo()

Para manejar la excepción, coloque declaraciones en el exceptbloque. Puede agregar cualquier declaración que desee para manejar el error.

def grok(): 
    ...
    raise RuntimeError('Whoa!')

def bar():
    try:
      grok()
    except RuntimeError as e:   # Excepción se atrapa aquí         
        declaraciones           # Use estas declaraciones         
        declaraciones
        ...

bar()

Después del manejo, la ejecución se reanuda con la primera declaración después de try-except.

def grok(): ...
    raise RuntimeError('Whoa!')

def bar():
    try:
      grok()
    except RuntimeError as e:   # Excepción se atrapa aquí          
        statements
        statements
        ...
    statements              # Reanuda ejecución aquí     
    statements              # y continúa aquí    
    ...

bar()

3.3.4 Excepciones integradas

Hay alrededor de dos docenas de excepciones integradas. Por lo general, el nombre de la excepción es indicativo de lo que está mal (por ejemplo, ValueError se genera a porque proporcionó un valor incorrecto). Esta no es una lista exhaustiva. Consulte la documentación para obtener más información.

ArithmeticError
AssertionError
EnvironmentError
EOFError
ImportError
IndexError
KeyboardInterrupt
KeyError
MemoryError
NameError
ReferenceError
RuntimeError
SyntaxError
SystemError
TypeError
ValueError

3.3.5 Valores de excepción

Las excepciones tienen un valor asociado. Contiene información más específica sobre lo que está mal.

raise RuntimeError('Invalid user name')

Este valor es parte de la instancia de excepción que se coloca en la variable proporcionada a except.

try:
    ...
except RuntimeError as e:   # `e` holds the exception raised
    ...

e es una instancia del tipo de excepción. Sin embargo, a menudo parece una cadena cuando se imprime.

except RuntimeError as e:
    print('Failed : Reason', e)

3.3.6 Detectar varios errores

Puede detectar diferentes tipos de excepciones utilizando varios bloques except.

try:
  ...
except LookupError as e:
  ...
except RuntimeError as e:
  ...
except IOError as e:
  ...
except KeyboardInterrupt as e:
  ...

Alternativamente, si las declaraciones para gestionar las excepciones son las mismas, puede agruparlas:

try:
  ...
except (IOError,LookupError,RuntimeError) as e:
  ...

3.3.7 Detectar todos los errores

Para detectar cualquier excepción, use Exception así:

try:
    ...
except Exception:       # PELIGRO. Mire abajo.     
    print('An error occurred')

En general, escribir código como ese es una mala idea porque no sabrá la razón del fallo, toda vez que está atrapando todos los errores.

3.3.8 Manera incorrecta de detectar errores

Esta es la forma incorrecta de usar las excepciones.

try:
    go_do_something()
except Exception:
    print('Computer says no')

Esto detecta todos los errores posibles y puede hacer que sea imposible depurar cuando el código falla por alguna razón que no esperaba (por ejemplo, módulo Python desinstalado, etc.).

3.3.9 Otro enfoque

Si va a detectar todos los errores, este es un enfoque más sensato.

try:
    go_do_something()
except Exception as e:
    print('Computer says no. Reason :', e)

Informe sobre el motivo específico de la falla. Casi siempre es una buena idea tener algún mecanismo para ver / informar errores cuando escribe código que detecta todas las posibles excepciones.

Sin embargo, en general, es mejor detectar el error de la forma más precisa posible. Solo detecte los errores que realmente pueda manejar. Deje pasar otros errores, tal vez algún otro código pueda manejarlos.

3.3.7 Volver a plantear una excepción

Úse raise para propagar un error detectado.

try:
    go_do_something()
except Exception as e:
    print('Computer says no. Reason :', e)
    raise

Esto le permite tomar medidas (por ejemplo, reportería) y transmitir el error al objeto que llama la función.

3.3.8 Mejores prácticas de excepción

No atrapes excepciones. Falle rápido y fuerte. Si es importante, alguien más se ocupará del problema. Solo capture una excepción si se puede recuperar y seguir adelante con el programa.

3.3.9 declaración finally

Especifique el código que debe ejecutarse independientemente de si se produce una excepción o no.

candado = Candado()
...
candado.acquire()
try:
    ...
finally:
    candado.release()  # esto SIEMPRE se ejecutará. Con y sin excepción.

Se usa comúnmente para administrar recursos de manera segura (especialmente bloqueos (locks), archivos, etc.).

3.3.10 declaración with

En código de hoy en día, un try-finally a menudo se reemplaza con la declaración with.

candado = Candado()
with candado:
    # candado adquirido     
    ...
# candado liberado 

Un ejemplo más familiar:

with open(filename) as f:
    # Utilice el archivo     
    ...
# Archivo cerrado 

with define un contexto de uso para un recurso. Cuando la ejecución sale de ese contexto, se liberan los recursos. with solo funciona con ciertos objetos que han sido programados específicamente para soportarlo.

3.3.9 Ejercicios

Ejercicio 3.8: Generación de excepciones

La función parse_csv() que escribió en la última sección permite seleccionar columnas especificadas por el usuario, pero eso solo funciona si el archivo de datos de entrada tiene encabezados de columna.

Modifique el código para que se genere una excepción si se pasan los argumentos select y has_headers=False. Por ejemplo:

>>> parse_csv('Data/prices.csv', select=['name','price'], has_headers=False)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "fileparse.py", line 9, in parse_csv
    raise RuntimeError("select argument requires column headers")
RuntimeError: select argument requires column headers
>>>

Una vez haya agregado esta comprobación, puede preguntarse si debería realizar otros tipos de comprobaciones en la función. Por ejemplo, ¿debería comprobar que el nombre del archivo es una cadena, que los tipos son una lista o algo por el estilo?

Como regla general, es mejor omitir tales pruebas y dejar que el programa falle en entradas incorrectas. El mensaje de rastreo señalará la fuente del problema y puede ayudar en la depuración.

La razón principal para agregar la verificación anterior es evitar ejecutar el código en un modo sin sentido (por ejemplo, usando una función que requiere encabezados de columna, pero especificando simultáneamente que no hay encabezados).

Esto indica un error de programación por parte del código de llamada. A menudo, es una buena idea verificar los casos que "no se supone que sucedan".

Ejercicio 3.9: captura de excepciones

La función parse_csv() que escribió se utiliza para procesar todo el contenido de un archivo. Sin embargo, en el mundo real, es posible que los archivos de entrada tengan datos dañados, faltantes o sucios.

Pruebe este experimento:

>>> portfolio = parse_csv('Data/missing.csv', types=[str, int, float])
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "fileparse.py", line 36, in parse_csv
    row = [func(val) for func, val in zip(types, row)]
ValueError: invalid literal for int() with base 10: ''
>>>

Modifique la función parse_csv() para detectar todas las excepciones ValueError generadas durante la creación de registros e imprima un mensaje de advertencia para las filas que no se pueden convertir.

El mensaje debe incluir el número de fila e información sobre la razón por la que falló. Para probar su función, intente leer el archivo de Data/missing.csv arriba. Por ejemplo:

>>> portfolio = parse_csv('Data/missing.csv', types=[str, int, float])
Row 4: Couldn't convert ['MSFT', '', '51.23'] Row 4: Reason invalid literal for 
int() with base 10: '' Row 7: Couldn't convert ['IBM', '', '70.44']
Row 7: Reason invalid literal for int() with base 10: ''
>>>
>>> portfolio
[{'price': 32.2, 'name': 'AA', 'shares': 100}, {'price': 91.1, 'name': 'IBM', 'shares': 50}, {'price': 83.44, 'name': 'CAT', 'shares': 150}, {'price': 40.37, 'name': 'GE', 'shares': 95}, {'price': 65.1, 'name': 'MSFT', 'shares': 50}]
>>>

Ejercicio 3.10: Silenciar errores

Modifique la función parse_csv() para que los mensajes de error de análisis puedan silenciarse si el usuario lo desea explícitamente. Por ejemplo:

>>> portfolio = parse_csv('Data/missing.csv', types=[str,int,float], silence_errors=True)
>>> portfolio
[{'price': 32.2, 'name': 'AA', 'shares': 100}, {'price': 91.1, 'name': 'IBM', 'shares': 50}, {'price': 83.44, 'name': 'CAT', 'shares': 150}, {'price': 40.37, 'name': 'GE', 'shares': 95}, {'price': 65.1, 'name': 'MSFT', 'shares': 50}]
>>>

El manejo de errores es una de las cosas más difíciles de corregir en la mayoría de los programas. Como regla general, no debe ignorar silenciosamente los errores. En su lugar, es mejor informar problemas y darle al usuario la opción de silenciar el mensaje de error si así lo desea.

3.4 Módulos

Esta sección presenta el concepto de módulos y el trabajo con funciones que abarcan varios archivos.

3.4.1 Módulos e importación

Cualquier archivo fuente de Python es un módulo.

# foo.py 
def grok(a):
    ...

def spam(b):
    ...

La declaración import carga y ejecuta un módulo.

# program.py import foo

a = foo.grok(2)
b = foo.spam('Hello')
...

3.4.2 Ámbito o espacio de nombres

Un módulo es una colección de valores con nombre y, a veces, se dice que es un espacio de nombres. Es un contenedor abstracto creado para alojar una agrupación lógica de identificadores únicos. Los nombres son todas las variables y funciones globales definidas en el archivo fuente. Después de la importación, el nombre del módulo se utiliza como prefijo. De ahí es donde viene el concepto de ámbti o espacio de nombres.

import foo

a = foo.grok(2)
b = foo.spam('Hello')
...

El nombre del módulo está directamente vinculado al nombre del archivo (foo -> foo.py).

3.4.3 Definiciones globales

Todo lo definido en el ámbito global es lo que llena el espacio de nombres del módulo. Considere dos módulos que definen la misma variable x.

# foo.py 
x = 42
def grok(a):
    ...
# bar.py 
x = 37
def spam(a):
    ...

En este caso, las definiciones x se refieren a diferentes variables. Uno es foo.x y el otro es bar.x. Diferentes módulos pueden usar los mismos nombres y esos nombres no entrarán en conflicto entre sí.

Los módulos están aislados.

3.4.4 Módulos como entornos

Los módulos forman un entorno envolvente para todo el código definido en el interior.

# foo.py 

x = 42
def grok(a):
    print(x)

Las variables globales siempre están vinculadas al módulo adjunto (mismo archivo). Cada archivo fuente es su propio pequeño universo.

3.4.5 Ejecución del módulo

Cuando se importa un módulo, todas las declaraciones del módulo se ejecutan una tras otra hasta que se llega al final del archivo. El contenido del espacio de nombres del módulo son todos los nombres globales que aún están definidos al final del proceso de ejecución. Si hay sentencias de scripting que llevan a cabo tareas en el ámbito global (impresión, creación de archivos, etc.) las verá ejecutar en la importación.

3.4.6 declaración import as

Puede cambiar el nombre de un módulo a medida que lo importa:

import math as m

def rectangular(r, theta):
    x = r * m.cos(theta)
    y = r * m.sin(theta)
    return x, y

Funciona igual que una importación normal. Simplemente cambia el nombre del módulo en ese archivo.

3.4.7 importación de módulo con from

Esto selecciona los símbolos seleccionados de un módulo y los hace disponibles localmente.

from math import sin, cos

def rectangular(r, theta):
    x = r * cos(theta)
    y = r * sin(theta)
    return x, y

Esto permite utilizar partes de un módulo sin tener que escribir el prefijo del módulo. Es útil para nombres de uso frecuente.

3.4.8 Comentarios sobre la importación

Las variaciones en la importación no cambian la forma en que funcionan los módulos.

import math
# vs import math as m
# vs from math import cos, sin
...

En concreto, import siempre ejecuta todo el archivo y los módulos siguen siendo entornos aislados.

La declaración import as solo cambia el nombre localmente. La declaración from math import cos, sin todavía carga todo el módulo de matemáticas detrás de escena. Simplemente copia los nombres cos y sin del módulo en el espacio local una vez hecho.

3.4.9 Carga del módulo

Cada módulo se carga y se ejecuta solo una vez. Nota: Las importaciones repetidas solo devuelven una referencia al módulo cargado anteriormente.

sys.modules es un dictado de todos los módulos cargados.

>>> import sys
>>> sys.modules.keys()
['copy_reg', '__main__', 'site', '__builtin__', 'encodings', 'encodings.encodings', 'posixpath', ...]
>>>

Precaución: surge una común confusión si repite una declaración import después de cambiar el código fuente de un módulo. Debido a la memoria caché del módulo sys.modules, las importaciones repetidas siempre devuelven el módulo cargado anteriormente, incluso si se realizó un cambio. La forma más segura de cargar código modificado en Python es salir y reiniciar el intérprete.

3.4.10 Localización de módulos

Python consulta una lista de rutas (sys.path) cuando busca módulos.

>>> import sys
>>> sys.path
[
  '',
  '/usr/local/lib/python36/python36.zip',
  '/usr/local/lib/python36',
  ...
]

El directorio de trabajo actual suele ser el primero.

3.4.11 Ruta de búsqueda del módulo

Como se señaló anteriormente, sys.path contiene las rutas de búsqueda. Puede ajustarlo manualmente si es necesario.

import sys
sys.path.append('/project/foo/pyfiles')

Las rutas también se pueden agregar mediante variables de entorno.

$ env PYTHONPATH=/project/foo/pyfiles python3
Python 3.6.0 (default, Feb 3 2017, 05:53:21)
[GCC 4.2.1 Compatible Apple LLVM 8.0.0 (clang-800.0.38)]
>>> import sys
>>> sys.path
['','/project/foo/pyfiles', ...]

Como regla general, no debería ser necesario ajustar manualmente la ruta de búsqueda del módulo. Sin embargo, a veces surge si está intentando importar código Python que se encuentra en una ubicación inusual o que no es fácilmente accesible desde el directorio de trabajo actual.

Ejercicios

Para este ejercicio que involucra módulos, es de vital importancia asegurarse de que está ejecutando Python en un entorno adecuado. Los módulos a menudo presentan a los nuevos programadores problemas relacionados con el directorio de trabajo actual o con la configuración de la ruta de Python. Para este curso, se asume que está escribiendo todo su código en el directorio donde están todos sus archivos de trabajo. Para obtener los mejores resultados, debe asegurarse de estar también en ese directorio cuando inicie el intérprete. De lo contrario, debe asegurarse de que su directorio de trabajo se agregue a sys.path.

Ejercicio 3.11: Importaciones de módulos

En la sección 3, creamos una función de propósito general parse_csv() para analizar el contenido de los archivos de datos CSV.

Ahora, veremos cómo usar esa función en otros programas. Primero, comience en una nueva ventana de shell. Navega hasta la carpeta donde tienes todos tus archivos. Los vamos a importar.

Inicie el modo interactivo de Python.

$ python
Python 3.6.1 (v3.6.1:69c0db5050, Mar 21 2017, 01:21:04)
[GCC 4.2.1 (Apple Inc. build 5666) (dot 3)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>>

Una vez que haya hecho eso, intente importar algunos de los programas que escribió anteriormente. Debería ver su salida exactamente como antes. Solo para enfatizar: la importación de un módulo ejecuta su código.

>>> import bounce
... mire la salida ...
>>> import mortgage
... mire la salida ...
>>> import report
... mire la salida ...
>>>

Si nada de esto funciona, probablemente esté ejecutando Python en el directorio incorrecto. Ahora, intente importar su módulo fileparse y obtenga ayuda sobre él.

>>> import fileparse
>>> help(fileparse)
... mire la salida ...
>>> dir(fileparse)
... mire la salida ...
>>>

Intente usar el módulo para leer algunos datos:

>>> portfolio = fileparse.parse_csv('Data/portfolio.csv', select=['name','shares','price'], types=[str,int,float])
>>> portfolio
... look at the output ...
>>> pricelist = fileparse.parse_csv('Data/prices.csv', types=[str,float], has_headers=False)
>>> pricelist
... look at the output ...
>>> prices = dict(pricelist)
>>> prices
... look at the output ...
>>> prices['IBM']
106.11
>>>

Intente importar una función para que no necesite incluir el nombre del módulo:

>>> from fileparse import parse_csv
>>> portfolio = parse_csv('Data/portfolio.csv', select=['name','shares','price'], types=[str,int,float])
>>> portfolio
... look at the output ...
>>>

Ejercicio 3.12: Uso de su módulo de biblioteca

En la sección 2, escribió un programa report.py que produjo un informe de acciones como este:

      Name     Shares      Price     Change
---------- ---------- ---------- ----------
        AA        100       9.22     -22.98
       IBM         50     106.28      15.18
       CAT        150      35.46     -47.98
      MSFT        200      20.89     -30.34
        GE         95      13.48     -26.89
      MSFT         50      20.89     -44.21
       IBM        100     106.28      35.84

Tome ese programa y modifíquelo para que todo el procesamiento del archivo de entrada se realice usando funciones en su módulo fileparse. Para hacer eso, importe fileparse como módulo y cambie las funciones read_portfolio() y read_prices() para usar la función parse_csv().

Utilice el ejemplo interactivo al comienzo de este ejercicio como guía. Posteriormente, debería obtener exactamente el mismo resultado que antes.

Ejercicio 3.14: Uso de más importaciones de bibliotecas

En la sección 1, escribió un programa pcost.py que leyó una cartera y calculó su costo.

>>> import pcost
>>> pcost.portfolio_cost('Data/portfolio.csv')
44671.15
>>>

Modifique el archivo pcost.py para que use la función report.read_portfolio().

Comentario

Cuando haya terminado con este ejercicio, debe tener tres programas. 1. fileparse.py que contiene una función parse_csv() de propósito general. 2. report.py que produce un informe agradable, pero también contiene read_portfolio() y read_prices(). 3. Y finalmente, pcost.py que calcula el costo de la cartera, pero hace uso de la read_portfolio() función escrita para el report.py programa.

3.5 Módulo principal

Esta sección presenta el concepto de programa principal o módulo principal.

3.5.1 Funciones principales

En muchos lenguajes de programación, hay un concepto de principal función o método.

// c / c++
int main(int argc, char *argv[]) {
    ...
}
// java
class myprog {
    public static void main(String args[]) {
        ...
    }
}

Esta es la primera función que se ejecuta cuando se inicia una aplicación.

3.5.2 Módulo principal de Python

Python no tiene principal función o método. En cambio, hay un módulo principal. El módulo principal es el archivo fuente que se ejecuta primero.

$ python3 prog.py
...

Cualquier archivo que le dé al intérprete al inicio se convierte en principal. No importa el nombre.

3.5.2 main

Es una práctica estándar para los módulos que se ejecutan como un script principal utilizar esta convención:

# prog.py ...
if __name__ == '__main__':
    # Ejecutando el programa principal ...     
    declaraciones
    ...

Las declaraciones incluidas dentro de la declaración if se convierten en el programa principal .

3.5.3 Programas principales frente a importaciones de bibliotecas

Cualquier archivo de Python puede ejecutarse como principal o como una importación de biblioteca:

$ python prog.py # Running as main
import prog   # Running as library import 

En ambos casos, __name__ es el nombre del módulo. Sin embargo, solo se establecerá en __main__ si se ejecuta como main. Por lo general, no desea que las instrucciones que forman parte del programa principal se ejecuten en una importación de biblioteca. Por lo tanto, es común tener un if-código de verificación que se puede usar de cualquier manera.

if __name__ == '__main__':
    # Does not execute if loaded with import ... 

3.5.3 Plantilla de programa

Aquí hay una plantilla de programa común para escribir un programa Python:

# prog.py 
# declaracioens import (librerias) 
import modules

# Functions 
def spam():
    ...

def blah():
    ...

# Main function 
def main():
    ...

if __name__ == '__main__':
    main()

3.5.4 Herramientas de línea de comandos

Python se usa a menudo para herramientas de línea de comandos

$ python report.py portfolio.csv prices.csv

Significa que los scripts se ejecutan desde el shell / terminal. Los casos de uso comunes son para automatización, tareas en segundo plano, etc.

3.5.5 Argumentos de línea de comando

La línea de comando es una lista de cadenas de texto.

$ python report.py portfolio.csv prices.csv

Esta lista de cadenas de texto se encuentra en sys.argv.

# en el comando de terminal previo
sys.argv # ['report.py, 'portfolio.csv', 'prices.csv'] 

Aquí hay un ejemplo simple de procesamiento de argumentos:

import sys

if len(sys.argv) != 3:
    raise SystemExit(f'Usage: {sys.argv[0]} ' 'portfile pricefile')
portfile = sys.argv[1]
pricefile = sys.argv[2]
...

3.5.6 Estándar I/O (Entrada/Salida)

La entrada / salida estándar (o stdio) son archivos que funcionan igual que los archivos normales.

sys.stdout
sys.stderr
sys.stdin

De forma predeterminada, la impresión está dirigida a sys.stdout. Se lee la entrada sys.stdin. Los rastreos y errores están dirigidos a sys.stderr.

Tenga en cuenta que stdio podría estar conectado a terminales, archivos, tuberías, etc.

$ python prog.py > results.txt
# o
$ cmd1 | python3 prog.py | cmd2

3.5.7 Variables de entorno

Las variables de entorno se establecen en el shell.

$ setenv NAME dave
$ setenv RSH ssh
$ python prog.py

os.environ es un diccionario que contiene estos valores.

import os
name = os.environ['NAME'] # 'dave' 

Los cambios se reflejan en cualquier subproceso que el programa inicie posteriormente.

3.5.8 Saliendo del programa

La salida de un programa se maneja mediante excepciones.

raise SystemExit
raise SystemExit(exitcode)
raise SystemExit('Informative message')

Una alternativa.

import sys
sys.exit(exitcode)

Un código de salida distinto de cero indica un error.

3.5.9 La #!linea

En Unix, la #!línea puede lanzar un script como Python. Agregue lo siguiente a la primera línea de su archivo de secuencia de comandos.

#!/usr/bin/env python3
# prog.py
...

Requiere el permiso ejecutable.

bash % chmod +x prog.py
# Then you can execute
bash % prog.py
... output ...

Nota: Python Launcher en Windows también busca la #!línea para indicar la versión del idioma.

3.5.10 Plantilla de script

Finalmente, aquí hay una plantilla de código común para los programas de Python que se ejecutan como scripts de línea de comandos:

#!/usr/bin/env python3 
# prog.py 
# declaraciones import (libraries) 

import modules

# Functions 
def spam():
    ...

def blah():
    ...

# función main]
def main(argv):
    # analice linea de argumentos, ambiente, etc.
    ...

if __name__ == '__main__':
    import sys
    main(sys.argv)

3.5.11 Ejercicios

Ejercicio 3.15: funciones main()

En el archivo, report.pyagregue una función main() que acepte una lista de opciones de línea de comando y produzca el mismo resultado que antes. Debería poder ejecutarlo de forma interactiva así:

>>> import report
>>> report.main(['report.py', 'Data/portfolio.csv', 'Data/prices.csv'])
      Name     Shares      Price     Change
---------- ---------- ---------- ----------
        AA        100       9.22     -22.98
       IBM         50     106.28      15.18
       CAT        150      35.46     -47.98
      MSFT        200      20.89     -30.34
        GE         95      13.48     -26.89
      MSFT         50      20.89     -44.21
       IBM        100     106.28      35.84
>>>

Modifique el pcost.py archivo para que tenga una main() función similar:

>>> import pcost
>>> pcost.main(['pcost.py', 'Data/portfolio.csv'])
Total cost: 44671.15
>>>

Ejercicio 3.16: Creación de guiones

Modifique los programas report.py y pcost.py para que puedan ejecutarse como un script en la línea de comando:

$ python3 report.py Data/portfolio.csv Data/prices.csv

      Name     Shares      Price     Change
---------- ---------- ---------- ----------
        AA        100       9.22     -22.98
       IBM         50     106.28      15.18
       CAT        150      35.46     -47.98
      MSFT        200      20.89     -30.34
        GE         95      13.48     -26.89
      MSFT         50      20.89     -44.21
       IBM        100     106.28      35.84
$ python pcost.py Data/portfolio.csv
Total cost: 44671.15

3.6 Discusión del diseño

En esta sección, reconsideramos una decisión de diseño tomada anteriormente.

3.6.1 Nombres de archivo versus iterables

Compare estos dos programas que devuelven el mismo resultado.

# proporcione un nombre de archivo 
def read_data(filename):
    records = []
    with open(filename) as f:
        for line in f:
            ...
            records.append(r)
    return records

d = read_data('file.csv')
# proporcione lineas
def read_data(lines):
    records = []
    for line in lines:
        ...
        records.append(r)
    return records

with open('file.csv') as f:
    d = read_data(f)
  • ¿Cuál de estas funciones prefieres? ¿Por qué?
  • ¿Cuál de estas funciones es más flexible?

3.6.2 Idea profunda: "Duck Typing"

Duck Typing es un concepto de programación de computadoras para determinar si un objeto se puede usar para un propósito particular. Es una aplicación de la prueba del pato .

Si parece un pato, nada como un pato y grazna como un pato, entonces probablemente sea un pato.

En la segunda versión de read_data() arriba, la función espera cualquier objeto iterable. No solo las líneas de un archivo.

def read_data(lines):
    records = []
    for line in lines:
        ...
        records.append(r)
    return records

Esto significa que podemos usarlo con otras líneas.

# archivo CSV 
lines = open('data.csv')
data = read_data(lines)

# archivo zipped
lines = gzip.open('data.csv.gz','rt')
data = read_data(lines)

# entrada estándar
lines = sys.stdin
data = read_data(lines)

# lista de cadenas 
lines = ['ACME,50,91.1','IBM,75,123.45', ... ]
data = read_data(lines)

Hay una considerable flexibilidad con este diseño.

Pregunta: ¿Debemos aceptar o rechazar esta flexibilidad? Piense en los pros y cons y llegue a una conclusíón a base de los mismos. Considere que distintos tipos de requerimentos le impulsarian hacia más o menos flexibilidad.

3.6.3 Mejores prácticas de diseño de bibliotecas

Las bibliotecas de códigos a menudo se sirven mejor adoptando la flexibilidad. No restrinja sus opciones. Una gran flexibilidad conlleva un gran poder.

3.6.4 Ejercicio

Ejercicio 3.17: De nombres de archivo a objetos similares a archivos

Ahora ha creado un archivo fileparse.py que contiene una función parse_csv(). La función funcionó así:

>>> import fileparse
>>> portfolio = fileparse.parse_csv('Data/portfolio.csv', types=[str,int,float])
>>>

En este momento, la función espera que se le pase un nombre de archivo. Sin embargo, puede hacer que el código sea más flexible. Modifique la función para que funcione con cualquier objeto similar a un archivo / iterable. Por ejemplo:

>>> import fileparse
>>> import gzip
>>> with gzip.open('Data/portfolio.csv.gz', 'rt') as file:
...      port = fileparse.parse_csv(file, types=[str,int,float])
...
>>> lines = ['name,shares,price', 'AA,100,34.23', 'IBM,50,91.1', 'HPE,75,45.1']
>>> port = fileparse.parse_csv(lines, types=[str,int,float])
>>>

En este nuevo código, ¿qué sucede si pasa un nombre de archivo como antes?

>>> port = fileparse.parse_csv('Data/portfolio.csv', types=[str,int,float])
>>> port
... mire la salida (debería estar algo alocada) ...
>>>

Sí, debe tener cuidado. ¿Podría agregar un control de seguridad para evitar esto?

Ejercicio 3.18: Arreglar funciones existentes

Corrija las funciones read_portfolio() y read_prices() en el archivo report.py para que funcionen con la versión modificada de parse_csv(). Esto solo debería implicar una pequeña modificación. Posteriormente, sus programas report.py y pcost.py deberían funcionar de la misma manera que siempre.