Mostrando las entradas con la etiqueta python. Mostrar todas las entradas
Mostrando las entradas con la etiqueta python. Mostrar todas las entradas

domingo, mayo 04, 2014

Substring matching in Python (run between naive, Boyer-Moore, and Suffix Array)

A  few days ago I found this very interesting problem: given a list of strings L, write a function that returns the elements of L which contains some substring S.

For example, given L=["Casa", "Perro", "Gato", "Onomatopeya", "internacionalizacion", "Om nom nom"] and S="nom", we want the result of find(L, S) = ['Onomatopeya', 'Om nom nom'].

Naïve Version

On Python this sounds simple enough, and we can write:
def find1(L, S):
    return [x for x in (L) if S in x]
However, for a big enough L and S we can see the runtime of this function depends not only on the size of L but also on the size of S. That's it, the runtime complexity of find1 in BigOh notation is: O(n.m.s), where:
  • n=len(L)
  • m=max([len(x) for x in L])
  • s=len(S)
The plot of time for find1 for a fixed L and where we increase the size of S looks like this:


Boyer-Moore-Horspool

It turns out that since version 2.5, Python's "in" operator is implemented internally using a modified version of Boyer-Moore algorithm for substring searching. The details are here.

We can take advantage of this detail by creating a temporary structure for faster lookups. We pre-process L so we can make fast queries.

The idea is to construct a big string W with the concatenation of all the elements of L, using a special char as separator, a char that is not present on S nor any element of L. For example:
L = ["Casa", "Perro", "Gato", ...]
W = "Casa\nPerro\nGato\n..."
Then, finding if a substring S is present in any of the elements of L can be answered by just writing: 
S in W
This allows us to answer whether a substring is present or not. To actually construct the resulting list of elements of L which contain S we need another helper structure. We build T, a list of integers that, for every elements in L, equals the starting index of this element in W. Continuing with the example:
T = [0, 5, 11, 16, ... ]
This means the first element, "Casa", starts at index 0 in W; the second element "Perro" starts at index 5 in W, etc. And this structure allows us to quickly determine the index in W for every element in, and we lookup the index by doing a binary search on T.

The runtime complexity for constructing this intermediate index is O(n), with O(n) memory usage.

Our new find function should then:
  • find the first position of S in W as p
  • determine for which element of L this index relates to, by doing a binary search on T
  • from p+1 onwards, find again the next S in W.
Since an element of L can contain many times the same substring S we may jump to the next word on W.

On code, the find2 function looks like:
def find2(L, S):
    # Using the native Boyer-Moore implementation of the "in" operator
    R = []
    i = W.find(S)
    while i != -1:
        p = bisect.bisect_right(T, i) - 1
        e = L[p]
        #assert S in e
        R.append(e)
        i = W.find(S, T[p] + len(e))
    return R
The runtime complexity of this new find function is: O(n.m). We still need to take into account the length of each element of L since BMH algorithm is (mostly) linear on the W string. 

The plot of runtime for find1 vs. find2 looks like the following graphic. Again, we are leaving a fixed L and increasing the size of S:



Suffix Array

There is a third way to solve this problem, by means of constructing a suffix array

This amazing data structure offers a runtime complexity of O(log N) for suffix lookups, where N is the length of the string. Incidentally, it also allows to lookup for substrings, since we just lookup until a suffix on the SA has S as prefix.

Again, we need to construct an intermediate index, which is again very simple: sort all the possible suffixes on W. The trick is how to do it: we shall not keep every possible suffix as an string, but just a list of starting positions for every suffix, and sort this list by the actual string of the suffix.

In code, the construction of the SA table is really simple:
# Suffix Array Table
SL = list(range(len(W)))
SL.sort(key=lambda x: W[x:x+100])
The runtime complexity for constructing this intermediate index is O(n.log n), with O(n) memory usage.

To find a specific suffix we should binary search the SA table, using the element on SL to determine where in W the suffix starts.

