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

Charla Bienvenido a Python en Instituto Libre 09

flyingEl viernes por la tarde se llevó a cabo el evento Instituto Libre 09 en la ciudad de Coronda, más precisamente en el Instituto Superior de Profesorado Nº 6 Dr. Leopoldo Chizzini Melo.

Presenté una charla llamada Bienvenido a Python. Es una introducción al lenguaje de programación Python bastante práctica y con muchos ejemplos. Los slides están disponibles en formato pdf bajo una licencia CC. El documento fue generado a partir de un archivo de texto utilizando rst2pdf.


Ejercicio 16 - proyecto Euler

De vez en cuando se me da por hacer saries de posts en el blog. Sagas. Una de las últimas fue la saga sobre resoluciones a problemas del proyecto Euler. Lo que intentaba era resolverlos con alguna característica interesante de Python y luego explicarla. Luego de los primeros, los ejercisios se volvieron más matemáticos y encontraba menos cosas interesantes de Python que comentar, así que dejé de postear mis soluciones. Acabo de encontrar esta entre mis archivos y creo que tiene algo de originalidad como para merecer ser publicada:

¿Cuál es la suma de los dígitos del número 2^(1000)?

>>> 2**1000

107150860718626732094842504906000[algunos números fueron intencionalmente borrados]

>>> a = 2**1000

>>> sum((int(x) for x in list(str(a))))

1366

¿A alguien se le ocurre otra forma?


Charla: Taint Mode en Python

Estas son los slides (más texto con el grueso de lo que dije) de mi charla en PyCon Argentina 2009. La misma se anunciaba como:

Taint Mode es una característica implementada en algunos lenguajes con el objetivo de prevenir que usuarios malintencionados alteren entradas al sistema para lograr la ejecución de comandos no permitidos u otros tipos de ataques.

En esta charla se introducen los conceptos básicos de Taint Mode y se discute la forma de implementarlo en Python mediante el uso de decoradores. Se muestra una implementación concreta y se analizan sus resultados.

img0

img1

Cómo digo ahí, no soy un experto en seguridad aunque es un área que me interesa, sino me considero un desarrollador.

Cómo desarrollador, muchas veces me encuentro dejando de lado la seguridad por cuestiones más apremiantes.

Por hacer lo urgente, no hacemos lo importante.

Entonces las fechas límite para entregas, las presiones por incorporar nuevas características, el pago solo por lo que se ve, hace que mucho software salga a la calle sin los más mínimos recaudos de seguridad.

Esto trae consecuencias graves para muchas organizaciones y personas, como:

  • Robo o alteración de información
  • Bloqueos en la disponibilidad de un recurso
  • Suplantación de identidad

Formas de mitigar este problema sin duda hay muchas. Una puede ser contar con herramientas que en tiempo de desarrollo permitan verificar la existencia de vulnerabilidades en el código escrito. ¿Qué vulnerabilidades? ¿Todas? Eso sería seguramente imposible. Pero existen herramientas que se especializan en distintos nichos: buffer overflow en C, XSS en PHP.

Empecé a prestarle atención a este tipo de cosas cuestiones cuando empecé a participar de un grupo de investigación sobre seguridad en mi facultad. Yo me estaba postulando para obtener una beca de maestría y una de las condiciones era participar de un grupo de investigación.

Así que empecé a buscar en Internet papers que relacionen cuestiones de  seguridad con Python, como una forma de encontrar algún tema que me interese y pueda profundizar.

Encontré algunos papers de 2 investigadores de la Universidad Estatal de Moscú, Andrew Petukhov y Dimitry Kozlov en los que discutían cómo implementar Taint Mode en Python. Si bien su enfoque consistía en modificar el intérprete en C de Python para lograrlo, de su trabajo incorporé la mayoría de los conceptos que voy a mencionar en la primer parte de la charla. A diferencia de ellos, mi implementación de Taint Mode para Python está escrita totalmente en Python.

img2

img3

El modelo de las manchas intenta describir cómo se comportan los valores que ingresan a un sistema desde el exterior y tiene 3 componentes fundamentales.

FUENTES NO CONFIABLES, FUNCIONES LIMPIADORAS Y SUMIDEROS SENSIBLES.

Todo valor que ingresa a un programa desde el exterior, es considerado por defecto como un valor manchado, ya que no sabemos de su procedencia. Decimos que viene de una fuente no confiable. Los valores recibidos desde el exterior pueden haber sido deliberadamente modificados para causar daño o simplemente por error causar un un problema dentro del sistema.Estas manchas se propagan por el programa mediante operaciones como la asignación o la concatenación.

Las funciones limpiadoras representan los mecanismos que se pueden utilizar dentro de un programa para convertir un valor manchado en un limpio: escapando, codificando o incluso eliminando caracteres sospechosos.

Los valores manchados no deben ser utilizados para: generar respuestas hacia los clientes o construir comandos a ser utilizados contra servicios externos, como los provistos por el sistema operativo, motores de bases de datos o el mismo intérprete. A estos componentes a los que no queremos que lleguen valores manchados, los denominamos "sumidero sensible".

img4

Si un valor manchado alcanza un sumidero sensible, entonces existe una vulnerabilidad en el programa.

De esto se deduce que los valores manchados deben ser limpiados antes de que el flujo del programa los lleve hacia un sumidero sensible.

Esto es correcto, pero debemos preguntarnos: ¿son todas las manchas iguales? ¿Una función limpiadora puede quitar todas las manchas de un valor? ¿Son todos los sumideros sensibles al mismo tipo de mancha? NO.

  • Un valor puede tener más de un tipo de mancha.
  • Una función limpiadora, por lo general, limpia un solo tipo de mancha.
  • Un sumidero es sensible a un solo tipo de mancha.

Cuando un valor ingresa al programa, no sabemos que manchas tiene ¿Debe un desarrollador aplicar todas las funciones limpiadoras a cada valor que procede de una fuente no confiable? No toda función limpiadora es apropiada para procesar todos los valores manchados e incluso hacerlo consumiría recursos en forma innecesaria.

Una mejor estrategia es, dado un tipo de mancha determinado, aplicar una función limpiadora de esta mancha solo a aquellos valores que alcanzarán un sumidero sensible a la mancha en cuestión.

img5

En concreto. Este valor está manchado con SQL Injection y no deberíamos dejar que forme parte de una sentencia que se va a ejecutar contra una base de datos. Pero, en principio, no habría problemas en que se use para renderizar una página que será devuelta al usuario.

