__new__ vs __init__

Gabriel Genellina gagsl-py2 en yahoo.com.ar
Jue Nov 15 07:32:22 CET 2007


En Thu, 15 Nov 2007 01:44:31 -0300, Luis Rodrigo Gallardo Cruz  
<rodrigo en nul-unu.com> escribió:

> On Thu, Nov 15, 2007 at 12:06:38AM -0300, Gabriel Genellina wrote:
>> En Wed, 14 Nov 2007 12:19:44 -0300, Luis Rodrigo Gallardo Cruz
>> <rodrigo en nul-unu.com> escribió:
>>
>>> On Wed, Nov 14, 2007 at 03:58:20PM +0100, tny wrote:
>>>> Sé que __new__ es llamado antes de que se cree el objeto, y es el
>>>> encargado de crearlo y devolverlo.
>>>>
>>>> Sé que __init__ es llamado después de __new__ para inicializarlo.
>>>
>>>> ¿Cuándo se debería emplear __init__ y cuando __new__?
>>>
>>> Usa __new__ cuando la creación del objeto no puede hacerse según las
>>> reglas normales de python. Por ejemplo, una forma (un poco fea en mi
>>> opinión) de implementar el patrón 'singleton' es que el __new__ de la
>>> clase, en vez de crear un objeto, regrese la instancia única.
>>>
>>> En general, usar __new__ es mágia. Casi siempre es mejor evitarlo.
>>
>> Uh, porque? No hay nada de magia. Y no tiene que ver con que se salteen  
>> las
>> reglas normales o no.
>
> Hay magia por que alteras el comportamiento "normal". Si uno ve en el
> código
>
>  var = Clase()
>
> uno supone que lo que está pasando es que se crea una nueva instancia
> de Clase. Si alteras __new__ lo que pasa es lo que sea que se te
> ocurrió poner en __new__ Si lo que se te ocurrió poner en __new__ es
> conceptualmente similar a crear una nueva instancia y regresarla, pues
> ni quien se de cuenta. Si no, estas escondiendo mucho comportamiento
> en un lugar inesperado.
>
> No estoy diciendo que sea malo, solo digo que hay que pensarselo, por
> que modificar el comportamiento de partes tan "básicas" debe hacerse
> con cuidado so pena de confundir durante días al pobre que le toque
> leer después ese código.

Tal vez tus comentarios apunten al ejemplo del singleton, porque ahi sí,  
estas cambiando el comportamiento; pero eso es propio de como funciona un  
singleton, no del hecho de usar __new__.

Otro uso común del __new__, aparte del ejemplo de heredar de tipos  
inmutables, sería una factoría de instancias, cuando por algun motivo  
puedo usar una funcion. Ejemplo: supongamos que originalmente yo tenía una  
clase como esta:

class Handler(object):
     def __init__(self, url):
         self.url = url
     def open(self):
         ...

y yo podia hacer cosas como:

h = Handler('http://www.ejemplo.com/nada')
...
if isinstance(h, Handler): # True
   h.open()

Al principio sólo manejaba HTTP, y yo era feliz con eso. Pero despues  
aparecio la necesidad de manejar FTP, asi que desdoblé el codigo en tres  
clases: Handler (abstracta), HandlerHTTP, y HandlerFTP:

class Handler(object):
     ...

class HandlerHTTP(Handler):
     def __init__(self, url):
         # solo acepta urls con protocolo HTTP
         ...

class HandlerFTP(Handler):
     def __init__(self, url):
         # solo acepta urls con protocolo FTP
         ...

Y ahora estoy contento porque mi modelo es extensible, y si mañana aparece  
la necesidad de usar HTTPS por ejemplo, pues escribo una nueva clase  
derivada y ya está.

El problema es que al momento de construir el objeto, tengo que  
discriminar cuál de las subclases voy a usar. Claro, puedo escribir una  
funcion que se fije en la url y cree una u otra (una función factoría).  
Digamos que mi funcion se llama crear_handler; entonces tendría que  
reemplazar todas las referencias a Handler(...) que hay actualmente en el  
codigo, por llamadas a crear_handler(...). Tal vez eso no es aceptable,  
tal vez forma parte de una libreria que usan miles de personas y tengo que  
mantener la compatibilidad. Gran idea: llamar Handler a la funcion (y  
renombrar internamente la clase Handler a otra cosa, digamos HandlerBase).  
En ese caso todo el codigo escrito por los usuarios seguiria funcionando  
sin necesidad de tocar nada. *Peeeeeero* hay un detalle, el resultado de  
Handler('http://...') ya no es una instancia de Handler, o sea, que  
chequeos como isinstance(h, Handler) ya no sirven más (porque Handler  
ahora es una funcion, no una clase). Necesito que Handler siga siendo una  
*clase*, y que devuelva instancias de *esa* clase (o alguna subclase  
derivada, claro).

La unica forma de mantener la compatibilidad y al mismo tiempo permitir  
extensiones en el modelo, es que Handler.__new__ sea su propia factoría, y  
devuelva instancias de la clase derivada que corresponda. No puedo usar  
__init__ porque cuando este metodo es llamado, ya la instancia fue  
construida:

class Handler(object):
     def __new__(cls, url):
         if url.startswith('ftp'): cls = FtpHandler
         elif url.startswith('http'): cls = HttpHandler
         else: raise ValueError, url
         return object.__new__(cls, url)
     def __init__(self, url):
         self.url = url

class FtpHandler(Handler):
     def open(self): print "open de", self

class HttpHandler(Handler):
     def open(self): print "open de", self

py> f = Handler('ftp://pirulo.com')
py> h = Handler('http://otro.com')
py> f.open()
open de <__main__.FtpHandler object at 0x00B571D0>
py> h.open()
open de <__main__.HttpHandler object at 0x00BDDF90>
py> isinstance(h, Handler)
True

-- 
Gabriel Genellina

------------ próxima parte ------------
_______________________________________________
Lista de correo Python-es 
http://listas.aditel.org/listinfo/python-es
FAQ: http://listas.aditel.org/faqpyes


Más información sobre la lista de distribución Python-es