Archive for the 'Python' category

Problemas con los nombres largos en NTFS

abr 04 2012 Published by under Python

Un buen día comenté a un compañero de trabajo que en su carpeta compartida del servidor de ficheros pronto iba a tener problemas al usar nombres de carpetas demasiado largos. El explorador de ficheros ya se negaba a listas algunas carpetas y el problema iba a más con algunas herramientas (backups, antivirus,…).

La respuesta fue una pregunta: ¿Sería posible sacar un listado de todos los ficheros con ruta absoluta demasiado larga?

Después de dudar un rato (y comprobar que el comando dir no era válido para esta labor) , me decidí a averiguar si python sería capaza de realizar dicha tarea. Éste es el resultado de ése estudio.

Problema con los nombres largos

Resulta bastante chocante que la API de windows limite la máxima longitud para una ruta al valor de MAX_PATH, definido como 260 caracteres. Se trata únicamente de una limitación en la API, ya que el kernel de windows está preparado para manejar rutas muchísimo más largas. Como consecuencia, muchas aplicaciones fallan con rutas largas, desde los comandos de terminal hasta las utilidades gráficas del sistema.

Para evitar en parte este problema, podemos usar en nuestras aplicaciones las versiones unicode de las funciones ANSI de la API. Estas versiones unicode admiten como parámetros rutas de hasta 32.767 caracteres, suficientemente largas para un uso normal.

En en caso de la librería estándar de python, el módulo os.path (sinónimo de os.ntpath en windows) tiene buena cuenta de qué API invocar, según sea el caso. De un modo transparente, con sólo codificar las rutas en unicode evitaremos la limitación MAX_PATH en la longitud de las rutas.

Codificación de rutas extendida

Además de codificar las rutas en unicode, tenemos que indicar que se trata de una ruta extendida añadiendo el prefijo '\\?\' a una ruta absoluta. Por ejemplo, "\\?\D:\ruta\muy\muy\larga". Como gran limitación, no se puede usar esta nomenclatura con rutas relativas, por lo que las rutas relativas siempre estarán limitadas a MAX_PATH como máxima longitud.

Es posible, también, indicar una ruta UNC (Universal Naming Convention) como ruta extendida como \\?\server\share, donde server sería el nombre del servidor y share el nombre de la carpeta compartida.

Para más información sobre las rutas extendidas, visitar la página web Naming Files, Paths, and Namespaces de MSDN.

Script para listar ficheros de nombres muy largos

Por último, sólo queda lo más fácil: codificar el script ;-)

Es común que la codificación que lleva por defecto la cónsola de comandos falle con las cadenas unicode. Si pensamos sacar por cónsola los nombres de ficheros, lo recomendable es cambiar la codificación por una más apropiada, como la cp1252, lo más aproximado a utf-8 que podemos encontrar:

    C:\> chcp 1252

Para hacer nuestro script, podríamos haber usado cualquiera de las funciones os.walk de la librería estándar; pero, no sé porqué, fallan a crear la lista de directorios. Ésto nos obliga a crear nuestro propio método para recorrer la jerarquía de directorios.

Por comodidad, voy a lanzar el script en una máquina distinta de la que hace de servidor de ficheros ya que no dispongo de python en los servidores. Usaré la nomenclatura UNC para identificar los recursos compartidos.

#-*- coding: utf-8 -*-

import os

MAX_PATH=260

def longnames(dirpath):
    """Iterador que devuelve las rutas largas (>=MAX_PATH)"""
   
    #Hay que forzar unicode para evitar el límite MAX_PATH
    dirpath=unicode(dirpath)
    if not dirpath.endswith("\\"):
        dirpath+="\\"

    dirs=[]
   
    for f in os.listdir(dirpath):
       
        name = dirpath+f
       
        if len(name)>=MAX_PATH:
            yield name

        #poblar la lista de directorios
        #para recorrer en orden
        if os.path.isdir(name):
            dirs.append(name)

    for d in dirs:
        for f in longnames(d):
            yield f

def listshare(server, share):
    """Crea un UNC para el recurso compartido.
       Devuelve un iterador para las rutas largas (>=MAX_PATH)
    """
   
    dirpath=r"\\?\UNC\%s\%s"%(server, share)
    return longnames(dirpath)

Su uso simple podía ser:

#-*- coding: utf-8 -*-

import codecs

#PARÁMETROS
SERVER="MiServidor"
SHARE=r"Usuario\dir1"

fOut=codecs.open("longnames.txt", "w", encoding="utf-8")

for name in listshare(SERVER,SHARE):
   
    print name
    print >>fOut, name.replace(DIRPATH,".\\")
   
fOut.close()

No responses yet

Codificando en binario

ene 26 2012 Published by under Python

