Automágica: durante 2017 estoy trabajando bastante en Automágica, mi software para editar libros: Más información - Posts relacionados

Entendiendo *args y **kwargs en Python

Me llegó un mail consultando sobre args y *kwargs en Python. ¿Qué es eso? Vamos por partes.

En Python existen varias formas de llamar a una función. Por ejemplo, en su definición puede tener valores por defecto, entonces no es necesario utilizarlos todos al llamarla:

>>> def f(a, b, c=3):

... print a, b, c

...

>>> f(1,2)

1 2 3

Se pueden utilizar palabras claves como argumento, por supuesto, respetando algunas reglas:

>>> def f(a, b, c):

...     print a, b, c

...

>>> f(a=1, b=2, c=3)

1 2 3

>>> f(a=1, 2, 3)

  File "", line 1

SyntaxError: non-keyword arg after keyword arg

>>> f(1, 2, b=3)

Traceback (most recent call last):

  File "", line 1, in

TypeError: f() got multiple values for keyword argument 'b'

>>> f(1, b=2, c=3)

1 2 3

Y finalmente, se puede llamar con listas arbitrarias de argumentos. Como en los casos anteriores, poder hacerlo depende de cómo hayamos definido la función:

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

...     print "args:", args

...     print "kwargs:", kwargs

...

Con args se indica, mapear todos los argumentos posicionales a una tupla llamada args. Y con *kwargs se indica, mapear todos los argumentos de palabra clave a un diccionario llamado kwargs.

>>> f(1,2,3)

args: (1, 2, 3)

kwargs: {}

>>> f(1,2,3, cuatro=4)

args: (1, 2, 3)

kwargs: {'cuatro': 4}

>>> f(cuatro=4)

args: ()

kwargs: {'cuatro': 4}

Notemos que se pueden definir argumentos con nombre propio antes de los argumentos agrupados y que no es necesario usar exactamente los nombres args y kwargs. También puede usarse solo uno de los dos:

>>> def f(p, *a, **kw):

...     print "p:", p

...     print "a:", a

...     print "kw:", kw

...

>>> f(1,2,3)

p: 1

a: (2, 3)

kw: {}

>>> f(1,2,3, cuatro=4)

p: 1

a: (2, 3)

kw: {'cuatro': 4}

>>> f(cuatro=4)

Traceback (most recent call last):

  File "", line 1, in

TypeError: f() takes at least 1 non-keyword argument (0 given)

Espero que los ejemplos sirvan. Para más detalle, se pueden consultar las secciones del Tutorial de Python que enlacé.


Twitter updates desde Twisted

Este artículo es una re-edición del publicado el 24/08/2010 ya que cuando lo publiqué, Twitter estaba terminando su proceso apagar el sistema de autenticación básica para pasar al más complejo sistema de la danza oAuth; lo cual convirtió al artículo en inservible. El artículo anterior no está más disponible. Gracias a Pupeno por revisar esta versión.

¿Tenés un servidor escrito en Twisted? ¿Tenés eventos críticos o importantes que mandás por mail o a celulares? ¿Qué tal publicarlos en Twitter?

La versión original de este artículo utilizaba la biblioteca Twitty Twister, la única pensada para usar Twitter desde Twisted. Lamentablemente no funciona bien con el nuevo sistema de autenticación de Twitter por lo que voy a usar Tweepy, una librería con la cual hacer la danza de oAuth es muy sencillo.

1) Registrar un usuario dónde publicar las notificaciones.

2) Con ese usuario, registrar una aplicación. Es muy importante usar el mismo usuario para ahorrarnos algunos pasos en la danza oAuth.

3) De la página de la aplicación creada, tomar la siguiente información: consumer key, consumer secret (desde la home de la aplicación), access token y access token secret (de la página My Access Token).

4) Código:

Con los datos del punto 3, creamos el módulo twitterupdates.py:

import tweepy



TWITTER_KEY = 'xxxxxxxxxxxxxxxx'

TWITTER_SECRET = 'xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'

