7. Temas Avanzados

En esta sección, veremos una pequeña colección de algunas características avanzadas de Python que posiblemente encontremos en nuestra programación cotidiana. Los temas en esta sección son sólo una introducción a estas ideas.

7.1 Argumentos variables

Esta sección cubre los argumentos de funciones variadas, a veces descritos como argsy *kwargs.

7.1.1 Argumentos de variables posicionales (*args)

Se dice que una función que acepta cualquier número de argumentos usa argumentos variables. Por ejemplo:

def f(x, *args):
    ...

Llamada de función.

f(1,2,3,4,5)

Los argumentos adicionales se pasan como una tupla.

def f(x, *args):
    # x -> 1
    # args -> (2,3,4,5)

7.1.2 Argumentos de variables de palabra clave (**kwargs)

Una función también puede aceptar cualquier número de argumentos de palabras clave. Por ejemplo:

def f(x, y, **kwargs):
    ...

Llamada de función.

f(2, 3, flag=True, mode='fast', header='debug')

Las palabras clave adicionales se pasan en un diccionario.

def f(x, y, **kwargs):
    # x -> 2
    # y -> 3
    # kwargs -> { 'flag': True, 'mode': 'fast', 'header': 'debug' }

7.1.3 Combinando ambos

Una función también puede aceptar cualquier número de argumentos variables de palabras clave y no palabras clave.

def f(*args, **kwargs):
    ...

Llamada de función.

f(2, 3, flag=True, mode='fast', header='debug')

Los argumentos se separan en componentes posicionales y de palabras clave

def f(*args, **kwargs):
    # args = (2, 3)     # kwargs -> { 'flag': True, 'mode': 'fast', 'header': 'debug' }     ...

Esta función toma cualquier combinación de argumentos posicionales o de palabras clave. A veces se usa al escribir envoltorios o cuando desea pasar argumentos a otra función.

7.1.4 Pasar tuplas y dictados

Las tuplas se pueden expandir en argumentos variables.

numbers = (2,3,4)
f(1, *numbers)      # Same as f(1,2,3,4)

Los diccionarios también se pueden expandir a argumentos de palabras clave.

options = {
    'color' : 'red',
    'delimiter' : ',',
    'width' : 400
}
f(data, **options)
# Same as f(data, color='red', delimiter=',', width=400)

7.1.5 Ejercicios

Ejercicio 7.1: un ejemplo simple de argumentos variables

Intente definir la siguiente función:

>>> def avg(x,*more):
        return float(x+sum(more))/(1+len(more))

>>> avg(10,11)
10.5
>>> avg(3,4,5)
4.0
>>> avg(1,2,3,4,5,6)
3.5
>>>

Observe cómo el parámetro *morerecopila todos los argumentos adicionales.

Ejercicio 7.2: Pasar tuplas y dictados como argumentos

Suponga que lee algunos datos de un archivo y obtiene una tupla como esta:

>>> data = ('GOOG', 100, 490.1)
>>>

Ahora, suponga que desea crear un objeto Stock a partir de estos datos. Si intenta pasar data directamente, no funciona:

>>> from stock import Stock
>>> s = Stock(data)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: __init__() takes exactly 4 arguments (2 given)
>>>

Esto se soluciona fácilmente usando *data en su lugar. Prueba esto:

>>> s = Stock(*data)
>>> s
Stock('GOOG', 100, 490.1)
>>>

Si tiene un diccionario, puede utilizar ** en su lugar. Por ejemplo:

>>> data = { 'name': 'GOOG', 'shares': 100, 'price': 490.1 }
>>> s = Stock(**data)
Stock('GOOG', 100, 490.1)
>>>

Ejercicio 7.3: Crear una lista de instancias

En su programa report.py, creó una lista de instancias usando un código como este:

def read_portfolio(filename):
    ''' Read a stock portfolio file into a list of dictionaries with keys name, shares, and price. '''
    with open(filename) as lines:
        portdicts = fileparse.parse_csv(lines,
                               select=['name','shares','price'],
                               types=[str,int,float])

    portfolio = [ Stock(d['name'], d['shares'], d['price'])
                  for d in portdicts ]
    return Portfolio(portfolio)

Puede simplificar ese código usando Stock(**d). Haz ese cambio.

Ejercicio 7.4: Transferencia de argumentos

La función fileparse.parse_csv() tiene algunas opciones para cambiar el delimitador de archivos y para informar de errores. Tal vez le gustaría exponer esas opciones a la función read_portfolio() anterior. Haz este cambio:

def read_portfolio(filename, **opts):
    '''
    Read a stock portfolio file into a list of dictionaries with keys
    name, shares, and price.
    '''
    with open(filename) as lines:
        portdicts = fileparse.parse_csv(lines,
                                        select=['name','shares','price'],
                                        types=[str,int,float],
                                        **opts)

    portfolio = [ Stock(**d) for d in portdicts ]
    return Portfolio(portfolio)

Una vez que haya realizado el cambio, intente leer un archivo con algunos errores:

>>> import report
>>> port = report.read_portfolio('Data/missing.csv')
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: ''
>>>

Ahora, intente silenciar los errores:

>>> import report
>>> port = report.read_portfolio('Data/missing.csv', silence_errors=True)
>>>

7.2 Funciones anónimas y lambda

7.2.1 Clasificación de listas revisada

Las listas se pueden ordenar in situ . Usando el sortmétodo.

s = [10,1,7,3]
s.sort() # s = [1,3,7,10]

Puede ordenar en orden inverso.

s = [10,1,7,3]
s.sort(reverse=True) # s = [10,7,3,1]

Parece bastante simple. Sin embargo, ¿cómo ordenamos una lista de dictados?

[{'name': 'AA', 'price': 32.2, 'shares': 100},
{'name': 'IBM', 'price': 91.1, 'shares': 50},
{'name': 'CAT', 'price': 83.44, 'shares': 150},
{'name': 'MSFT', 'price': 51.23, 'shares': 200},
{'name': 'GE', 'price': 40.37, 'shares': 95},
{'name': 'MSFT', 'price': 65.1, 'shares': 50},
{'name': 'IBM', 'price': 70.44, 'shares': 100}]

¿Con qué criterios?

Puede guiar la clasificación utilizando una función clave . La función clave es una función que recibe el diccionario y devuelve el valor de interés para ordenar.

def stock_name(s):
    return s['name']

portfolio.sort(key=stock_name)

Aquí está el resultado.

# Check how the dictionaries are sorted by the `name` key [
  {'name': 'AA', 'price': 32.2, 'shares': 100},
  {'name': 'CAT', 'price': 83.44, 'shares': 150},
  {'name': 'GE', 'price': 40.37, 'shares': 95},
  {'name': 'IBM', 'price': 91.1, 'shares': 50},
  {'name': 'IBM', 'price': 70.44, 'shares': 100},
  {'name': 'MSFT', 'price': 51.23, 'shares': 200},
  {'name': 'MSFT', 'price': 65.1, 'shares': 50}
]

7.2.2 Funciones de devolución de llamada

En el ejemplo anterior, la función de tecla es un ejemplo de función de devolución de llamada. El método sort() "vuelve a llamar" a una función que usted proporciona. Las funciones de devolución de llamada suelen ser funciones breves de una línea que solo se utilizan para esa operación. Los programadores a menudo piden un atajo para especificar este procesamiento adicional.

7.2.3 Lambda: funciones anónimas

Utilice una lambda en lugar de crear la función. En nuestro ejemplo de clasificación anterior.

portfolio.sort(key=lambda s: s['name'])

Esto crea una función sin nombre que evalúa una sola expresión. El código anterior es mucho más corto que el código inicial.

def stock_name(s):
    return s['name']

portfolio.sort(key=stock_name)
# vs lambda
portfolio.sort(key=lambda s: s['name'])

7.2.4 Usando lambda

  • lambda está muy restringido.
  • Solo se permite una expresión.
  • No hay declaraciones como if, while, etc.
  • El uso más común es con funciones como sort().

7.2.5 Ejercicios

Lea algunos datos de la cartera de acciones y conviértalos en una lista:

>>> import report
>>> portfolio = list(report.read_portfolio('Data/portfolio.csv'))
>>> for s in portfolio:
        print(s)

Stock('AA', 100, 32.2)
Stock('IBM', 50, 91.1)
Stock('CAT', 150, 83.44)
Stock('MSFT', 200, 51.23)
Stock('GE', 95, 40.37)
Stock('MSFT', 50, 65.1)
Stock('IBM', 100, 70.44)
>>>

Ejercicio 7.5: ordenar en un campo

Pruebe las siguientes afirmaciones que ordenan los datos de la cartera alfabéticamente por nombre de la acción.

>>> def stock_name(s):
       return s.name

>>> portfolio.sort(key=stock_name)
>>> for s in portfolio:
           print(s)

... inspeccione los resultados ...
>>>

En esta parte, la stock_name()función extrae el nombre de una acción de una sola entrada en la portfoliolista. sort() usa el resultado de esta función para hacer la comparación.