A raiz de la consulta de un colega, me ha llamado la atención el módulo binascii. Este módulo se encarga de la codificación y decodificación de cadenas binarias para su transmisión en mensajes de texto. Lo habitual es que sea usado por otros módulos como uu, base64 o binhex, por lo que no es nada frecuente ver su uso directo en una aplicación.

Sin embargo, binascii posee algunas funciones que pueden sernos bastante útiles a la hora de simplificar el empaquetado de datos que requieren determinadas APIs, en lugar de usar estructuras más complejas como array o struct. También se revela muy útil a la hora de crear batería de tests donde necesitemos chequear valores binarios.

Estructuras array y struct

Por ejemplo, imaginemos que nuestra API nos pide que empaquetemos un número entero como cuatro bytes. Antes de python3 no existía una forma para controlar el tamaño en bytes de un objeto sin tener que recurrir a alguna estructura especializada. Por ejemplo:

from array import array

def numpack(num):
    a=array('L')
    a.append(num)
    return a.tostring()[::-1]

n=numpack(0xffeeddcc)  # res: \xff\xee\xdd\xcc

En el resultado final ha hecho falta invertir el orden de los bytes debido a que nos estaba usando un orden “little-endian” para su codificación. El orden puede depender del sistema donde estemos trabajando, con lo que no es nada seguro usar este método. Es preferible el uso más especializado de struct donde tendremos algo más de control sobre éste y muchos otros aspectos:

from struct import pack

def numpack(num):
    return pack('!L', num)

n=numpack(0xffeeddcc)  # res: \xff\xee\xdd\xcc

Nota que en la cadena de formato que se pasa a pack() tiene un indicador '!' al principio, con el que indicamos que queremos una ordenación “network (=big-endian)”.

El proceso inverso es tan fácil como usar la función complementaria unpack:

unpack('!L', n)[0]

Codificando mensajes

Lo visto hasta ahora funciona bien cuando tenemos que interaccionar con una API que use tamaños fijos para los datos. Pero, ¿qué pasa cuando los datos son de longitud variable, por ejemplo, un mensaje largo de decenas de bytes? En el mejor de los casos, tendríamos que ir byte a byte, tal vez apoyándonos en array o struct para realizar las conversiones, algo a todas luces resulta bastante tedioso.

Como ya adelanté, el módulo binascii nos va a ayudar en este cometido, en concreto la función b2a_hex y su contraparte a2b_hex.

Por ejemplo, nos envían en un mensaje un entero codificado en multibyte. No sabemos si son 2, 4 u 8 bytes, o incluso podrían ser más bytes de tratarse de un BigInt (entero muy largo). Con binascii podríamos resolverlo así:

from binascii import b2a_hex, a2b_hex

num = int(b2a_hex(msg), 16)

Para el proceso contrario, codificar un entero en una cadena binaria, usaríamos a2b_hex:

h = "%x"%num
if len(h)%2 == 1:
    h = "0" + h

msg = a2b_hex(h)

Hemos tenido cuidado de que la cadena hexadecimal tenga longitud par ya que a2b_hex codifica siempre cada byte a partir de una pareja de dígitos hexadecimales.

Estudio codificaciones unicode

También es posible usar binascii para estudiar las codificaciones de cadenas unicode, lo que hace más sencillo crear tests automáticos para funciones que empleen unicode. Sin muchos adornos, haríamos algo así:

#-*- coding: utf-8 -*-

from binascii import b2a_hex, a2b_hex

cadena = u"Peón \N{BLACK CHESS PAWN}"

print b2a_hex(cadena.encode('utf-8'))

#res: 5065c3b36e20e2999f

Comparando el resultado obtenido con la cadena unicode, vemos que la ó acentuada se codifica en ‘utf-8′ como 0xc3b3, o que la figura de peón negro se codifica como 0xe2999f.

Si cambiamos la codificación por ‘utf-16′ obtenermos como resultado fffe50006500f3006e0020005f26. Además de ser más larga, vemos que se añade al principio fffe, que es lo que se denomina BOM, y que nos indica qué ordenación de bytes se ha usado en la codificación ('FEFF' para Big Endian / 'FFFE' para Little Endian). Con fffe nos indica concretamente que se ha usado la codificación ‘UTF-16 Little Endian’, con lo que tenemos los bytes invertidos para cada caracter codificado (ver más info en el artículo sobre BOM de la wikipedia).

De no desear que se nos añada la marca BOM, podríamos haber forzado la codificación mediante 'utf-16be' ó 'utf-16le' para Big Endian y Little Endian, respectivamente.

No responses yet

web2py, con pilas incluidas

dic 01 2011 Published by under Python

He estado últimamente trabajando en varios proyectos que me han hecho descuidar un poco el blog. En próximos días espero publicar algunos artículos relacionados que creo serán de interés.

Lo último que estoy haciendo es un desarrollo de aplicación de escritorio con web2py, a pesar de tratarse de un framework para crear aplicaciones web. Corre desde un navegador cualquiera, interaccionando con [jQuery] para la presentación, y con python corriendo por debajo para dar soporte a la capas de datos y lógica de negocio.