Algo similar sucede con este valor. Si bien está manchado para XSS. En principio no habría problemas en que se utilice para generar un comando a ejecutar contra el sistema operativo.

img7

img8

Tres menciones sobre la implementación antes de ver ejemplos de uso.

1) Existen 2 formas generales de implementar este tipo de análisis. De forma estática o de forma dinámica. Una implementación estática realiza la detección analizando el texto de un programa, sin ejecutarlo. Mientras que una implementación dinámica, analiza el coportamiento del programa. Esta es una implementación dinámica, por lo que va a requerir que el programa a evaluar sea ejecutado y utilizado: ejecutando manualmente casos de uso o corriendo tests automatizados.

2) El uso de módulo requiere que sea importado dentro del programa a evaluar y hacer algunas modificaciones en el código fuente. Es intrusivo.

3) La implementación se basa en el uso de decoradores. Existen decoradores para marcar dentro del programa los 3 tipos de elementos del modelo de las manchas: FUENTES NO CONFIABLES, FUNCIONES LIMPIADORAS Y SUMIDEROS SENSIBLES.

img9

img10

Cada tipo de mancha con la que va a trabajar el módulo es identificada por un entero. En la lista KEYS que se ve en la pantalla, XSS es identificada con el 0, SQLI con el 1 y así. Esta lista de valores debe ser modificada si se quiere agregar nuevas vulnerabilidades para que sean reconocidas por la herramienta.

TAINTED es la la estrucutura de datos dónde se lleva el registro de qué variables están manchadas. Es un diccionario dónde las claves son los enteros de la lista KEYS sus valores conjuntos, sets, inicialmente vacíos. Cuando se recibe un valor manchado del exterior, este es almacenado en todos los conjuntos, ya que por defecto tiene todas las manchas y a medida que se va limpiando, es eliminado del conjunto correspondiente.

STR, en mayúsuculas, es una clase que envuelve a la clase str, en minúsculas, provista por Python. Ya que en Python los strings son inmutables, siempre que se procesa un string, por ejemplo cuando se obtiene un substring, se crea un nuevo objeto. Esta clase es necesaria para sobreescribir los métodos que generan un nuevo string y así hacer que la mancha perdure a lo largo del flujo del programa. Por ejemplo:

img11

Si a está manchada, y b está limpia. El string que se obtiene de concatenarlos, también está manchado. Lo mismo sucede con otras operaciones sobre strings: multiplicación, rebanada, formateo, métodos como upper que convierten el string a mayúsculas, o split que devuelve una lista de strings. Por ejemplo, si en este último caso 'a' sería un string de palabras separadas por coma, esta llamada al método split devolvería una lista de palabras y cada una estaría manchada.

Los siguientes elementos a mostrar son los decoradores necesarios para que los elementos del programa manchen variables, las limpien o se alarmen.

img12untrusted es un decorador utilizado para indicar que los valores retornados por una función o método no son confiables. Como los valores no confiables pueden contener potencialmente cualquier tipo de mancha, estos valores son marcados como manchados por todos los tipos de machas.

Si se tiene acceso a la definición de la función o método, se puede utilizar la sintáxis provista por Python para aplicar el decorador.

Al usar módulos de terceros, podemos aplicar de todas formas el decorador. Este ejemplo es de un programa que utiliza el framework web.py.

img13

Algunos frameworks funcionan de una forma diferente: le piden al programador que escriba cierta función o método de forma tal que luego el framework las utiliza para pasarle como parámetro al programa de usuario los valores recibidos desde el exterior. El framework para hacer aplicaciones de red, Twisted, provee un claro ejemplo  cuando se extiende la clase LineOnlyReceiver. Nos pide sobreescribir el método lineReceived, el cual se ejecutará cada vez que se recibe una cadena desde el exterior.

En estos casos, utilizar el decorador untrusted es engorroso o incluso imposible. En su lugar se debe utilizar el decorador untrusted_args, que recibe 2 argumentos opcionales: una lista de posiciones de argumentos no confiables y una lista de argumentos de palabra clave.

En el ejemplo se indica que el argumento de posición 1 no es confiable, y debe ser mancado como tal. Entonces cuando se ejecute el método, el valor recibido será manchado.

Tanto para el decorador untrusted como untrusted_args, se utiliza la siguiente regla para marcar valores:

  1. Si el valor a manchar es un string, se utiliza para crear una instancia de STR y este objeto es guardado en TAINTED.
  2. Si el valor es una lista. Se aplica este algoritmo a todos los elementos de la lista y se retorna una nueva lista con el resultado de cada aplicación.
  3. Si el valor es un diccionario. Se aplica este algoritmo a todos los valores del diccionario y se retorna un nuevo diccionario con las claves originales y los resultados de cada aplicaicón como valores.

img14

Una vez que los valores manchados ingresan en el programa, fluirán a lo largo del mismo y en algún momento, puede ser que se encuentren con una función limpiadora como esta. texto_plano elimina las marcas html de un string.

cleaner es un decorador utilizado para indicar que un método o función tiene la habilidad de limpiar manchas en un valor. Se debe indicar mediante un parámetro el tipo de mancha que la función es capás de limpiar. Como con los otros decoradores, tenemos dos formas de aplicarlo: dependiendo de si tenemos acceso o no a la definición de la función limpiadora.

Funciona de la siguiente forma: si la función texto_plano decorada se aplica al primer ejemplo, nada cambia. Como el valor retornado es distinto al argumento, un proceso de limpieza fue llevado acabo. El valor argumento sigue en la estructura de datos TAINTED.

En cambio, si es aplicada al segundo ejemplo, el valor argumento es igual al retornado, no se limpió nada por que no se requería limpiar nada. El valor original, presumido manchado, en realidad no estaba manchado (para XSS) y por eso es removido del conjunto correspondiente.

img15

Los últimos elementos a marcar en el programa son los sumideros sensibles.

Se utiliza el decorador ssink para marcar aquellas funciones o métodos que no queremos sean alcanzadas por valores manchados.

La función eval de Python es un típico ej. de sumidero sensible a ataques de Interpreter Injection, ya que interpreta ciegamente el string que recibe como  argumento.