Ejercicio 7.6: ordenar en un campo con lambda

Intente ordenar la cartera de acuerdo con la cantidad de acciones usando una lambdaexpresión:

>>> portfolio.sort(key=lambda s: s.shares)
>>> for s in portfolio:
        print(s)

... inspeccione los resultados ...
>>>

Intente ordenar la cartera según el precio de cada acción

>>> portfolio.sort(key=lambda s: s.price)
>>> for s in portfolio:
        print(s)

... inspeccione los resultados ...
>>>

Nota: lambdaes un atajo útil porque le permite definir una función de procesamiento especial directamente en la llamada a sort() en lugar de tener que definir primero una función separada.

7.3 Función de retorno y cierres

Esta sección presenta la idea de usar funciones para crear otras funciones.

7.3.1 Introducción

Considere la siguiente función.

def add(x, y):
    def do_add():
        print('Adding', x, y)
        return x + y
    return do_add

Esta es una función que devuelve otra función.

>>> a = add(3,4)
>>> a
<function do_add at 0x6a670>
>>> a()
Adding 3 4
7

7.3.2 Variables locales

Observe cómo la función interna se refiere a variables definidas por la función externa.

def add(x, y):
    def do_add():
        # `x` and `y` are defined above `add(x, y)`
        print('Adding', x, y)
        return x + y
    return do_add

Observe además que esas variables se mantienen vivas de alguna manera después de que add()ha terminado.

>>> a = add(3,4)
>>> a
<function do_add at 0x6a670>
>>> a()
Adding 3 4      # Where are these values coming from?
7

7.3.3 Cierres

Cuando se devuelve una función interna como resultado, esa función interna se conoce como cierre .

def add(x, y):
    # `do_add` is a closure
    def do_add():
        print('Adding', x, y)
        return x + y
    return do_add

Característica esencial: un cierre conserva los valores de todas las variables necesarias para que la función se ejecute correctamente más adelante. Piense en un cierre como una función más un entorno adicional que contiene los valores de las variables de las que depende.

7.3.4 Usando cierres

El cierre es una característica esencial de Python. Sin embargo, su uso suele ser sutil. Aplicaciones habituales:

  • Utilizar en funciones de devolución de llamada.
  • Evaluación retrasada.
  • Funciones de decorador (más tarde).

7.3.5 Evaluación retrasada

Considere una función como esta:

def after(seconds, func):
    import time
    time.sleep(seconds)
    func()

Ejemplo de uso:

def greeting():
    print('Hello Guido')

after(30, greeting)

after ejecuta la función proporcionada más tarde.

Los cierres llevan información adicional.

def add(x, y):
    def do_add():
        print(f'Adding {x} + {y} -> {x+y}')
    return do_add

def after(seconds, func):
    import time
    time.sleep(seconds)
    func()

after(30, add(2, 3))
# `do_add` has the references x -> 2 and y -> 3

7.3.6 Repetición de código

Los cierres también se pueden utilizar como técnica para evitar la repetición excesiva de código. Puede escribir funciones que hacen código.

7.3.7 Ejercicios

Ejercicio 7.7: Uso de cierres para evitar la repetición

Una de las características más poderosas de los cierres es su uso para generar código repetitivo. Si vuelve a consultar el ejercicio 5.7 , recuerde el código para definir una propiedad con verificación de tipos.

class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price
    ...
    @property
    def shares(self):
        return self._shares

    @shares.setter
    def shares(self, value):
        if not isinstance(value, int):
            raise TypeError('Expected int')
        self._shares = value
    ...

En lugar de escribir ese código repetidamente una y otra vez, puede crearlo automáticamente usando un cierre.

Haga un archivo typedproperty.pyy coloque el siguiente código en él:

# typedproperty.py
def typedproperty(name, expected_type):
    private_name = '_' + name
    @property
    def prop(self):
        return getattr(self, private_name)

    @prop.setter
    def prop(self, value):
        if not isinstance(value, expected_type):
            raise TypeError(f'Expected {expected_type}')
        setattr(self, private_name, value)

    return prop

Ahora, pruébelo definiendo una clase como esta:

from typedproperty import typedproperty

class Stock:
    name = typedproperty('name', str)
    shares = typedproperty('shares', int)
    price = typedproperty('price', float)

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

Intente crear una instancia y verificar que la verificación de tipos funcione.

>>> s = Stock('IBM', 50, 91.1)
>>> s.name
'IBM'
>>> s.shares = '100'
... should get a TypeError ...
>>>

Ejercicio 7.8: Simplificación de llamadas a funciones