¿Por qué web2py?

Como indico en el título, web2py lleva las “pilas incluidas”, que tratándose de python significa que incluye todo lo puedas necesitar. ¡Y no exagero! La selección de librerías que incluye es extensa, permitiendo desarrollar casi cualquier aplicación web que necesites, con herramientas administrativas tanto para el modelo de datos como para editar los ficheros vistas y controladores. Vamos, que con descomprimir el fichero con web2py y un navegador ya tienes todo lo necesario para desarrollar la aplicación, desde una simple aplicación estática, hasta una completa aplicación que funcione en el Google AppEngine. Además posee un sistema de plugins que permite cambiar fácilmente el diseño de la web o añadir funcionalidades.

Hasta ahora, había probado algunos GUIs graficos como pyqt o wxpython que incluyeran algún widget html para renderizar páginas webs y poder controlarlas desde el programa python, algo bastante costoso de programar y con serias incompatibilidades a la hora de presentar la página web. Con web2py, la aplicación de escritorio se orienta a una aplicación web con el servidor web corriendo directamente en el cliente. Web2py permite abstraer algunas facilidades ajax y jquery habituales, aunque no resulta difícil usar el resto de funcionalidades de jquery en las aplicaciones sin tener que ser un experto en programación javascript.

Uno de los problemas que también había tenido hasta ahora era en la distribución de aplicaciones python para windows. Habitualmente, hace falta instalar varias herramientas en el sistema, desde el intérprete python hasta todas las librerías necesarias, algunas complicadas de hacerlas funcionar. Bien es cierto que existen formas de empaquetar una aplicación usando py2exe (py2app en MacOS), pero casi siempre hay alguna cosa que no funciona bien del todo. Web2py viene con todo empaquetado para su uso en windows y macOS, incluyendo el intérprete python y resto de librerías necesarias. Basta descomprimir el fichero y, sin intalación, ejecutar el servidor web y abrir un navegador. Las aplicaciones web son carpetas que podemos copiar de una instalación a otra o, aún más sencillo, usar el interface administrativo para empaquetar e instalar aplicaciones (al estilo tomcat).

Échale un vistazo, y no te dejes las “pilas”.

One response so far

Rentrée (nueva temporada)

oct 08 2011 Published by under Pensamientos, Python

Desconectado de mis tareas habituales depués de algunas semanas viajando por Francia, veo que me quedaron varios proyectos y artículos en dique seco que pretendo recuperar. Con la “reentrada” (o, como dirían los franceses, “rentrée”) me he propuesto algunas metas para esta nueva temporada (por llamarla de algún modo) que ahora empiezo.

Junto a los artículos que tengo previstos, intentaré incorporar al blog más comentarios sobre temas técnicos que me vayan surgiendo en el día a día, preferiblemente relacionados con la programación. Sin llegar a la extensión de un artículo, espero que sirvan como gérmen de desarrollos posteriores más extensos.

Como primeras ideas para esta rentrée, he tomados dos decisiones: centrar mis desarrollos en la máquina virtual java (plataforma JVM) y aprender a programar con scala.

Máquina Virtual Java (JVM)

Hoy en día, la JVM está omnipresente para casi cualquier dispositivo y sistema operativo. Su uso empresarial es muy extendido, tanto para desarrollo en el lado servidor como para clientes móviles. Librerías y paquetes suficientemente robustos y probados completan una gran plataforma donde desarrollar cualquier tipo de aplicativo que podamos pensar.

Al evaluar la robustez de las librerías java, hay que tener en cuenta que java y su JVM están en constante evolución. El paso de Java5 a Java6 sido muy lento debido a las pocas ventajas que ofrecía el cambio frente al coste de tener que adaptar el código; pero con Java7 se incorpora a la máquina virtual el poder trabajar con tipos dinámicos de datos, lo que mejorará bastante el rendimiento de los lenguajes de scripting como jython, jruby ó groovy, por poner algunos ejemplos. Este cambio parece independizar el desarrollo de la JVM del lenguaje java para pasar a ser una plataforma común para la ejecución de aplicaciones, sea cual sea el lenguaje que se haya usado (objetivo similar a lo que tenía que haber sido .Net).

En lo personal, desde hace mucho tiempo que estoy programando en jython, tal como comenté en otro artículo. La llegada de los dispositivos android hace aún más interesante la programación para JVM, así como que las numerosas herramientas de software libre que estoy usando estén para esta plataforma. No quiero decir con ésto que renuncie a utilizar la CPython, la máquina virtual “nativa” que lleva python, siempre que sea necesario. Tan sólo priorizo la plataforma, JVM, frente a las últimas implementaciones del lenguaje python. Espero que el proyecto PyPy facilite un único camino para el desarrollo del lenguaje, independiente de la máquina virtual empleada.