Since a substring may appear many times on many elements of S, we may have many sufixes starting with S. The good news is, since the list of suffixes is sorted, all this suffixes will be one after another on the SL table. But since we are doing a binary search on the list of suffixes, we can't be sure on where the middle pointer will jump in this contiguous sequence of suffixes, all starting with S. 

Therefore, when we find the position of some suffix we should go back a little to make sure we are starting on the first suffix on the sequence of suffixes that start with S.

On code, our new find3 function looks like:
def find3(L, S):
    # Suffix array
    start = 0
    end = len(SL)
    while start < end:
        mid = start + (end - start) // 2
        pa = SL_key_fn(W, SL[mid], 100)
        pb = SL_key_fn(S, 0, len(S))
        if pa < pb:
            start = mid + 1
        elif pb < pa:
            end = mid
        else:
            # A word may contain the same S multiple times
            R = set()
            while mid > 0 and W.startswith(S, SL[mid]):
                mid = mid - 1
            if not W.startswith(S, SL[mid]):
                mid = mid + 1
            while mid < len(SL) and W.startswith(S, SL[mid]):
                p = bisect.bisect_right(T, SL[mid]) - 1
                e = L[p]
                assert S in e
                R.add(p)
                mid = mid + 1
            return [(L[i]) for i in R]
    return []
The SL_key_fn function was a failed experiment to enhance the performance of the lookups. This function today is:
def SL_key_fn(data, x, llen):
    return data[x:x+llen]
Which is the same as the key on the SA table sorter.

The runtime performance of the find3 function is: O(log (n.m)), and the plot of the three functions looks like this:



Drawbacks

This SA implementation in Python is using a lot of temporary memory for sorting the table. My implementation on my laptop is using 2.4GB of RAM to sort an L of 150k elements. There's been some discussion about this memory issue on this blog post and in this Stack Overflow question.

Special thanks

Python Argentina community is a great place to look for help for all your spanish Python programming needs. 

jueves, junio 07, 2012

DjangoDay La Plata 2012 en la UTN


El DjangoDay Argentina 2012 es una reunión de programadores, diseñadores e interesados en el framework web Django, a realizarse en La Plata (especificamente en la UTN FRLP).

Consiste en un sólo día en el que van a haber charlas variadas y un taller para tirar código y aprender un poco mas de Django. Es una buena oportunidad para conocer el framework, conocer gente piola y pasar un buen rato.

Cabe aclarar que el evento es GRATUITO!!!! Les dejo el afiche promocional para que lean más y pueden visitar la pagina del evento! http://djangoday.com.ar Dicho evento necesita una previa registración por razones administrativas!!!

Para la registración pueden ir a http://djangoday.com.ar o bien directamente usando Eventioz en https://eventioz.com/events/djangoday

miércoles, mayo 09, 2012

Bindings de SDL 1.2 para Python 2 y 3


La invasión de los ajíes mutantes del espacio.


Usando SWIG hice unos bindings para usar SDL 1.2 desde Python 2 y 3.

Por ahora es altamente experimental, no todas las funciones están correctamente bindeadas (por ej. las que tienen parametros de punteros de arrays o de tipo salida), y es muy fácil tirar un segfault de la VM de Python desde código Python.

De todas formas ya está funcionando, y en el directorio "tests" puse dos programitas de prueba. El código está en un repositorio Github:

https://github.com/alejolp/sdl1-python

Cualquier comentario es altamente bienvenido.

sábado, octubre 02, 2010

Ejecutando código Python de forma segura con eval()

Respuesta corta


Eval es maligno, no lo intentes.

Respuesta larga


Hace unos días publicaron en la lista de Python Argentina la pregunta de qué tan peligroso era usar la función eval para ejecutar código:

code = raw_input('Ingrese una entrada: ')
eval(code)

Sucede que si el usuario ingresa una expresion similar a:

Ingrese una entrada: __import__('os').system('ls /')

Pueden ocurrir cosas malas; en este ejemplo se esta listando los archivos del directorio raíz, pero ingresando: "rm -rf ~" se van a borrar todos los archivos de la carpeta personal del usuario.