En el ejemplo anterior, los usuarios pueden encontrar llamadas typedproperty('shares', int)un poco detalladas para escribir, especialmente si se repiten mucho. Agregue las siguientes definiciones al typedproperty.pyarchivo:

String = lambda name: typedproperty(name, str)
Integer = lambda name: typedproperty(name, int)
Float = lambda name: typedproperty(name, float)

Ahora, reescribe la Stockclase para usar estas funciones en su lugar:

class Stock:
    name = String('name')
    shares = Integer('shares')
    price = Float('price')

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

Ah, eso es un poco mejor. La principal conclusión aquí es que los cierres y lambda, a menudo, se pueden usar para simplificar el código y eliminar la repetición molesta. Suele ser bueno.

Ejercicio 7.9: Poniéndolo en práctica

Vuelva a escribir la clase Stock en el archivo stock.py para que utilice propiedades escritas como se muestra.

7.4 Decoradores de funciones

Esta sección presenta el concepto de decorador. Este es un tema avanzado para el que solo arañamos la superficie.

7.4.1 Ejemplo de registro

Considere una función.

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

Ahora, considere la función con algunos registros agregados.

def add(x, y):
    print('Calling add')
    return x + y

Ahora una segunda función también con algunos registros.

def sub(x, y):
    print('Calling sub')
    return x - y

7.4.2 Observación

Observación: es algo repetitivo.

Escribir programas donde hay mucha replicación de código a menudo es realmente molesto. Son tediosos de escribir y difíciles de mantener. Especialmente si decide que desea cambiar cómo funciona (es decir, quizás un tipo diferente de registro).

7.4.3 Código que hace el registro

Quizás pueda crear una función que haga funciones con registro agregado a ellas. Una envoltura.

def logged(func):
    def wrapper(*args, **kwargs):
        print('Calling', func.__name__)
        return func(*args, **kwargs)
    return wrapper

Ahora úsalo.

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

logged_add = logged(add)

¿Qué sucede cuando llamas a la función devuelta por logged?

logged_add(3, 4)      # Usted ve que el mensaje aparece

Este ejemplo ilustra el proceso de creación de la denominada función contenedora .

Una envoltura es una función que envuelve otra función con algunos bits adicionales de procesamiento, pero por lo demás funciona exactamente de la misma manera que la función original.

>>> logged_add(3, 4)
Calling add   # Extra output. Added by the wrapper 7
>>>

Nota: La logged() función crea el contenedor y lo devuelve como resultado.

7.4.4 Decoradores

Poner envoltorios alrededor de funciones es extremadamente común en Python. Tan común que tiene una sintaxis especial.

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

# Sintaxis especial @logged
def add(x, y):
    return x + y

La sintaxis especial realiza exactamente los mismos pasos que se muestran arriba. Un decorador es solo una nueva sintaxis. Se dice que decora la función.

Comentario

Hay muchos más detalles sutiles para los decoradores de los que se han presentado aquí. Por ejemplo, usándolos en clases. O usando múltiples decoradores con una función. Sin embargo, el ejemplo anterior es una buena ilustración de cómo tiende a surgir su uso. Por lo general, es en respuesta al código repetitivo que aparece en una amplia gama de definiciones de funciones. Un decorador puede mover ese código a una definición central.

7.4.5 Ejercicios

Ejercicio 7.10: un decorador de tiempos

Si define una función, su nombre y módulo se almacenan en los atributos __name__ y __module__. Por ejemplo:

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

>>> add.__name__
'add'
>>> add.__module__
'__main__'
>>>

En un archivo timethis.py, escriba una función decoradora timethis(func) que envuelva una función con una capa adicional de lógica que imprima cuánto tarda en ejecutarse una función. Para hacer esto, rodeará la función con llamadas de tiempo como esta:

start = time.time()
r = func(*args,**kwargs)
end = time.time()
print('%s.%s: %f' % (func.__module__, func.__name__, end-start))

Aquí tienes un ejemplo de cómo debería funcionar tu decorador:

>>> from timethis import timethis
>>> @timethis
def countdown(n):
        while n > 0:
             n -= 1

>>> countdown(10000000)
__main__.countdown : 0.076562
>>>

Discusión: Este decorador @timethis se puede colocar delante de cualquier definición de función. Por lo tanto, puede usarlo como una herramienta de diagnóstico para ajustar el rendimiento.

7.5 Métodos estáticos y de clase

En esta sección se analizan algunos decoradores integrados que se utilizan en combinación con definiciones de métodos.

7.5.1 Decoradores predefinidos