Lenguaje Scala

Poco conozco de este lenguaje, la verdad. En el índice [tiobe] de septiembre de 2011 figura en la posición 50, la última posición que entra en valoración. Pero los comentarios que he leído sobre este lenguaje me han picado tanto la curiosidad que he decidido darle un vistazo. Si quieres un consejo: no te limites a un sólo lenguaje de programación. Sólo comparando con otros lenguajes descubrirás las virtudes y limitaciones de los lenguajes que uses. Sobre todo, intenta aprender algún lenguaje “exótico”, si por exótico se entiende aquél que no se ve en los estudios oficiales de informática. Siempre que te pidan mostrar tus conocimientos de programador, saber programar en un lenguaje “exótico” será visto como que te entusiasma la programación y que tienes capacidad para aprender cosas nuevas por tu cuenta (Python sigue siendo un excelente ejemplo de lenguaje para estas demostraciones).

Algunas características interesantes de Scala:

  • Lenguaje funcional orientado a objeto similar a java, pero superando a éste en simplicidad. Incorpora clausuras y tipado perezoso de datos.
  • Escalable (como indica su nombre: scalable language)
  • Emplea la JVM, aunque también hay versión para .Net. Puede usarse en cualquier sitio que se use java como, por ejemplo, para programación en android.
  • Preparado para la programación concurrente. Sigue el modelo “Actor”, o lo que es lo mismo, todos los objetos son “actores” con su propio entorno de ejecución.

Asociacionismo en torno a python

Tangencialmente, he empezado a meterme en la organización de un evento relacionado con python. Creo importante que todos retomemos los contactos personales e intentar hacer reflotar las ilusiones perdidas por esta crisis que estamos viviendo. Si todo va bien, espero vernos en el Día Python 2011 en Zaragoza dentro de la LSWC’11.

2 responses so far

Mercurial como cliente DCVS universal

ago 30 2011 Published by under Python

Introducción

Con la llegada de los DCVS (Distributed Concurrent Versions System), se ha convertido en habitual el uso de un sistema de control de versiones en todo desarrollo. La popularización de sitios webs basados en estos sistemas como github, gitorious, bitbucket o googlecode como foros públicos donde compartir código entre programadores hasta el punto de convertirse en auténticas redes sociales, ha hecho de estos sistemas una necesidad para todo desarrollador que se precio, con el consabido dilema de cuál de los sistemas elegir.

Gracias a las extensiones que podemos añadir, cada día es menos transcendente la elección de un DCVS, pudiendo usar cualquier cliente con cualquier otro servidor.

Comparando git y mercurial

Sin entrar en mucho detalle, podemos comparar estos dos sistemas DCVS populares para hacernos una idea:

git

  • Desarrollado en Depende de perl y está pensado para linux (mal soporte en windows)
  • Velocidad: muy rápido
  • Comandos: algo complejo
  • Interface gráfico: no tiene
  • Popularidad muy alta
  • Repositorio público estrella: github

mercurial (hg)

  • Desarrollado en python, con versiones para linux, windows y mac
  • Velocidad: rápido
  • Comandos: sencillo (similar a subversion)
  • Interface gráfico: tortoiseHG para gnome y windows
  • Popularidad alta
  • Repositorio público estrella: bitbucket

Éstos son algunos apuntes rápidos. Evidentemente, hay algunos interfaces gráficos para git y es posible emplear git en windows, pero en mi opinión tiene algunos problemas que necesitan pulirse. Por otro lado, existen varios IDEs como netbeans o eclipse que pueden usar cualquiera de estos DCVS, abstrayendo su uso interno a través de un interface común.

Para un programador de python, la elección debería ser clara: mercurial. Realizado en python y con numerosas extensiones, también desarrolladas en python, parece sin duda la mejor elección. Además, es el sistema de control de versiones más utilizado en proyectos python, incluyendo el desarrollo del lenguaje en si, por lo que se uso es casi obligado si queremos colaborar con otros desarrolladores python.

Pero no hace falta renunciar a nada: desde mercurial también se puede usar repositorios git o subversion. Basta con añadir la extensión adecuada.

En el resto del articulo, me centreré sólo en la extensión hg-git, con la que se posibilita el uso de repositorios git desde mercurial, sin necesidad de instalar ningún cliente de git adicional (no existen dependecias con ningún ejecutable git).

hg-git

Instalación

La última versión de mercurial a la hora de escribir este artículo es la 1.9. Como la versión “estable” de hg-git tiene problemas con esta versión en concreto de mercurial, voy a explicar aquí lo que sería el método manual de instalación, bastante más seguro.