MY_KEY = 'xxxxxxxxxxxxxxxxxxxxxxxx'

MY_SECRET = 'xxxxxxxxxxxxxxxxxxxxxxxxxx'



auth = tweepy.OAuthHandler(TWITTER_KEY, TWITTER_SECRET)

auth.set_access_token(MY_KEY, MY_SECRET)

twitter = tweepy.API(auth)

y desde nuestra aplicación Twisted lo importamos:

from twitterupdates import twitter

Postear una actualización en Twitter o cualquier otra petición a un servidor externo tiene una naturaleza bloqueante y en un framework de concurrencia colaborativa, como es Twisted, no podemos darnos ese lujo. Para solucionar esto, utilizamos deferToThread:

deferToThread(twitter.update_status, "Mensaje a postear en Twitter")

Esta llamada retorna un objeto Deferred al cual se le pueden añadir callbacks o errbacks. La llamada bloqueante es corrida en un hilo aparte.

Enjoy!


Un servidor web con pocas líneas de Python

Hoy un amigo necesitaba un servidor web para engañar a un programa. Cada vez que el programa iniciaba, se conectaba con un servidor web para verificar si había actualizaciones disponibles.

El nombre del host a dónde se hacía la petición era leído de un archivo de configuración, por lo que lo podíamos cambiar. El resto solo era levantar un servidor web que responda con la información apropiada.

En la librería estándar de Python tenemos todos los elementos necesarios para realizar la tarea. Luego de probar un poco, el resultado final fue algo como esto:

PORT = 8090

from BaseHTTPServer import BaseHTTPRequestHandler

import SocketServer

import cgi

class MyHandler(BaseHTTPRequestHandler):

def do_GET(self):

    self.send_response(200)

    self.end_headers()

        self.wfile.write('1.9.1')



def do_POST(self):

    form = cgi.FieldStorage(

        fp=self.rfile,

        headers=self.headers,

        environ={'REQUEST_METHOD':'POST',

        'CONTENT_TYPE':self.headers['Content-Type'],

        })

    print form

    self.send_response(200)

    self.end_headers()

    self.wfile.write('1.9.1')

httpd = SocketServer.TCPServer(("", PORT), MyHandler)

print "serving at port", PORT

httpd.serve_forever()

Levanta en localhost y responde la cadena '1.9.1' al ser consultado tanto por GET como por POST y en el caso de POST, también imprime los valores recibidos.


argparse - Command line option and argument parsing

Hoy recibí la última edición de Python Module of the Week, un semanal de Doug Hellmann sobre módulos de Python. Leo:

The argparse module was added to Python 2.7 as a replacement

for optparse.

Nunca había usando optparse hasta hace un par de meses y estaba contento de haberlo empezado a usar, ordenaba mis programas de línea de comando con bonitos parámetros. ¿Tengo que aprender otro sistema totalmente nuevo? Veamos.

Esta es una parte de mi programa original:

from optparse import OptionParser

parser = OptionParser(usage="rpcping ip [-p port] [-r rep] [-s size]", version="rpcping 1.0")



parser.add_option("-p", type="int", dest="port", default=PORT)

parser.add_option("-r", type="int", dest="rep", default=REP)

parser.add_option("-s", type="int", dest="size", default=SIZE)

(options, args) = parser.parse_args()



if not args:

    parser.error("se necesita la direccion ip del servidor como argumento posicional.")

else:

    ip = args[0]



if not 1024 < options.port < 65535:

    parser.error("el puerto debe ser mayor a 1024 y menor a 65535.")

else:

    port = options.port



if not 0 < options.rep < 101:

    parser.error("las repeticiones deben ser mayores a 0 y menores a 101.")

else:

    rep = options.rep



if not 0 < options.size < 10001:

    parser.error("el tamaño deben ser mayores a 0 y menores a 10001.")

else:

    size = options.size