Hay decoradores predefinidos que se utilizan para especificar tipos especiales de métodos en las definiciones de clases.

class Foo:
    def bar(self,a):
        ...

    @staticmethod
    def spam(a):
        ...

    @classmethod
    def grok(cls,a):
        ...

    @property
    def name(self):
        ...

Vayamos uno por uno.

7.5.2 Métodos estáticos

@staticmethod se utiliza para definir los llamados métodos de clase estática (de C ++ / Java). Un método estático es una función que forma parte de la clase, pero que no opera en instancias.

class Foo(object):
    @staticmethod
    def bar(x):
        print('x =', x)

>>> Foo.bar(2) x=2
>>>

Los métodos estáticos se utilizan a veces para implementar código de soporte interno para una clase. Por ejemplo, código para ayudar a administrar las instancias creadas (administración de memoria, recursos del sistema, persistencia, bloqueo, etc.). También son utilizados por ciertos patrones de diseño (no discutidos aquí).

7.5.3 Métodos de clase

@classmethod se utiliza para definir métodos de clase. Un método de clase es un método que recibe el objeto de clase como primer parámetro en lugar de la instancia.

class Foo:
    def bar(self):
        print(self)

    @classmethod
    def spam(cls):
        print(cls)

>>> f = Foo()
>>> f.bar()
<__main__.Foo object at 0x971690>   # La instancia `f`
>>> Foo.spam()
<class '__main__.Foo'> # La clase `Foo`
>>>

Los métodos de clase se utilizan con mayor frecuencia como una herramienta para definir constructores alternativos.

class Date:
    def __init__(self,year,month,day):
        self.year = year
        self.month = month
        self.day = day

    @classmethod
    def today(cls):
        # Notice how the class is passed as an argument
        tm = time.localtime()
        # And used to create a new instance
        return cls(tm.tm_year, tm.tm_mon, tm.tm_mday)

d = Date.today()

Los métodos de clase resuelven algunos problemas complicados con características como la herencia.

class Date:
    ...
    @classmethod
    def today(cls):
        # Gets the correct class (e.g. `NewDate`)
        tm = time.localtime()
        return cls(tm.tm_year, tm.tm_mon, tm.tm_mday)

class NewDate(Date):
    ...

d = NewDate.today()

7.5.4 Ejercicios

Ejercicio 7.11: Métodos de clase en la práctica

En sus archivos report.py y portfolio.py, la creación de un objeto Portfolio es un poco confusa. Por ejemplo, el programa report.py tiene un código como este:

def read_portfolio(filename, **opts):
    '''
    Read a stock portfolio file into a list of dictionaries with keys name, shares, and price.
    '''
    with open(filename) as lines:
        portdicts = fileparse.parse_csv(lines,
                                        select=['name','shares','price'],
                                        types=[str,int,float],
                                        **opts)

    portfolio = [ Stock(**d) for d in portdicts ]
    return Portfolio(portfolio)

y el archivo portfolio.py se define Portfolio() con un inicializador extraño como este:

class Portfolio:
    def __init__(self, holdings):
        self.holdings = holdings
    ...

Francamente, la cadena de responsabilidad es un poco confusa porque el código está disperso. Si Portfolio se supone que una clase contiene una lista de instancias Stock, tal vez debería cambiar la clase para que sea un poco más clara. Me gusta esto:

# portfolio.py
import stock

class Portfolio:
    def __init__(self):
        self.holdings = []

    def append(self, holding):
        if not isinstance(holding, stock.Stock):
            raise TypeError('Expected a Stock instance')
        self.holdings.append(holding)
    ...

Si desea leer un portafolio de un archivo CSV, tal vez debería crear un método de clase para él:

# portfolio.py
import fileparse
import stock

class Portfolio:
    def __init__(self):
        self.holdings = []

    def append(self, holding):
        if not isinstance(holding, stock.Stock):
            raise TypeError('Expected a Stock instance')
        self.holdings.append(holding)

    @classmethod
    def from_csv(cls, lines, **opts):
        self = cls()
        portdicts = fileparse.parse_csv(lines,
                                        select=['name','shares','price'],
                                        types=[str,int,float],
                                        **opts)

        for d in portdicts:
            self.append(stock.Stock(**d))

        return self

Para usar esta nueva clase Portfolio, ahora puede escribir código como este:

>>> from portfolio import Portfolio
>>> with open('Data/portfolio.csv') as lines:
...     port = Portfolio.from_csv(lines)
...
>>>

Realice estos cambios en la clase Portfolio y modifique el código report.py para usar el método de la clase.