Member of The Internet Defense League Últimos cambios
Últimos Cambios
Blog personal: El hilo del laberinto Geocaching

Decoración de "sockets" utilizados internamente por la librería estándar

Última Actualización: 03 de abril de 2007 - Martes

Python es un lenguaje de programación orientado a objetos, moderno y "hermoso". Como cualquier otro lenguaje orientado a objetos que se precie, incorpora el concepto de herencia. Mediante herencia podemos modificar el comportamiento de un objeto fácilmente.

El problema surge cuando los objetos que nos interesan son instanciados por una librería intermedia. Lo evidente es modificar la librería, pero eso rompe el encapsulamiento. Una posibilidad interesante sería que la librería estándar de python utilizase constructores opcionales, pero no es el caso.

¿Qué nos queda?.

Python permite modificar los objetos de forma dinámica. Es posible, por ejemplo, modificar los objetos de un módulo, de forma que cuando sean instanciados por la librería (que no hemos alterado), se creen instancias alteradas.

Veamos el siguiente código, que llamo "socket_decorator.py":

# $Id: socket_decorator.py 120 2007-04-03 01:10:03Z jcea $

from __future__ import with_statement

import sys,socket,threading,time

socket.socket_old=socket.socket

lock_conexiones=threading.Lock()
locks_conexiones={}
LOCK_LIMPIEZA=3600 # Segundos
lock_limpieza=time.time()+LOCK_LIMPIEZA

class socket_new(socket.socket_old) :
  def connect(self,*args,**kwargs) :
    global lock_conexiones,locks_conexiones,lock_limpieza,time

    correo=False
    if args[0][1]==25 :
      correo=True
      with lock_conexiones :
        if time.time()>lock_limpieza :
          a=filter(lambda x: not x[1].locked(),locks_conexiones.items())
          for i,j in a :
            del locks_conexiones[i]
          lock_limpieza=time.time()+LOCK_LIMPIEZA
        l=locks_conexiones.get(args[0][0],None)
        if l==None :
          l=locks_conexiones[args[0][0]]=threading.Lock()

      l.acquire()
      self.lock_ip=l
      self.direccion=args[0][0]

      try :
        self.bind(("IP_DE_SALIDA",0)) # Salimos con una IP concreta, pero un puerto arbitrario
      except :
        import sys
        print >>sys.stderr,"No podemos utilizar la direccion IP especificada: "+str(sys.exc_info()[1])
        raise

    if correo : self.settimeout(30)
    socket.socket_old.connect(self,*args,**kwargs)
    if correo : self.settimeout(15*60)

  def __del__(self) :
    if hasattr(self,"lock_ip") :
      self.lock_ip.release()

socket.socket=socket_new

Este código forma parte de mi proyecto "mmailer", un sistema masivo de envío de correo electrónico. No, no se usa para envíar Spam; estoy muy concienciado con el asunto y todo es muy legal, con contratos de por medio cumpliendo la LOPD.

La cuestión es que "mmailer" utiliza la librería estándar python "smtplib", que es la que instancia los "sockets".

Me interesan que esos sockets se conecten con el exterior utilizando una IP determinada, que se limiten a una conexión por máquina destino y que implementen "timeouts" razonables. De hecho quiero dos "timeouts" distintos: uno para el establecimiento de la conexión y otro para el intercambio de datos en sí.

No quiero modificar la "smtplib" para que instancie los objetos que me interesan. Pero, en cambio, puedo sobreescribir los objetos "socket" en memoria, reemplazando los normales, de forma que cuando la "smtplib" instancie sus "sockets" de toda la vida, en realidad esté instanciando los míos.

Y eso es lo que hace el código mostrado: creamos una clase nueva con el comportamiento que deseamos, y luego reemplazamos la original. En Python esta operación es simple y obvia; en otros lenguajes es sencillamente imposible :-).

Para modificar los "sockets" basta poner en nuestro código "import socket_decorator". A partir de ese momento, los "sockets" que se instancien utilizarán nuestro código, no el original.

Este código es para Python 2.5. No pretende realizar una sustitución "limpia", pero funciona y es bastante evidente. Sirva como ejemplo de la flexibilidad del lenguaje y de cómo hacer algo que, en otros lenguajes, obligaría a reimplementar la librería "smtplib".

¿Por qué no heredar de "smtplib" y alterar su comportamiento?. La razón principal es que el método "connect()" de "smtplib" es bastante sofisticado (por ejemplo, intenta conectar a las diferentes IPs que puede tener un mismo nombre de máquina). Si lo reimplemento, perdería esas funcionalidades, y si copio el código original, estaría dependiendo de una implementación concreta del objeto, que podría variar en versiones futuras de Python.



Python Zope ©2007 jcea@jcea.es

Más información sobre los OpenBadges

Donación BitCoin: 19niBN42ac2pqDQFx6GJZxry2JQSFvwAfS