Qué tuve que cambiar para que funcione con argparse:

  • Cambiar nombres; optparse por argparse, OptionParser por ArgumentParser, add_option por add_argument.
  • Cambiar el valor del agumento type de string a un tipo de verdad. En mi caso type="int" por type=int.
  • Si tratamos de obtener el resultado del parsing así (options, args) = parser.parse_args(), vamos a tener un error TypeError: 'Namespace' object is not iterable. El nombre result en el nuevo código está asociado a un objeto del tipo Namespace.
  • optparse no trabaja con argumentos obligatorios, por lo que para el argumento ip había tenido que escribir código extra por mi cuenta. En argparse es más simple.

El resultado es:

from argparse import ArgumentParser

parser = ArgumentParser(description="Realiza ping via RPC contra un servidor", version="rpcping 1.0")



parser.add_argument("ip")

parser.add_argument("-p", type=int, dest="port", default=PORT)

parser.add_argument("-r", type=int, dest="rep", default=REP)

parser.add_argument("-s", type=int, dest="size", default=SIZE)

result = parser.parse_args()



ip = result.ip



if not 1024 < result.port < 65535:

    parser.error("el puerto debe ser mayor a 1024 y menor a 65535.")

else:

    port = result.port



if not 0 < result.rep < 101:

    result.error("las repeticiones deben ser mayores a 0 y menores a 101.")

else:

    rep = result.rep



if not 0 < result.size < 10001:

    parser.error("el tamaño deben ser mayores a 0 y menores a 10001.")

else:

    size = result.size

Parece que con poco esfuerzo se puede migrar de optparse a argparse y luego empezar a estudiar las nuevas funcionalidades que incorpora.

The implementation of argparse supports features that would not have been easy to add to optparse, and that would have required backwards-incompatible API changes, so a new module was brought into the library instead. optparse is still supported, but is not likely to receive new features.

Más en: PyMOTW: argparse - Command line option and argument parsing.


functools.update_wrapper

Este post se alinea con la serie Decoradores en Python (I, II, III) pero no es tan elaborado como para ser Decoradores en Python (IV) :)

Desde Python 2.5, al crear un decorador, se puede utilizar functools.update_wrapper para quela versión decorada de la función, tenga los atributos name, doc, module y dict de la función original.

>>> import functools

>>> def deco(f):

... def inner(a, *kw):

... print "Este decorador no hace nada"

... return f(a, *kw)

... return inner

...

>>> def saludo():

... print "hola"

...

>>> saludo2 = deco(saludo)

>>> saludo2()

Este decorador no hace nada

hola

>>> saludo2.name

'inner'

>>> def deco(f):

... def inner(a, *kw):

... print "Este decorador no hace nada"

... return f(a, *kw)

... return functools.update_wrapper(inner, f)

...

>>> saludo3 = deco(saludo)

>>> saludo3()

Este decorador no hace nada

hola

>>> saludo3.name

'saludo'

>>> saludo = saludo3


Puedo disfrazar cualquier cosa de Modbus

Hace más de un año participo en un proyecto de desarrollo de un sistema SCADA, específicamente en la capa de comunicación. La distribución física es algo como esto:

En cada edificio hay una red 485 con dispositivos de hardware propio (aka Robot) y un módulo celular (Motorolla G24) con salida a Internet vía GPRS. Esos módulos se registran en un servidor (escrito con Twisted) para ser luego sondeados: se registran sus mediciones, hay gráficos en tiempo real, se puede hacer telecomando y encender un equipo a la distancia o cambiar un set point; entre otras cosas.

Nuestra primera implementación estaba inspirada en el protocolo Modbus ASCII, pero bastante alejada del estándar. Nos sirvió por bastante tiempo y no apegarnos a una especificación nos permitió acelerar el desarrollo; especialmente del hardware.

Modbus es un protocolo de línea maestro/esclavo en donde el maestro realiza peticiones de lectura o escritura sobre algún o algunos registros en un esclavo. El protocolo define 4 tipos de registros (analógicos o digitales, de solo lectura o lectura-escritura) y fue pensado para funcionar sobre una línea serie; existen dos sabores: RTU, dónde los datos viajan en forma binaria y la ocupación del canal es optimizada y ASCII, menos eficiente pero legible por humanos. Se puedo leer más al respecto en Wikipedia o en la página oficial del protocolo.