Supongamos q nuestro programa es una calculadora e implementamos la funcionalidad de sumar 2 números evaluando el string formado por: un primer valor recibido del exterior, el signo más y un segundo valor recibido desde el exterior. Si los valores no son apropiadamente validados, un atacante, utilizando nuestra calculadora, podría leer el contenido de archivos locales, borrarlos, colgar el programa o cualquier otra cosa que se pueda hacer desde el intérprete de Python.

Aplicando el decorador a eval podemos detectar cuando esta función es utilizada con un valor no confiable como parámetro. Otra forma de lograr el mismo efecto en el ejemplo sería aplicando el decorador a la función suma en lugar de a eval.

img16

El segundo ejemplo también es tomado del framework web.py y marca como sumideros sensibles a SQL Injection los métodos delete, select e insert del objeto que accede a la base de datos.

img17

Con las fuentes inseguras, las funciones limpiadoras y los sumideros sensibles marcados en el programa, podemos ejecutarlo y utilizarlo para ir obteniendo las advertencias sobre posibles problemas de seguridad. Se pueden obtener mensajes de este tipo por la salida estándar, escribir en un archivo o daler cualquier otro destino.

img18

Algunas otras cosas que podemos encontrar con/en el módulo:

  • una batería de más de 80 tests que prueban su funcionalidad
  • la posibilidad de definir la función que se ejecutará cuando una variable manchada con X alcanza un sumidero sensible a X
  • la posibilidad de establecer una bandera que indique si la ejecución se debe cortar o no cuando una variable manchada con X alcanza un sumidero sensible a X
  • un nuevo decorador, validator, para marcar funciones que retornan un valor booleano indicando si una variable puede usarse o no
  • funciones auxiliares para manchar un valor o para testear si un valor está manchado

img19

Último slide de la charla. Todo lo que mostré hasta ahora es un experimento. Puede haber errores u omisiones, de hecho mientras lo fui desarrollando, al probarlo con aplicaciones reales encontraba errores en la implementación o situaciones en los que no podía aplicarlo, lo que me obligaba a darle una vuelta más de rosca a la implementación: así surgió por ejemplo el decorador untrusted_args.

La pregunta ahora es cómo seguimos, y el objetivo de la charla era justamente presentar esta idea entre desarrolladores Python para continuarla en conjunto. Hay  muchas que podemos pensar, desde detalles de implementación hasta casos de uso que se pueden lograr, integración con frameworks de desarrollo para que alguien que se sienta a codificar un sistema tenga facilitada la aplicación de esta herramienta.

Por ejemplo, para web.py sería relativamente sencillo crear un parche para que por defecto los valores recibidos estén manchados. En Django hice el ejercicio de escribir un middleware que haga ese trabajo. Y así, es necesario ir probando cómo se puede adaptar el módulo a herramientas de desarrollo ya conocidas.

Hace poco cree una lista de correos en Google Groups para charlar estos temas, así que los que estén interesados en discutir al respecto y desarrollar esta herramienta, pueden suscribirse.

img20

img21



Django Middleware

Mientras estudié el tema para hacer un experimento, traduje el 80% de la documentación oficial. La dejo aquí más algunos condimentos personales.

Middleware

Es un sub framework que permite modificaciones al sistema de procesamiento de request/response de Django. Es un sistema de plugins liviano y de bajo nivel que permite alterar globalmente las entradas y salidas de Django.

Cada componente middleware es responsable de hacer alguna función específica.

Activar componentes middleware

Para hacerlo, añadirlo a la lista MIDDLEWARE_CLASSES en la configuración de Django (settings.py). En esta lista, cada componente se representa por un string: el camino completo al nombre de la clase. Por ejemplo:

MIDDLEWARE_CLASSES = (

'django.middleware.common.CommonMiddleware',

'django.contrib.sessions.middleware.SessionMiddleware',

'django.contrib.auth.middleware.AuthenticationMiddleware',

)

En el tratamiento de requests y la generación de responses, existen dos faces:

1) Fase Request (se llama a los métodos process_request() y process_view())

2) Fase Response (se llama a los métodos process_response() y process_exception())

En la primera, las clases son aplicadas desde la primera a la última, según el orden de la lista mencionada. En la segunda fase, se aplican en orden inverso; podemos pensarlo como una cebolla:

middleware

Una instalación de Django puede funcionar sin ningún middleware, pero esto no es recomendado.

Para escribir middleware propios

Cada componente es una clase Python, que no tiene que extender a ninguna clase en particular y debe definir uno o más de los siguientes métodos.

process_request(self, request)

request es un objeto HttpRequest. El método es llamada por cada request, antes de que Django decida que vista ejecutar.

Debe retornar None o un objeto HttpResponse. Si retorna None, Django seguirá procesando el request, ejecutando los otros middlewares y luego la vista apropiada. Si retorna un objeto HttpResponse, Django no hará nada más, solo retornar ese objeto.

process_view(self, request, view_func, view_args, view_kwargs)

request es un objeto HttpRequest. view_func es la función Python que Django está por usar. (Es el objeto function, no el nombre de la función en un string). view_args es un alista de argumentos posicionales que serán pasados a la vista. Y view_kwargs es un diccionario de argumentos de palagra clave que serán pasados a la vista. Ni view_args ni view_kwargs incluye al primer argumento de la vista (request).

process_view() es llamado antes de que Django ejecute la vista. Debe retornar None o un objeto HttpResponse. Si retorna None, Django seguirá procesando el request, ejecutando otros process_view() y luego la vista apropiada. Si retorna un objeto HttpResponse, Django no hará nada más, solo retornar ese objeto.

process_response(self, request, response)

request es un objeto HttpRequest. response es un objeto HttpResponse retornado por una vista de Django.

process_response() debe retornar un objeto HttpResponse. Puede altener el objeto response dado o puede crear uno nuevo.

A diferencia de proces_request() y process_view(), este siempre es ejecutado.

process_exception(self, request, exception)

request es un objeto HttpRequest. exception es un objeto Exception lanzado por la vista.

Django llama a process_exception() cuando la vista lanza una excepción. process_exception() debe retornar None o un objeto HttpResponse. Si retorna un objeto HttpResponse, la respuesta es devuelta al navegador. De lo contrario, el sistema por defecto para manejo de excepciones entra en acción.

__init__

Por lo general estas clases no tienen estado; simplemente contienen a los anteriores métodos. Se puede usar el método init pero se debe tener en cuenta que Django inicializa estas clases sin argumentos y que el método init es llamado solo una vez, cuando el servidor web arranca.

Experimento