Suponemos que tenemos ya instalado mercurial por lo medios habituales (autoinstalador en windows/instalador de paquetes en linux). Nos será de gran ayuda tener instalado tortoiseHG como interface gráfico para manejar los repositorios. Para windows, la instalación de tortoiseHG incluye todo lo necesario al empotrar un intérprete de python, mercurial y varias extensiones, algunas de ellas necesarias para transformar rutas y nombres de ficheros codificados en MBCS. Los siguientes pasos a ejecutar con mercurial serán más fáciles de aplicar desde la interface de tortoiseHG.

En el emplazamiento que queramos, empezamos por clonar un repositorio con hg-git desde mercurial:

$ hg clone http://bitbucket.org/durin42/hg-git hg-git

Normalmente, yo suelo usar un mismo directorio para agrupas todos los repositorios clonados. Ése podría ser el lugar adecuado para guardar este repositorio.

Añadimos esta extensión a la configuración de mercurial. Normalmente, se hace en el fichero mercurial.ini (windows) o en ~/.hgrc (linux). Si usamos tortoiseHG, desde las "opciones globales" podemos editar directamente este fichero.

Para añadir la extensión:

[extensions]
hggit = <ruta-al-repositorio>\hg-git\hggit

Como anotación, en alguna documentación se recomienda añadir también la extensión opcional bookmarks a la configuración; pero a partir de la versión 1.8 de mercurial, el comando bookmark es parte propia de los comandos de mercurial, por lo tanto este paso ya no es necesario.

Como dependencia, hace falta instalar el módulo de python dulwich para manejo de repositorios git con python. En windows ya viene incluído en tortoiseHG, por lo que no hay que hacer nada más. En linux, viene como paquete instalable (python-dulwich en ubuntu), pero también se podría haber instalado mediante easy_install sin mayor problema. Lo que sí hay que tener cuidado es en asegurarnos que no tenemos instalado el paquete python-git para que no interfiera con el módulo hg-git que estamos configurando.

Como lista final, estas serían las versiones probadas:

  • mercurial (hg) 1.9
  • hg-git 0.2.4
  • dulwich 0.6.1

Utilización

Con hg-git instalado ya podemos acceder, por ejemplo, a los repositorios de github directamente desde mercurial. Basta con especificar que se trata de un repositorio git:

$ hg clone git://github.com/django/django.git django.git

Para realizar un push a github con conexión codificada con SSH:

$ hg push git+ssh://user@github.com/user/myrep.git

Así mismo, si partimos de un repositorio mercurial también podemos “convertirlo” para su uso en git con el siguiente proceso:

$ cd myrep # (dentro del repositorio mercurial)
$ hg bookmark -r default master # marcamos 'default' como 'master'
$ hg push git+ssh://user@github.com/user/myrep.git
$ hg push

Al marcar con el nombre master a default facilitamos la conversión de los datos de mercurial a objetos git. Este proceso sólo es necesario hacerlo la primera vez.

github o bitbucket

En cuanto a elegir entre github o bitbucket, es más una cuestión de gustos. github se ha posicionado como el sistema predilecto para darse a conocer, sobre todo como referencia en los curriculo a la hora de solicitar empleo. En cambio, bitbucket permite el uso de repositorios privados, muy útil para pequeños grupos de trabajo y para colaboraciones en la “nube” (dispositivos móviles).

Ambos son gratuitos, por lo que no debes dejar de probarlos tan sólo por lo que haya podido decir aquí. Es una nueva forma de conocer y darse a conocer entre programadores, algo que sin duda hace de nuestro pequeño mundo algo mucho más grande.

8 responses so far

iconv en python

ago 04 2011 Published by under Python

Últimamente he necesitado pasar algunos ficheros de una web a codificación utf-8, codificación de caracteres más acorde con lo que se lleva hoy en día. En sistemas linux es una labor que se puede hacer fácilmente con la utilidad iconv:

$ iconv -f cp850 -t utf8 <fichero_entrada.txt >fichero_salida.txt

Pero hay veces que es necesario realizar esta conversión en windows. Si tenemos instalado python, una forma rápida de hacerlo sería:

$ python -c "import sys,codecs;codecs.EncodedFile(sys.stdout,'latin-1','utf-8').writelines(sys.stdin)" <fichero_entrada.txt >fichero_salida.txt

…¡y todo en una sóla línea!1

Tan sólo puntualizar que esta conversión emplea iteradores, por lo que no tiene que ser un problema el tamaño del fichero de texto a convertir.


  1. Para ver más ejemplos de “one-liners” os recomiendo este artículo de Joe di Castro 

One response so far

Evento Python Madrid 2011

jul 06 2011 Published by under Python

El grupo Python-Madrid y el Grupo de Usuarios de Linux de la Universidad Carlos III de Madrid, celebrarán el próximo día 15 de Julio de 10:00 a 18:00 el evento Python Madrid 2011. El evento tendrá lugar en la Universidad Carlos III de Madrid, en su campus de Leganés. Se trataran temas como Python, Django, PyGame y mucho más. Toda la información en www.espython.org

No responses yet

Método __getattribute__