Entonces, eval() parece bastante malo.

Una primera mejora a la fución eval() es asignarle dos parámetros adicionales para anular las funciones de importacion de módulos. Haciendo:

eval(code, {'__import__': None}, {})

El anterior ejemplo ya no funciona:

>>> eval('__import__("os")', {'__import__': None}, {})
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<string>", line 1, in <module>
TypeError: 'NoneType' object is not callable

Pero hay otra forma de pasar por arriba la limitacion de __import__:

>>> eval('__builtins__["__import__"]("os")', {'__import__': None}, {})
<module 'os' from '/usr/lib/python2.6/os.pyc'>

Ya tenemos de vuelta la referencia a __import__. Entonces como bien comentaron en la lista, una solución sería anular la referencia a __builtins__:

>>> eval('__builtins__["__import__"]("os")', {'__import__': None, '__builtins__': None}, {})
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<string>", line 1, in <module>
TypeError: 'NoneType' object is unsubscriptable

Y si queremos abrir un arhivo tampoco se puede:

>>> eval('open("/tmp/aa", "w")', {'__import__': None, '__builtins__': None}, {})
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<string>", line 1, in <module>
NameError: name 'open' is not defined

Con la función 'file' tampoco funciona. El módulo __builtins__ es el módulo donde residen todas las funciones predefinidas de Python: file, open, list, dir, etc. Si queremos construir una lista tampoco se puede:

>>> eval('list()', {'__import__': None, '__builtins__': None}, {})
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<string>", line 1, in <module>
NameError: name 'list' is not defined

Pero hay otras formas de pasar por arriba las limitaciones de no tener __builtins__. Solo se necesita tener una referencia a un objeto que referencie a una función que nos permita hacer cosas no permitidas.

Una forma es usar las operaciones de 'introspection' de Python. Dada una instancia de un objeto podemos saber su clase, la clase padre y las subclases. Por ejemplo:

>>> (1).__class__.__bases__[0].__subclasses__()
[<type 'type'>, <type 'weakref'>, <type 'weakcallableproxy'>, <type 'weakproxy'>, <type 'int'>, <type 'basestring'>, <type 'bytearray'>, ...]

En este caso estamos tomando la instancia del objeto int(1), preguntando su clase, luego la clase padre (object), y luego las subclases de ésta (¡un montón!).

Mirando atentamente la lista encontramos dos referencias interesantes:

  • <type 'file'>
  • <type 'zipimport.zipimporter'>

La primera es el tipo file(), la misma que se usa para leer y escribir archivos. La segunda es la clase zipimporter, que permite importar módulos dentro de un zip.

Lo interesante es que aún anulando la referencia al módulo __builtins__, las anteriores operaciones siguen funcionando:

>>> eval("""[x for x in (1).__class__.__bases__[0].__subclasses__() if x.__name__=='file'][0]("/proc/version")""", {'__import__': None, '__builtins__': None, }, {})
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
File "<string>", line 1, in <module>
IOError: file() constructor not accessible in restricted mode

Intentando usar la referencia a 'file' tenemos un problema: el IOError.

El mensaje de error IOError aparece porque dentro de la función file() hay un bloque de código que verifica que __builtins__ no sea None.

Años atrás, el equipo de Python intentó agregarle al lenguaje la funcionalidad de ejecución restringida de código, que permita ejecutar código no seguro de forma segura.

Luego de varios intentos desistieron, justamente porque no era posible contemplar todos los casos. Hoy en día esas funciones están deprecated y en Python 3 fueron completamente removidas del código fuente.

Entonces, en Python 3 es posible importar cualquier módulo y ejectar codigo malicioso fácilmente, aún anulando __builtins__:

>>> (eval('[x for x in (1).__class__.__bases__[0].__subclasses__() if x.__name__ == "ImpImporter"][0]().find_module("os").load_module("os").system("id")', {'__import__': None, '__builtins__': None}, {}))
uid=1000(alejo) gid=1000(alejo) groups=1000(alejo),20(dialout),24(cdrom),25(floppy),27(sudo),29(audio),44(video),46(plugdev)