Con lo anterior en mente, me quedaron algunas dudas al respecto:

  • si modifico los argumentos de view_func en process_view y retorno None, ¿estos son pasados modificados a la vista?
  • si lo anterior es falso, puedo lograr el mismo efecto haciendo:
    def process_view(self,request, view_func, view_args, view_kwargs):
    
        # modificar request, view_args y view_kwargs
    
        return view_func(request, view_args, view_kwargs)
    ?
  • ¿puedo definir process_response de tal forma que examine response?


Una gran oportunidad de conocer Python!

El 4 y 5 de Septiembre se va a llevar a cabo en Capital Federal el evento PyCon Argentina 2009.

Organizado por la comunidad, con entrada libre y gratuita, se llevará a cabo en la Universidad de Belgrano. Habrá charlas plenarias, programadas y relámpago.

PyCon 2009 Argentina

¿Querés conocer Python? ¿Venís oyendo sobre este lenguaje por mucho tiempo y todavía no te animaste a probarlo? Esta es tu oportunidad! Te esperamos!


Decoradores en Python (II) - Decoradores con parámetros

Un año después del primer artículo, llega el segundo. ¿Por qué tardó tanto? Por lo general mis artículos técnicos surgen de algún problema que se me presenta y para el cual necesito investigar antes de poder solucionarlo. El artículo anterior cubría todo lo que necesité hacer con decoradores en Python hasta el mes pasado, cuando necesité decoradores con parámetros.

Si no leíste el artículo anterior, te recomiendo que lo hagas antes de seguir: Decoradores en Python (I).

Decoradores con Parámetros

Cuando quise escribir un decorador con un parámetro me encontré con errores que ni siquiera entendía. No solo que los estaba escribiendo mal, sino que también los estaba usando mal. Te voy a evitar el sufrimiento.

Un decorador con parámetro se aplica así (siendo deco un decorador y 1 el argumento utilizado):

@deco(1)

def funcion_a_decorar(a, b, c):

    pass

Creo que la raíz de mi confusión fue el azúcar sintáctica (si, el @). Así que vamos a sacarlo y ver cómo se usaría este decorador en una versión de Python más vieja:

def funcion_a_decorar(a, b, c):

    pass
funcion_a_decorar = deco(1)(funcion_a_decorar)

Esto luce más claro para mi: deco es llamado con un argumento y el resultado tiene que ser algún objeto que pueda ser llamado con una función como parámetro para... decorarla. ¿Se entiende la idea? Vamos a definir deco, va a recibir un parámetro y utilizarlo para crear un decorador como los del artículo anterior. Finalmente retorna este decorador interno.

Agreguemos semántica al ejemplo. Mi decorador con parámetro recibirá un número, este número se usará para indicar cuantas veces queremos ejecutar la función decorada.

def deco(i):

    def _deco(f):

        def inner(*args, **kwargs):

            for n in range(i):

                r = f(*args, **kwargs)

            return r

        return inner

    return _deco

Como una convención personal, uso para el nombre de la segunda función _{nombre de la primer funcion}. Notemos entonces que _deco es un decorador dinámico, dependiendo del parámetro i, la función inner se compilará de una forma o de otra. Apliquemos el decorador:

@deco(2)

def saluda(nombre):

    print "hola", nombre
>>> saluda("juanjo")

hola juanjo

hola juanjo
@deco(3)

def suma1():

    global n

    n += 1
>>> n = 0

>>> suma1()

>>> n

3

Cuando aplicamos deco, se ejecuta deco, se compila _deco, se aplica _deco a la función que definimos y se compila inner utilizando un valor dado para i. Cuando llamamos a nuestra función (saluda, o suma1, en los ejemplos) se ejecuta inner.

¡Espero que se haya entendido!

Si no...

Si en lo anterior no fui lo suficientemente claro (por favor quejate en un comentario), no todo está perdido. Te puedo entregar un decorador para decoradores que convierte a tu decorador en un decorador con parámetros. ¿Qué tal?

def decorador_con_parametros(d):

    def decorador(*args, **kwargs):

        def inner(func):

            return d(func, *args, **kwargs)

        return inner

    return decorador

Original usando lambda en http://pre.activestate.com/recipes/465427/

Se usa así:

@decorador_con_parametros

def deco(func, i):

    def inner(*args, **kwargs):

        for n in range(i):

           r = func(*args, **kwargs)

        return r

    return inner
@deco(2)

def saludar(nombre):

    print "chau", nombre
>>> saludar("juanjo")

chau juanjo

chau juanjo

Para la próxima

Para el próximo artículo voy a explorar utilizar clases decoradoras en lugar de funciones decoradoras. Si bien todavía no lo terminé de investigar, me parece un enfoque que permite escribir código más organizado. Veremos! update: aquí está.


La historia de Python: El problema con la división entre enteros

El siguiente texto es una traducción del artículo The Problem with Integer Division de Guido van Rossum publicado en http://python-history.blogspot.com/.

El problema con la división entre enteros

La forma en que Python maneja la división entre enteros es un ejemplo de errores iniciales con enormes consecuencias. Como se mencionó anteriormente, cuando Python fue creado abandoné el enfoque con el que ABC abordaba los números. Por ejemplo, en ABC, cuando dividías dos enteros, el resultado era un número racional exacto que representaba el resultado. En Python, sin embargo, la división entre enteros trunca el resultado a un entero.

En mi experiencia, los números racionales no son tan buenos como los diseñadores de ABC esperaban. Una experiencia típica podría ser escribir un programa simple con alguna aplicación comercial (digamos, calcular tus impuestos) y encontrar que corre más lento de lo esperado. Luego de depurar se encuentra la causa: internamente el programa usa números racionales con miles de dígitos de precisión para representar valores que serán truncados a dos o tres dígitos al ser impresos. Esto podía ser fácilmente solucionado empezando una suma con un cero inexacto, pero esto era a menudo no intuitivo y difícil de depurar para los principiantes.

Entonces en Python utilicé el otro modelo numérico que me era familiar, C. C tiene enteros y números de punto flotante de varios tamaños. Entonces elegí representar los enteros de Python con longs de C (garantizando por lo menos 32 bits de precisión) y los números de punto flotante con doubles de C. Luego añadí un tipo entero con precisión arbitraria que llamé "long".