jun 26 2011 Published by under Python

Si has seguido hasta ahora la serie de artículos sobre descriptores, habrás visto que buena parte de la magia de los objetos en python se debe al método __getattribute__ que todo objeto adquiere de su antecesor común, la clase object.

En el último artículo, donde hablaba de las optimizaciones de los métodos especiales, también comentaba algunas optimizaciones que tenían qué ver con el método __getattribute__ y proponía un ejercicio:

¿Sabrías qué es lo que pasa en el siguiente caso? ¿Se invoca el método getattribute en algún momento? ¿Sería una llamada implícita o explícita?

   obj.__getattribute__("__getattribute__")

Quien se enfrenta a este código por primera vez, lo primero que piensa es que se va a producir una autorecursividad puesto que en el acceso al método __getattribute__ se debería invocar el propio método __getattribute__ y así indefinidamente.

Si embargo, cuando se prueba se ve que funciona tal y como se espera. Entonces, ¿cómo se evita la recursividad?

En el artículo de optimizaciones de los métodos especiales hablábamos de dos optimizaciones (atajos) de las llamadas implícitas a métodos especiales:

  1. Implícitamente, sólo se buscará métodos especiales en la clase, nunca en el diccionario del objeto.

  2. Implícitamente, nunca se accederá a un método especial a través de __getattribute__

La intuición nos dice que aquí está la respuesta de que no tengamos autorecursividad.

Antes de analizar lo que está pasando, señalar que en el acceso a atributos se usa el operador ‘.‘ (punto) que, como cualquier otro operador, está sujeto a las mismas optimizaciones que hemos apuntado. Para su labor, el operador . empleará el método especial __getattribute__.

La invocación obj.__getattribute__("atributo") se produce en dos pasos:

  1. Implícitamente, el operador ‘.‘ accede directamente al método __getattribute__, aplicando las optimizaciones.
  2. Se invoca explícitamente a __getattribute__ para que retorne el valor del "atributo"

Así pues, el resultado final consiste en la combinación de una llamada implícita y otra explícita.

Como corolario, se puede afirmar que “Nunca se invocará a __getattribute__ para acceder a __getattribute__“. No será la primera vez que alguien lo haya intentado.

One response so far

Optimizaciones con los Métodos Especiales

jun 24 2011 Published by under Python

Métodos Especiales

Dentro del llamado “modelo de datos” de python, la sobrecarga de operadores, tan característica de la programación orientada a objetos, se realiza mediante la definición de algunos métodos de nombre especial. A través de esta técnica se define cómo se comportará una clase frente a los operadores del lenguaje.

Estos métodos especiales son invocados implícitamente por el intérprete para realizar la operación, decidiendo en tiempo de ejecución cuáles de los métodos serán más adecuados para realizar la operación.

Por ejemplo, el método especial __nonzero__ determina el valor True o False de la instancia, útil para expresiones condicionales. De no estar definido este método, se considera el método __len__ (usado por la función len()) para determinar como False si tiene tamaño cero. En caso de que tampoco cuente con este método, se considera siempre como True.

Al ser parte intrínseca del lengueje, estos métodos especiales inciden seriamente en redimiento del intérprete. Con sólo definir el método especial __getattribute__, por ejemplo, encargado de controlar todo acceso a los atributos de un objeto, la sobrecarga del intérprete se volvería pesada y lenta sin posibilidad de mejorar mucho mediante rutinas en lenguaje C. Para evitar este impacto negativo, se toman algunas “optimizaciones” que sólo son aplicables a los métodos especiales.

Optimizaciones de Métodos Especiales

Con los “Métodos Especiales” se dan dos optimizaciones en la invocación “implícita” de un método especial:

  1. Implícitamente, sólo se buscará métodos especiales en la clase, nunca en el diccionario del objeto.

  2. Implícitamente, nunca se accederá a un método especial a través de __getattribute__

Estas dos optimizaciones (más bien “atajos“) son origen de muchos errores y malas interpretaciones, responsable en primera instancia de que determinado código no funcione como se esperaba en teoría.

Recalcar que estas optimizaciones sólo ocurren en las invocaciones “implícitas”. Si hacemos la invocación explícita, a través del nombre especial del método, entonces se sigue el procedimiento estándar de búsqueda de atributos.

Veamos algunos ejemplos y contraejemplos:

>>> class C(object):
...     def __len__(self):
...         return 5
...
>>> obj=C()
>>> len(obj)
5
>>>
>>> obj.__len__=lambda:100
>>> len(obj)
5
>>> obj.__len__()
100

La clase responde a la función estándar len() a través del método __len__. Como se ve, aunque cambiemos el método en el diccionario del objeto, la función len() sigue usando el método especial de la clase. Si se invoca el método explícitamente (obj.__len__()), entonces sí que se usará el método del diccionario del objeto.

Primera conclusión:

Para que funcione correctamente, toda técnica dinámica que involucre métodos especiales ha de actuar sobre la clase.

Estudiemos otro ejemplo:

class C(object):

    a=100

    def __getattribute__(self, attr):
        value=super(C,self).__getattribute__(attr)
        print "Desde C # '%r'.'%s'==%r"%(self,attr,value)
        return value
>>> obj=C()
>>> obj.a
Desde C # '<__main__.C object at 0xb77d4b2c>'.'a'==100
100
>>> C.a
100

En la prueba, accedemos al atributo a a través de la instancia obj y a través de la clase C. En el primer caso, se llama a __getattribute__ para acceder al atributo; mientras que en el segundo no lo hace. Muy a menudo se piensa errónemente que la clase usa implícitamente sus propios métodos especiales y no es así.

Como objeto que es, una clase también es una instancia. A la clase de una clase la denominaremos “metaclase”1 y tendrá como ancestro superior la clase type (del mismo modo que toda clase tenía como ancestro la clase object)2. Es a esta metaclase donde se buscan los métodos especiales de la propia clase:

class Meta(type):
    def __getattribute__(cls, attr):
        value=super(Meta,cls).__getattribute__(attr)
        print "Desde Meta# '%r'.'%s'==%r"%(cls,attr,value)
        return value

class C(object):

    __metaclass__=Meta

    a=100

    def __getattribute__(self, attr):
        value=super(C,self).__getattribute__(attr)
        print "Desde C # '%r'.'%s'==%r"%(self,attr,value)
        return value

De donde podemos sacar la segunda conclusión:

Los métodos especiales que operen con clases deberán ir en la metaclase.

Haciendo un fundido de los ejemplos anteriores:

class Meta(type):
    def __getattribute__(cls, attr):
        value=super(Meta,cls).__getattribute__(attr)
        print "Desde Meta# '%r'.'%s'==%r"%(cls,attr,value)
        return value
   
    def __len__(cls):
        return 999

class C(object):

    __metaclass__=Meta

    def __getattribute__(self, attr):
        value=super(C,self).__getattribute__(attr)
        print "Desde C # '%r'.'%s'==%r"%(self,attr,value)
        return value

    def __len__(self):
        return 100

Con este código, se puede comprobar las siguientes formas de invocar __len__ para la instancia:

>>> obj=C()
>>> len(obj)
100
>>> len(C)
999

Aquí observamos que la llamada implícita a __len__ se salta el __getattribute__ tanto de la clase como de la metaclase como ya estaba anunciado.

Analicemos algunas llamadas explícitas (recomiendo ir probándolas):

  • vía la instancia obj.__len__(): se usará el __getattribute__ de la clase para buscar el método __len__
  • vía el tipo type(obj).__len__(obj): se usará el __getattribute__ de la metaclase para buscar el método __len__. Por orden de prioridad, se usará el __len__ de la clase.
  • vía la metaclase type(C).__len__(C) se usará el __getattribute__ de type, invocando finalmente el __len__ de la metaclase

En el orden de búsqueda, tiene prioridad el método __len__ definido en la clase frente al definido en la metaclase. Por ese motivo no puede emplearse la llamada explícita C.__len__() ya que no corresponde con un método de clase.

Ejercicio de comprensión

¿Sabrías qué es lo que pasa en el siguiente caso? ¿Se invoca el método __getattribute__ en algún momento? ¿Sería una llamada implícita o explícita?

obj.__getattribute__("__getattribute__")

Conclusión

Entendiendo cómo funcionan estas optimizaciones vistas con los métodos especiales, y con bastante cuidado, será posible hacer que nuestras clases se comporten según lo esperado en las operaciones normales. Una buena planificación de nuestro modelo de datos según lo que espera el intérprete conseguirá que nuestro código sea más legible y fácil de mantener.


  1. En próximos artículos veremos el funcionamiento de las metaclases 

  2. Siguiendo con la relaciones entre clases y objetos, la clase type es a la vez instancia de type y subclase de object

One response so far

Descriptores – Parte 2

jun 21 2011 Published by under Python

¿Cómo funciona un descriptor?

Todos los objetos y todas las clases que derivan de object1 adquieren de él un método llamado __getattribute__. Siempre a través de este método se accede a los atributos, y es en este método donde se hace toda la magia de los descriptores, de modo que un acceso al atributo obj.x se transformará en una llamada a type(obj).__dict__['x'].__get__(obj, type(obj)) si el atributo se trate de un descriptor. Una expresión casi ininteligible que va a requerir alguna que otra explicación. Lo importante es saber que al sobrecargar el método __getattribute__ deberemos cuidarnos de invocar al método de la clase padre si queremos que los descriptores sigan funcionando con normalidad.

Atributos de un objeto