Hace algunos meses empezamos a utilizar Mango m2m. Mango tiene todos los componentes de interfaz de usuario que se pueden necesitar en un SCADA en adición a la posibilidad de conectarle distintas fuentes de datos (desde Modbus Serie a un motor de base de datos); las más interesante para nosotros es Modbus TCP.

Modbus TCP extiende el protocolo original para poder utilizarlo sobre la red de redes en lugar de sobre una línea serie. Por ejemplo, una de las cosas que hace es agregar al paquete enviado un número consecutivo para solucionar el problema de paquetes perdidos o duplicados.

Teníamos entonces, por un lado Mango que sabe hablar Modbus TCP y por el otro los robotitos hablando un dialecto de Modbus ASCII. En el medio, el servidor de comunicaciones. La pregunta en este punto era cómo unir ambos extremos, de manera de aprovechar esta herramienta que soluciona muchos de nuestros requerimientos.

Allí es donde entra en acción pymodbus, una implementación en Python del stack Modbus. No solo está escrita en Python, sino que está implementada utilizando el framework Twisted, el mismo que venía usando para el servidor de comunicaciones. Utilizando pymodbus pude engañar a Mango y hacerle creer que hablaba contar dispositivos Modbus TCP cuando en realidad interceptaba sus peticiones, enviaba los comandos necesarios a los robots y contestaba con la respuesta correcta.

En el camino encontré varios problemas en la librería, los fui reportando y enviando parches para solucionarlos. A lo último eran tanto los cambios que enviaba y empecé a modificar el core de la librería para que funcione con deferrers (es decir, siguiendo la filosofía de Twisted) que recibí permiso de commit en la rama devel.

Nota: el esquema descripto funcionó bastante tiempo. Actualmente seguimos avanzando y los robots ya hablan Modbus ASCII estándar a la vez que el componente intermedio adelgazó para convertirse en un traductor o gateway.


Diccionario bidireccional en Python

Bidirectional dict o Injective mapping es una estructura de datos muy útil.

Por lo general cuando usamos un diccionario o tabla hash, tenemos un valor asociado a una clave:

>>> d = {1:'uno', 2:'dos'}

>>> d[1]

'uno'

Pero algunas veces también resulta útil indexar por el valor y obtener la clave.

>>> d['uno']

1

Por ejemplo, en un programa tengo un archivo de configuración que contiene un diccionario dónde las claves son strings de 3 caracteres representando un edificio ('SFE', 'GDP', ...) y las claves números de puerto (5007, 5008, ...). En algunas partes el programa requiere conocer el edificio a partir del número de puerto y en otras el puerto a partir del edificio.

¿Cómo obtenemos este comportamiento?

Pregunté en StackOverflow y si bien me apuntaron a una implementación, la solución que más me gustó fue esta:

>>> d.update( dict((d[k], k) for k in d))

Así se ve d ahora:

{1: 'uno', 2: 'dos', 'uno': 1, 'dos': 2}

Y podemos efectivamente indexar por clave o por valor:

>>> d[1]

'uno'

>>> d['uno']

1

Advertencias

No se pueden usar objetos mutables

En los diccionarios de Python solo objetos inmutables pueden ser clave de diccionarios, por lo tanto, en nuestro diccionario bidireccional, tanto las claves como los valores deberán serlo.

Actualizaciones

Otra limitación de este enfoque son las desincronizaciones que puede sufrir al modificar el bidict luego de realizar la transformación; hay que tener cuidado!

  • Luego de agregar un nuevo elemento debemos volver a ejecutar la línea mágica para que cree la entrada inversa.
  • No podemos agregar un par que tenga como clave algo que ya existía como valor o que tenga como valor algo que ya existía como clave.
  • Si borramos una entrada, hay que también borrar su inversa.

Conclusión personal