El mayor error fue que también tomé prestada una regla que tenía sentido en C pero no en un lenguaje de tan alto nivel. Para las operaciones aritméticas estándares, incluyendo la división, el resultado siempre sería del mismo tipo que los operandos. Para empeorar las cosas, inicialmente usé otra regla equivocada que prohibía usar aritmética mixta, con la idea de hacer que las implementaciones de los distintos tipos sean independientes entre sí. Entonces, en un principio no se podía sumar un int con un float, o incluso un int con un long. Luego de que Python se distribuyera públicamente, Time Peters rápidamente me convenció de que esto era realmente una mala idea e introdujo una aritmética mixta con las reglas de coerción típicas. Por ejemplo, mezclar un operando int y uno long convertiría el argumento de tipo int a long y retornaría un long como resultado y la mezcla con un float convertiría al argumento int o long en un float y retornaría un resultado float.

Desafortunadamente, el daño estaba hecho: la división entre enteros daba un resultado entero. Estarás pensando "¿por qué era esto tan malo?". ¿Estaba haciendo escándalo por nada? Históricamente, la propuesta para cambiar esto ha tenido algunas oposiciones duras por parte de quienes pensaban que aprender división entre enteros era una de los más útiles "ritos de iniciación" para todos los programadores. Así que déjenme explicarles las razones por la cual considero esto un error de diseño.

Cuando escribes una función para implementar un algoritmo numérico (por ejemplo, calcular las fases de la luna) esperas que los argumentos sean números de punto flotante. Sin embargo, ya que Python no tiene declaración de tipos, nada evita que la función sea llamada con argumentos enteros. En un lenguaje estáticamente tipado, como C, el compilador convertiría los argumentos a floats, pero Python no sabe nada de eso; el algoritmo corre con valores enteros hasta que las maravillas de la aritmética mixta produzca resultados intermedios que sean floats.

Para todo excepto para la división, los enteros se comportan de la misma forma que sus números de punto flotante correspondiente. Por ejemplo, 1+1 es igual a 2 de la misma forma que 1.0+1.0 es igual a 2.0, y así. Por lo tanto uno puede fácilmente confundirse y esperar que los algoritmos numéricos funcionen independientemente de si son ejecutados con argumentos enteros o de punto flotante. Sin embargo, cuando hay una división, y existe la posibilidad de que los dos operandos sean enteros, el resultado numérico es truncado silenciosamente, esencialmente introduciendo un gran error potencial en el cómputo. Aunque uno puede escribir código defensivo que convierta todos los argumentos a float apenas se introducen, esto es tedioso, y no mejora la legibilidad o mantenibilidad del código. Adicionalmente, evita que el algoritmo sea usado con números complejos (aunque eso sería en casos muy especiales).

Nuevamente, todo esto es un problema porque Python no convierte argumentos automáticamente a algún tipo declarado. Pasar un argumento inválido, por ejemplo un string, es generalmente atrapado rápido por que muy pocas operaciones aceptan mezclar operados strings/números (siendo la excepción la multiplicación). Sin embargo, pasar un entero puede causar una respuesta cercana a la correcta, pero con error; difícil de depurar o incluso advertir (esto me pasó recientemente en un programa que dibuja un reloj analógico; las posiciones de las manecillas eran calculadas incorrectamente debido al truncamiento, pero el error era apenas detectable excepto en ciertas horas del día).