De todos los atributos que tiene un objeto python, algunos son “Atributos especiales” que aporta python para su funcionamiento interno como son __class__ o __bases__. Estos atributos son bastante antipáticos de manejar ya que, o bien no son reportados por la función dir(), o bien tienen restricciones para ser modificados.

Por otro lado, están los atributos definidos dinámicamente por el programa que forman lo que se conoce como “diccionario del objeto”. Estos atributos se guandan en el (también) atributo __dict__.

Los “atributos de tipo” son los atributos asociados a un objeto por pertenencia a una clase. Estos atributos pueden estar enmascarados por los atributos del diccionario del objeto, algo muy útil cuando se aplican “técnicas dinámicas” de parcheo.

Hay que tener en cuenta que algunos de los tipos estándar como list,tuple,dict,… no tienen atributo __dict__ con lo que no tienen diccionario donde añadir o suplantar atributos dinámicamente. La única opción pasa por derivar clases a partir de ellos para añadir allí los atributos deseados.

Búsqueda de atributos

Al buscar un atributo obj.attr, se sigue un orden determinado de prioridad según el tipo de atributo que se esté buscando:

  1. Atributos especiales: son los que tienen mayor prioridad.

  2. Descriptores de datos: se buscan en el diccionario de la clase (obj.__class__.__dict__) y en todos los diccionarios de las clases padre. Si se encuentra, se retorna el resultado del descriptor (la expresión tan chula que puse al principio del artículo). Si no es un descriptor de datos, entonces se ignora y se sigue buscando.

  3. Atributos del diccionario del objeto: se busca el atributo en el diccionario del objeto (obj.__dict__). Si obj fuera una clase (==isinstance(obj,type)), entonces también se buscaría en los diccionarios de las clases padre (obj.__bases__) y, de ser un descriptor de datos, se devolverá el resultado del descriptor en su lugar.

  4. Descriptores de no-datos: se repite el paso 2, pero esta vez se buscan descriptores de no-datos.

  5. Método __getattr__: por último, si no ha habido éxito en la búsqueda del atributo, se intenta invocar el método __getattr__, de existir, para delegar en él.

  6. Si todo ha fallado, se termina la búsqueda retornando un error AttributeError.

En resumidas cuentas, se priorizan los descriptores de datos a las variables de instancia, las variables de instancia a los descriptores de no-datos y, con la más baja prioridad, se invocaría el método __getattr__.

Remarcar la diferencia que hay entre un descriptor de datos y uno de no-datos en el orden de búsqueda. Por el simple hecho de añadir un método __get__, un descriptor se pondría por delante de los atributos del diccionario del objeto en el orden de búsqueda. También apuntar que sólo se buscan descriptores entre los atributos de clase, por lo que no tendrá sentido asignar descriptores en otro atributos.

En el caso de la asignación de atributos, se seguirían estos pasos:

  1. Se busca descriptores de datos en el diccionario de la clase (obj.__class__.__dict__) y todos los diccionarios de las clases padre. Si se encuentra un descriptor de datos, entonces se invoca el método __set__ del descriptor.

  2. Se invoca el método __setattr__, si existe, para delegar en él.

  3. Como última prioridad, se inserta el atributo en el diccionario del objeto.

En estos pasos no aparecen los descriptores de no-datos. Si realizamos una asignación sobre un descriptor de no-datos, acabaría siendo reemplazado como cualquier atributo normal.

¿Se puede saltar un descriptor de datos?

La prioridad de los descriptores de datos frente al resto de atributos hace prácticamente imposible saltárselos para acceder directamente a un atributo. Todo acceso al atributo pasa por sus manos, regla que se aplica también con el propio descriptor y que da origen a bastantes recursividades sin fin. Por ello es habitual que el descriptor mantenga un atributo auxiliar “privado”, ya que de otro modo no tendrá otra forma de acceso directo.

Algo que sí podemos hacer es cambiar las prioridades con la definición de un método __getattribute__ propio. Como ejemplo, se podría priorizar los atributos del diccionario frente a los descriptores de esta manera:

class Descrip(object):
    def __init__(self, mul):
        self.mul=mul
    def __get__(self, obj, cls=None):
        return obj.value*self.mul
    def __set__(self, obj, value):
        raise AttributeError
    def __delete__(self, obj):
        del self

class C(object):
    a12=Descrip(12)
    a200=Descrip(200)
   
    def __init__(self,value):
        self.value=value
       
    def __getattribute__(self, attr):
        dic=super(C,self).__getattribute__("__dict__")
        if attr in dic:
            return dic[attr]
        else:
            return super(C,self).__getattribute__(attr)

c=C(2)

print c.a12  #--> 24  (valor del descriptor)
c.__dict__["a12"]=100
print c.a12  #--> 100 (valor del diccionario)

  1. En python 2.x, a las clases que derivan de object se las denomina “nuevas clases” por contraste con las clases que había hasta ese momento. En python 3.x, todas las clases derivarán por defecto de object

2 responses so far

Older posts »