Como les contaba antes, yo estaba usando el diccionario original en un archivo de configuración, por lo que no toco la estructura de datos durante la ejecución del programa, así que esta solución compacta y elegante... como dijo el filósofo: me viene al pelo!

Espero a alguien más le sirva.



Pilas y colas en Python

La forma más directa de tener pilas y colas en Python es usando listas, una de las poderosas estructuras de datos que vienen con el lenguaje.

Una pila es una estructura de datos secuencial en la que el último elemento insertado es el primero en retirarse (LIFO) y puede implementarse directamente con los métodos append y pop.

Una operación común en las pilas es top, que devuelve el elemento en el tope de la pila, pero sin quitarlo; esta operación se puede emular indexando por -1.

>>> pila = [6,7,8]

>>> pila.append(9)

>>> pila

[6, 7, 8, 9]

>>> pila.pop()

9

>>> pila.pop()

8

>>> pila[-1]

7

Implementar una cola requiere algo más. En las colas, a diferencia de las pilas, el primer valor insertado es el primero en quitarse (FIFO).

Un primer intento es implementarla con append y pop(0) que obtiene el primer elemento (o con insert insertando al principio y pop). Pero las listas de Python, si bien están optimizadas para insertar al final y quitar del final, no lo están para insertar al principio o quitar del principio. La solución es utilizar collections.deque que si está implementada con estas operaciones optimizadas.

>>> from collections import deque

>>> cola = deque([1,2,3])

>>> cola.append(4)

>>> cola

deque([1, 2, 3, 4])

>>> cola.popleft()

1

>>> cola.popleft()

2

>>> cola

deque([3, 4])


La historia de Python: Y la serpiente ataca

El siguiente texto es una traducción del artículo And the Snake Attacks de Greg Stein publicado en http://python-history.blogspot.com/.

Y la serpiente ataca

Esta bien. Bueno. En 1995, cuando tuve mi primer contacto con Python, cualquier referencia a una "serpiente" estaba prohibida. Python tomaba el nombre de Monty Python, no del reptil. Si alguien estaba atacando, eran Los Caballeros que dicen Ni o posiblemente el Conejo de Caerbannog.

De cualquier manera, volviendo a 1994, yo estaba combatiendo con villanos ficticios en la escena LPMUD. La Web apenas estaba presente, y la banda ancha era algo inaduito. El entretenimiento de bajo ancho de banda estaba a la orden del día.

Esperá. Demos un paso atrás. 1979. Mi primera computadora fue una Apple ][, y uno de mis juegos favoritos era Colossal Cave. Inmediatamente después, conocí y jugué Zork. Yo estaba enamorado del concepto de ficción interactiva y de como una computadora podía llevarte a través de esas historias. Esos juegos me engancharon, y me llevaron a una vida de computadoras. (Y si, ¡te podes imaginar la emoción de reunirme con Don Woods casi 25 años mas tarde!)

Por eso la escena MUD me intereso bastante. Pero yo quería ayudar a hacer esos juegos. Conocí a John Viega, un compañero creador de LPMUD, programador y diseñador. Al mismo tiempo, el estaba trabajando en el Laboratorio de Graficos por Computadora de la Universidad de Virgina, en un sistema llamado Alice. Este sistema estaba pensado para gente poco conocedora, y querian un lenguaje facil de aprender para crear animaciones. Elijieron Python por su claridad, poder y simplicidad.

John era un fanático tremendo, y me encamino a Python. "Tenes que aprender esto!" "Bueno, bueno" decía yo. El lenguaje era fácil, pero poderoso. Podía hacer de todo, sin mucho fastidio.

Era febrero de 1995, y no volví atras.

No sabía en ese momento cuan trascendental seria Python en mi carrera y en mi vida. Gracias, Guido, por tu creación.

Traducido por Joaquín Sorianello.

Revisado por Juan José Conti.

Si encontrás errores en esta traducción, por favor reportalos en un comentario y los corregiremos a la brevedad.

Todas las traducciones de esta serie pueden encontrarse en La historia de Python.