Arreglar la división entre enteros no fue una tarea fácil debido a los programas que dependían de este truncamiento. Se añadió al lenguaje un operador de división truncada (//) que provee la misma funcionalidad. Además, se introduzco un mecanismo ("from __future__ import division") para habilitar fácilmente la nueva semántica para la división entre enteros. Finalmente, se agregó una bandera para la línea de comando (-Qxxx) para cambiar el comportamiento y para asistir la conversión de programas. Afortunadamente, el comportamiento correcto se convirtió en el comportamiento por defecto en Python 3000.

Traducido por Juan José Conti.
Revisado por César Portela.
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.


Euler 8 (Python)

Enunciado 8

Encontrar el mayor producto de cinco dígitos consecutivos en este número de 100 dígitos:

73167176531330624919225119674426574742355349194934 96983520312774506326239578318016984801869478851843 85861560789112949495459501737958331952853208805511 12540698747158523863050715693290963295227443043557 66896648950445244523161731856403098711121722383113 62229893423380308135336276614282806444486645238749 30358907296290491560440772390713810515859307960866 70172427121883998797908792274921901699720888093776 65727333001053367881220235421809751254540594752243 52584907711670556013604839586446706324415722155397 53697817977846174064955149290862569321978468622482 83972241375657056057490261407972968652414535100474 82166370484403199890008895243450658541227588666881 16427171479924442928230863465674813919123162824586 17866458359124566529476545682848912883142607690042 24219022671055626321111109370544217506941658960408 07198403850962455444362981230987879927244284909188 84580156166097919133875499200524063689912560717606 05886116467109405077541002256983155200055935729725 71636269561882670428252483600823257530420752963450

Solución

>>> b = 7316717653133062491922511967442657474235534919493496983520312774506326239578318016984801869478851843858615607891129494954595017379583319528532088055111254069874715852386305071569329096329522744304355766896648950445244523161731856403098711121722383113622298934233803081353362766142828064444866452387493035890729629049156044077239071381051585930796086670172427121883998797908792274921901699720888093776657273330010533678812202354218097512545405947522435258490771167055601360483958644670632441572215539753697817977846174064955149290862569321978468622482839722413756570560574902614079729686524145351004748216637048440319989000889524345065854122758866688116427171479924442928230863465674813919123162824586178664583591245665294765456828489128831426076900422421902267105562632111110937054421750694165896040807198403850962455444362981230987879927244284909188845801561660979191338754992005240636899125607176060588611646710940507754100225698315520005593572972571636269561882670428252483600823257530420752963450

>>> s = str(b)

>>> s

'7316717653133062491922511967442657474235534919493496983520312774506326239578318016984801869478851843858615607891129494954595017379583319528532088055111254069874715852386305071569329096329522744304355766896648950445244523161731856403098711121722383113622298934233803081353362766142828064444866452387493035890729629049156044077239071381051585930796086670172427121883998797908792274921901699720888093776657273330010533678812202354218097512545405947522435258490771167055601360483958644670632441572215539753697817977846174064955149290862569321978468622482839722413756570560574902614079729686524145351004748216637048440319989000889524345065854122758866688116427171479924442928230863465674813919123162824586178664583591245665294765456828489128831426076900422421902267105562632111110937054421750694165896040807198403850962455444362981230987879927244284909188845801561660979191338754992005240636899125607176060588611646710940507754100225698315520005593572972571636269561882670428252483600823257530420752963450'

>>> l = list(s)

>>> l

['7', '3', '1', '6', '7', '1', '7', '6', '5', '3', '1', '3', '3', '0', '6', '2', '4', '9', '1', '9', '2', '2', '5', '1', '1', '9', '6', '7', '4', '4', '2', '6', '5', '7', '4', '7', '4', '2', '3', '5', '5', '3', '4', '9', '1', '9', '4', '9', '3', '4', '9', '6', '9', '8', '3', '5', '2', '0', '3', '1', '2', '7', '7', '4', '5', '0', '6', '3', '2', '6', '2', '3', '9', '5', '7', '8', '3', '1', '8', '0', '1', '6', '9', '8', '4', '8', '0', '1', '8', '6', '9', '4', '7', '8', '8', '5', '1', '8', '4', '3', '8', '5', '8', '6', '1', '5', '6', '0', '7', '8', '9', '1', '1', '2', '9', '4', '9', '4', '9', '5', '4', '5', '9', '5', '0', '1', '7', '3', '7', '9', '5', '8', '3', '3', '1', '9', '5', '2', '8', '5', '3', '2', '0', '8', '8', '0', '5', '5', '1', '1', '1', '2', '5', '4', '0', '6', '9', '8', '7', '4', '7', '1', '5', '8', '5', '2', '3', '8', '6', '3', '0', '5', '0', '7', '1', '5', '6', '9', '3', '2', '9', '0', '9', '6', '3', '2', '9', '5', '2', '2', '7', '4', '4', '3', '0', '4', '3', '5', '5', '7', '6', '6', '8', '9', '6', '6', '4', '8', '9', '5', '0', '4', '4', '5', '2', '4', '4', '5', '2', '3', '1', '6', '1', '7', '3', '1', '8', '5', '6', '4', '0', '3', '0', '9', '8', '7', '1', '1', '1', '2', '1', '7', '2', '2', '3', '8', '3', '1', '1', '3', '6', '2', '2', '2', '9', '8', '9', '3', '4', '2', '3', '3', '8', '0', '3', '0', '8', '1', '3', '5', '3', '3', '6', '2', '7', '6', '6', '1', '4', '2', '8', '2', '8', '0', '6', '4', '4', '4', '4', '8', '6', '6', '4', '5', '2', '3', '8', '7', '4', '9', '3', '0', '3', '5', '8', '9', '0', '7', '2', '9', '6', '2', '9', '0', '4', '9', '1', '5', '6', '0', '4', '4', '0', '7', '7', '2', '3', '9', '0', '7', '1', '3', '8', '1', '0', '5', '1', '5', '8', '5', '9', '3', '0', '7', '9', '6', '0', '8', '6', '6', '7', '0', '1', '7', '2', '4', '2', '7', '1', '2', '1', '8', '8', '3', '9', '9', '8', '7', '9', '7', '9', '0', '8', '7', '9', '2', '2', '7', '4', '9', '2', '1', '9', '0', '1', '6', '9', '9', '7', '2', '0', '8', '8', '8', '0', '9', '3', '7', '7', '6', '6', '5', '7', '2', '7', '3', '3', '3', '0', '0', '1', '0', '5', '3', '3', '6', '7', '8', '8', '1', '2', '2', '0', '2', '3', '5', '4', '2', '1', '8', '0', '9', '7', '5', '1', '2', '5', '4', '5', '4', '0', '5', '9', '4', '7', '5', '2', '2', '4', '3', '5', '2', '5', '8', '4', '9', '0', '7', '7', '1', '1', '6', '7', '0', '5', '5', '6', '0', '1', '3', '6', '0', '4', '8', '3', '9', '5', '8', '6', '4', '4', '6', '7', '0', '6', '3', '2', '4', '4', '1', '5', '7', '2', '2', '1', '5', '5', '3', '9', '7', '5', '3', '6', '9', '7', '8', '1', '7', '9', '7', '7', '8', '4', '6', '1', '7', '4', '0', '6', '4', '9', '5', '5', '1', '4', '9', '2', '9', '0', '8', '6', '2', '5', '6', '9', '3', '2', '1', '9', '7', '8', '4', '6', '8', '6', '2', '2', '4', '8', '2', '8', '3', '9', '7', '2', '2', '4', '1', '3', '7', '5', '6', '5', '7', '0', '5', '6', '0', '5', '7', '4', '9', '0', '2', '6', '1', '4', '0', '7', '9', '7', '2', '9', '6', '8', '6', '5', '2', '4', '1', '4', '5', '3', '5', '1', '0', '0', '4', '7', '4', '8', '2', '1', '6', '6', '3', '7', '0', '4', '8', '4', '4', '0', '3', '1', '9', '9', '8', '9', '0', '0', '0', '8', '8', '9', '5', '2', '4', '3', '4', '5', '0', '6', '5', '8', '5', '4', '1', '2', '2', '7', '5', '8', '8', '6', '6', '6', '8', '8', '1', '1', '6', '4', '2', '7', '1', '7', '1', '4', '7', '9', '9', '2', '4', '4', '4', '2', '9', '2', '8', '2', '3', '0', '8', '6', '3', '4', '6', '5', '6', '7', '4', '8', '1', '3', '9', '1', '9', '1', '2', '3', '1', '6', '2', '8', '2', '4', '5', '8', '6', '1', '7', '8', '6', '6', '4', '5', '8', '3', '5', '9', '1', '2', '4', '5', '6', '6', '5', '2', '9', '4', '7', '6', '5', '4', '5', '6', '8', '2', '8', '4', '8', '9', '1', '2', '8', '8', '3', '1', '4', '2', '6', '0', '7', '6', '9', '0', '0', '4', '2', '2', '4', '2', '1', '9', '0', '2', '2', '6', '7', '1', '0', '5', '5', '6', '2', '6', '3', '2', '1', '1', '1', '1', '1', '0', '9', '3', '7', '0', '5', '4', '4', '2', '1', '7', '5', '0', '6', '9', '4', '1', '6', '5', '8', '9', '6', '0', '4', '0', '8', '0', '7', '1', '9', '8', '4', '0', '3', '8', '5', '0', '9', '6', '2', '4', '5', '5', '4', '4', '4', '3', '6', '2', '9', '8', '1', '2', '3', '0', '9', '8', '7', '8', '7', '9', '9', '2', '7', '2', '4', '4', '2', '8', '4', '9', '0', '9', '1', '8', '8', '8', '4', '5', '8', '0', '1', '5', '6', '1', '6', '6', '0', '9', '7', '9', '1', '9', '1', '3', '3', '8', '7', '5', '4', '9', '9', '2', '0', '0', '5', '2', '4', '0', '6', '3', '6', '8', '9', '9', '1', '2', '5', '6', '0', '7', '1', '7', '6', '0', '6', '0', '5', '8', '8', '6', '1', '1', '6', '4', '6', '7', '1', '0', '9', '4', '0', '5', '0', '7', '7', '5', '4', '1', '0', '0', '2', '2', '5', '6', '9', '8', '3', '1', '5', '5', '2', '0', '0', '0', '5', '5', '9', '3', '5', '7', '2', '9', '7', '2', '5', '7', '1', '6', '3', '6', '2', '6', '9', '5', '6', '1', '8', '8', '2', '6', '7', '0', '4', '2', '8', '2', '5', '2', '4', '8', '3', '6', '0', '0', '8', '2', '3', '2', '5', '7', '5', '3', '0', '4', '2', '0', '7', '5', '2', '9', '6', '3', '4', '5', '0']

>>> i = [int(x) for x in l]

>>> i

[7, 3, 1, 6, 7, 1, 7, 6, 5, 3, 1, 3, 3, 0, 6, 2, 4, 9, 1, 9, 2, 2, 5, 1, 1, 9, 6, 7, 4, 4, 2, 6, 5, 7, 4, 7, 4, 2, 3, 5, 5, 3, 4, 9, 1, 9, 4, 9, 3, 4, 9, 6, 9, 8, 3, 5, 2, 0, 3, 1, 2, 7, 7, 4, 5, 0, 6, 3, 2, 6, 2, 3, 9, 5, 7, 8, 3, 1, 8, 0, 1, 6, 9, 8, 4, 8, 0, 1, 8, 6, 9, 4, 7, 8, 8, 5, 1, 8, 4, 3, 8, 5, 8, 6, 1, 5, 6, 0, 7, 8, 9, 1, 1, 2, 9, 4, 9, 4, 9, 5, 4, 5, 9, 5, 0, 1, 7, 3, 7, 9, 5, 8, 3, 3, 1, 9, 5, 2, 8, 5, 3, 2, 0, 8, 8, 0, 5, 5, 1, 1, 1, 2, 5, 4, 0, 6, 9, 8, 7, 4, 7, 1, 5, 8, 5, 2, 3, 8, 6, 3, 0, 5, 0, 7, 1, 5, 6, 9, 3, 2, 9, 0, 9, 6, 3, 2, 9, 5, 2, 2, 7, 4, 4, 3, 0, 4, 3, 5, 5, 7, 6, 6, 8, 9, 6, 6, 4, 8, 9, 5, 0, 4, 4, 5, 2, 4, 4, 5, 2, 3, 1, 6, 1, 7, 3, 1, 8, 5, 6, 4, 0, 3, 0, 9, 8, 7, 1, 1, 1, 2, 1, 7, 2, 2, 3, 8, 3, 1, 1, 3, 6, 2, 2, 2, 9, 8, 9, 3, 4, 2, 3, 3, 8, 0, 3, 0, 8, 1, 3, 5, 3, 3, 6, 2, 7, 6, 6, 1, 4, 2, 8, 2, 8, 0, 6, 4, 4, 4, 4, 8, 6, 6, 4, 5, 2, 3, 8, 7, 4, 9, 3, 0, 3, 5, 8, 9, 0, 7, 2, 9, 6, 2, 9, 0, 4, 9, 1, 5, 6, 0, 4, 4, 0, 7, 7, 2, 3, 9, 0, 7, 1, 3, 8, 1, 0, 5, 1, 5, 8, 5, 9, 3, 0, 7, 9, 6, 0, 8, 6, 6, 7, 0, 1, 7, 2, 4, 2, 7, 1, 2, 1, 8, 8, 3, 9, 9, 8, 7, 9, 7, 9, 0, 8, 7, 9, 2, 2, 7, 4, 9, 2, 1, 9, 0, 1, 6, 9, 9, 7, 2, 0, 8, 8, 8, 0, 9, 3, 7, 7, 6, 6, 5, 7, 2, 7, 3, 3, 3, 0, 0, 1, 0, 5, 3, 3, 6, 7, 8, 8, 1, 2, 2, 0, 2, 3, 5, 4, 2, 1, 8, 0, 9, 7, 5, 1, 2, 5, 4, 5, 4, 0, 5, 9, 4, 7, 5, 2, 2, 4, 3, 5, 2, 5, 8, 4, 9, 0, 7, 7, 1, 1, 6, 7, 0, 5, 5, 6, 0, 1, 3, 6, 0, 4, 8, 3, 9, 5, 8, 6, 4, 4, 6, 7, 0, 6, 3, 2, 4, 4, 1, 5, 7, 2, 2, 1, 5, 5, 3, 9, 7, 5, 3, 6, 9, 7, 8, 1, 7, 9, 7, 7, 8, 4, 6, 1, 7, 4, 0, 6, 4, 9, 5, 5, 1, 4, 9, 2, 9, 0, 8, 6, 2, 5, 6, 9, 3, 2, 1, 9, 7, 8, 4, 6, 8, 6, 2, 2, 4, 8, 2, 8, 3, 9, 7, 2, 2, 4, 1, 3, 7, 5, 6, 5, 7, 0, 5, 6, 0, 5, 7, 4, 9, 0, 2, 6, 1, 4, 0, 7, 9, 7, 2, 9, 6, 8, 6, 5, 2, 4, 1, 4, 5, 3, 5, 1, 0, 0, 4, 7, 4, 8, 2, 1, 6, 6, 3, 7, 0, 4, 8, 4, 4, 0, 3, 1, 9, 9, 8, 9, 0, 0, 0, 8, 8, 9, 5, 2, 4, 3, 4, 5, 0, 6, 5, 8, 5, 4, 1, 2, 2, 7, 5, 8, 8, 6, 6, 6, 8, 8, 1, 1, 6, 4, 2, 7, 1, 7, 1, 4, 7, 9, 9, 2, 4, 4, 4, 2, 9, 2, 8, 2, 3, 0, 8, 6, 3, 4, 6, 5, 6, 7, 4, 8, 1, 3, 9, 1, 9, 1, 2, 3, 1, 6, 2, 8, 2, 4, 5, 8, 6, 1, 7, 8, 6, 6, 4, 5, 8, 3, 5, 9, 1, 2, 4, 5, 6, 6, 5, 2, 9, 4, 7, 6, 5, 4, 5, 6, 8, 2, 8, 4, 8, 9, 1, 2, 8, 8, 3, 1, 4, 2, 6, 0, 7, 6, 9, 0, 0, 4, 2, 2, 4, 2, 1, 9, 0, 2, 2, 6, 7, 1, 0, 5, 5, 6, 2, 6, 3, 2, 1, 1, 1, 1, 1, 0, 9, 3, 7, 0, 5, 4, 4, 2, 1, 7, 5, 0, 6, 9, 4, 1, 6, 5, 8, 9, 6, 0, 4, 0, 8, 0, 7, 1, 9, 8, 4, 0, 3, 8, 5, 0, 9, 6, 2, 4, 5, 5, 4, 4, 4, 3, 6, 2, 9, 8, 1, 2, 3, 0, 9, 8, 7, 8, 7, 9, 9, 2, 7, 2, 4, 4, 2, 8, 4, 9, 0, 9, 1, 8, 8, 8, 4, 5, 8, 0, 1, 5, 6, 1, 6, 6, 0, 9, 7, 9, 1, 9, 1, 3, 3, 8, 7, 5, 4, 9, 9, 2, 0, 0, 5, 2, 4, 0, 6, 3, 6, 8, 9, 9, 1, 2, 5, 6, 0, 7, 1, 7, 6, 0, 6, 0, 5, 8, 8, 6, 1, 1, 6, 4, 6, 7, 1, 0, 9, 4, 0, 5, 0, 7, 7, 5, 4, 1, 0, 0, 2, 2, 5, 6, 9, 8, 3, 1, 5, 5, 2, 0, 0, 0, 5, 5, 9, 3, 5, 7, 2, 9, 7, 2, 5, 7, 1, 6, 3, 6, 2, 6, 9, 5, 6, 1, 8, 8, 2, 6, 7, 0, 4, 2, 8, 2, 5, 2, 4, 8, 3, 6, 0, 0, 8, 2, 3, 2, 5, 7, 5, 3, 0, 4, 2, 0, 7, 5, 2, 9, 6, 3, 4, 5, 0]

>>> def mult(lista):

...     return reduce(mul, lista, 1)

...

>>> mult([])

1

>>> mult([1,2,3])

6

>>> for x in range(1001-5):

...     m = mult(i[x:x+5])

...     if m > maxmult:

...             maxmult = m

...

>>> maxmult

40824


La historia de Python: Cómo las excepciones llegaron a ser clases

El siguiente texto es una traducción del artículo How Exceptions Came to be Classes de Guido van Rossum publicado en http://python-history.blogspot.com/.

Cómo las excepciones llegaron a ser clases

Pronto supe que quería que Python utilice excepciones para el manejo de errores. Sin embargo, una parte crítica para que las excepciones funcionen es lograr algún tipo de esquema para identificar distintos tipos de excepciones. En los lenguajes modernos (incluyendo el moderno Python :-), las excepciones son definidas en términos de clases definidas por los usuarios. Sin embargo, en los comienzos de Python, elegí identificar las excepciones por strings. Esto fue desafortunado, pero tenía dos razones para tomar esta aproximación. Primero, aprendí sobre las excepciones en Modula-3, donde las mismas son señales únicas. Segundo, introduje las excepciones antes de introducir las clases definidas por los usuarios.

En teoría, supongo que podría haber creado un nuevo tipo de objeto a medida para ser utilizado en excepciones, pero como todo tipo de objeto a medida requiere un considerable esfuerzo de codificación en C, decidí reutilizar un tipo a medida existente. Y, ya que las excepciones están relacionadas con mensajes de error, pareció natural usar strings para representar excepciones.

Lamentablemente elegí una semántica donde diferentes objetos string podían representar diferentes excepciones, aún cuando tenían el mismo valor (es decir, contenían la misma secuencia de caracteres). Elegí esta semántica porque quería que las excepciones definidas en distintos módulos sean independientes, incluso si sucedía que tenían el mismo valor. La idea era que las excepciones siempre debían ser referenciadas por su nombre, lo que implicaría identidad del objeto, nunca por su valor, lo que requeriría string iguales.

Este enfoque fue influenciado por las excepciones de Modula-3, donde cada declaración de excepción crea una única “señal de excepción” que no puede ser confundida con cualquier otra. Creo que también quería optimizar el testeo de excepciones usando punteros de comparación en vez de valores de strings en un equivocado intento de optimización prematura del tiempo de ejecución (un caso inusual – ¡yo generalmente optimizaba mi propio tiempo de codificación!). La principal razón, sin embargo, es que me preocupaban los conflictos de nombres entre excepciones no relacionadas definidas en diferentes módulos. Intenté usar patrones para adherirme estrictamente a la convención de definir una excepción como una constante global en algunos módulos, y, entonces, usarla por nombre en todo, lanzamiento o captura (esto fue mucho tiempo antes de que ciertos string literales fueran automáticamente “internos”).

¡Ay de mí!, en la práctica las cosas nunca resultan como se espera. Pronto los usuarios de Python descubrieron que dentro del mismo módulo, el compilador byte code unificaba los strings literales (es decir, creaba un sólo objeto compartido para todas las ocurrencias de strings literales con el mismo valor). Así, por accidente, los usuarios encontraron que las excepciones podían ser capturadas especificando el nombre de la excepción o el string literal conteniendo el mensaje de error. Bien, al menos esto parecía trabajar la mayor parte del tiempo. En realidad, esto sólo trabajó para el código definido en el mismo módulo. Si uno intentaba capturar excepciones usando el mensaje de error de la excepción en un módulo diferente, el código se rompía misteriosamente. Es innecesario decir que este es el tipo de cosas que causa una gran confusión.

En 1997, con Python 1.5, introduje clases para excepciones dentro del lenguaje. Aunque este ha sido el enfoque recomendado desde entonces, las excepciones de strings todavía eran soportadas para el uso en determinadas aplicaciones heredadas hasta Python 2.5. Estas fueron eliminadas en Pyton 2.6.

Traducido por César Portela.

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.