En este caso, la clase ImpImporter es una subclase de object que abstrae todo el comportamiento de importación de módulos. Acceder a esa clase es algo que no se puede evitar usando las opciones de eval().

Quedó pendiente la instancia de zipimporter.

Importar modulos con esa clase no imposible, la única complicación es conocer la ruta de algún egg o zip dentro del sistema que nos ofrezca alguna referencia a __builtins__, __import__, al módulo 'os' o a otro módulo que nos interese.

Por ejemplo, hacer un zip que tenga dentro un modulo que importe el módulo 'os' es bastante trivial:

>>> eval("""[x for x in (1).__class__.__bases__[0].__subclasses__() if x.__name__=='zipimporter'][0]("/tmp/modulo1.zip").find_module("modulo1").load_module("modulo1").os.system("id")""", {'__import__': None, '__builtins__': None, }, {})
uid=1000(alejo) gid=1000(alejo) groups=1000(alejo),20(dialout),24(cdrom),25(floppy),27(sudo),29(audio),44(video),46(plugdev)

En resumen, no se confíen de eval() para ejecutar código potencialmente inseguro. En la wiki de Python.org hay una sección dedicada exclusivamente a la seguridad, aquellos que estén interesados no dejen de leerla.

domingo, septiembre 12, 2010

The Knights who say Ni

''.join(reduce(lambda a,b:[a[0]/13,a[1]+[" aeghiknostwy"[a[0]%13]]],' '*22,[0x1220cc0bb8b8a537ca145,[]])[1])

update: reducida la cantidad de caracteres y corregido el error del espacio

martes, junio 29, 2010

Mac Pan


Mac Pan es una versión alternativa del Pac Man hecha en Python con Pygame. Los gráficos son de Emiliano Luciani y se puede bajar desde:

http://code.google.com/p/macpan/

Espero les guste :)

viernes, abril 09, 2010

python: 70's

Para cerrarlo apretar Control-C en la terminal.

import pygame,math,itertools,random
for x in itertools.chain(itertools.islice(itertools.imap(lambda x: x(), [lambda: pygame.init(), lambda: pygame.display.set_mode((320,240))]),2,2), itertools.count()): (pygame.draw.circle(pygame.display.get_surface(),(random.randint(0,255),random.randint(0,255),random.randint(0,255)),(160,120),x%200+2,1), [pygame.display.get_surface().set_at((random.randint(0,319),random.randint(0,239)), (random.randint(0,255),random.randint(0,255),random.randint(0,255))) for y in xrange(1000)], pygame.display.flip()) and None

jueves, abril 08, 2010

python: wave or wave

import itertools, math, time
for x in itertools.count(0): print "".join([time.sleep(0.003)][0:0] + [y in [int(math.cos(x*3.14/(50 + 25*math.cos((x/50.0)*3.14/50)))*37 + 37), int(math.sin(x*3.14/(50 + 25*math.sin((x/50.0)*3.14/50)))*37 + 37)] and '#' or ' ' for y in xrange(75)])

miércoles, abril 07, 2010

python: wave

import itertools, math, time
for x in itertools.count(0): print "".join([time.sleep(0.005)][0:0] + [int(math.sin(x*3.14/(50))*37 + 37) == y and '#' or ' ' for y in xrange(75)])

martes, abril 06, 2010

python: 60's

import itertools,math,time
for z,w in itertools.imap(lambda x: (x % 12, (x / 12) % 5), itertools.count(0)): print ''.join(map(lambda x: x, ["\n".join([''.join([(math.hypot(x-10,y-10) < z and (math.hypot(x-10,y-10) > z/2 and '#$%&!'[w] or ' @'[(z/2)%2]) or ('.,:;+'[z%5])) for x in xrange(21)]) for y in xrange(21)]), time.sleep(0.05) or '']))

lunes, abril 05, 2010

python: ping pong

[actualizado] en una sola linea:

while 1: print str('d' not in globals() and globals().__setitem__('d', [__import__('random').randint(1,8),6,1,1]))[0:0] + "\n".join(['-'*12] + ['|' + ''.join([((x,y) == (d[0], d[1])) and '#' or ' ' for x in xrange(10)]) + '|' for y in xrange(10)] + ['-'*12]) + str((lambda: (d.__setitem__(0, d[0]+d[2]), d.__setitem__(1, d[1]+d[3]), d[0] not in range(1,9) and d.__setitem__(2, d[2] * -1), d[1] not in range(1,9) and d.__setitem__(3, d[3] * -1)))())[0:0] + str(__import__('time').sleep(0.2))[0:0]

domingo, abril 04, 2010

python: otra silueta

print "\n".join(["|" + "".join([int(10*(((x-10)**2)/float(10**2) + ((y-10)**2)/float(10**2))) in [10,11] and '#' or (((y == 10) and (x in range(3,18))) and '66656c696365732070617363756173'.decode('hex')[x-3] or ' ') for x in xrange(21)]) + "|" for y in xrange(21)])

python: silueta

import math;print "\n".join([''.join([int(math.cos(x * 3.14 / 19) * 5 + 2) == (y -2) and (abs(math.sin(x * 3.14 / 19)) < 0.05 and '*' or '#') or ' ' for x in xrange(78)]) for y in xrange(11)])

miércoles, diciembre 23, 2009

Cualo: Juego en Pygame




Recorriendo las cosas de mi disco encontré un juego que hice para una presentación de PyGame hace unos meses.

Lo acabo de publicar por si alguien le interesa mirarlo:

http://bitbucket.org/alejolp/cualo-pygame/get/tip.zip

jueves, diciembre 17, 2009

Este blog es miembro de Planeta PyAr



Planeta Python Argentina:
Gracias Joaquin por el logo!

    martes, diciembre 08, 2009

    PHP No sabe sumar




    Leyendo feeds encontré un viejo post de Facundo Batista donde dice que PHP No sabe sumar:
    $r = 3 + 058;
    echo $r."\n";
    ?>
    Ese código en PHP imprime "8".

    WTF?! Esta es una de esas cosas que me hacen sentir FURIA, IRA, MUERTE, PESTE, DESTRUCCIÓN MASIVA. ¿Cómo es que alguien en su sano juicio va a hacer semejante idiotez?

    El error está en que el número 058 intenta interpretarse como un octal, pero no es válido (el 8 no es un dígito octal!) y el número se trunca hasta el ultimo dígito válido. El "058" se interpreta como "05".

    Buscando en bugs.php.net encontré un reporte de bug del año 2004 en donde se menciona este bug, y la única respuesta fue:
    I once looked into fixed this, but this would slow down PHP dramatically in all cases where numbers are used inside scripts. Hence it's not worthwhile to "fix" this.

    Veamos de vuelta... en una versión relativamente moderna de PHP (5.2.6) qué pasa...
    $ php5 -r "echo 3 + 058 . \"\\n\";"
    8
    $ php5 -v
    PHP 5.2.6-1+lenny3 with Suhosin-Patch 0.9.6.2 (cli) (built: Apr 26 2009 22:16:23)
    Copyright (c) 1997-2008 The PHP Group
    Zend Engine v2.2.0, Copyright (c) 1998-2008 Zend Technologies
        with XCache v1.2.2, Copyright (c) 2005-2007, by mOo
        with Suhosin v0.9.27, Copyright (c) 2007, by SektionEins GmbH

    Ya está. Si alguien quiere seguir programando en ese lenguaje de mala muerte, yo avisé!

    Hay una página llamada PHPWTF.org en donde se juntan otras de estas joyitas ...

    Ahora, ¿qué pasa en Python?

    >>> 3 + 058
      File "", line 1
        3 + 058
              ^
    SyntaxError: invalid token

    ¡OH SI! y tal como dice Facundo en su post:
    "Ningún error debería pasar silenciosamente", dice el Zen de Python.

    Por favor, no usen PHP. Usen Python. Ruby. Java. Lo que sea, pero por favor, basta de usar PHP.