2011-11-02 13 views
22

Al crear subclases de tipos incorporados, noté una diferencia bastante importante entre Python 2 y Python 3 en el tipo de retorno de los métodos de los tipos incorporados. El código siguiente ilustra esto para conjuntos:Subclassing builtin types en Python 2 y Python 3

class MySet(set): 

    pass 

s1 = MySet([1, 2, 3, 4, 5]) 

s2 = MySet([1, 2, 3, 6, 7]) 

print(type(s1.union(s2))) 

print(type(s1.intersection(s2))) 

print(type(s1.difference(s2))) 

Con Python 2, todos los valores de retorno son de tipo MySet. Con Python 3, los tipos de devolución son set. No pude encontrar ninguna documentación sobre el resultado, ni documentación sobre el cambio en Python 3.

De todos modos, lo que realmente me importa es esto: ¿hay una manera simple en Python 3 para obtener el comportamiento visto en Python 2, sin redefinir todos los métodos de los tipos incorporados?

+0

En Python 2 sólo el tipo de 's1' es relevante no es el tipo de s2 '. – agf

+2

Es similar a la forma en que 'False + False' es' 0', no 'False' (' bool' es una subclase de 'int', por cierto). –

Respuesta

11

Esto no es un cambio general para los tipos incorporados al pasar de Python 2.xa 3.x - list y int, por ejemplo, tienen el mismo comportamiento en 2.x y 3.x. Solo se cambió el tipo de conjunto para alinearlo con los otros tipos, como se explica en this bug tracker issue.

Me temo que no hay una forma realmente agradable de hacerlo funcionar a la vieja usanza. Aquí hay un código que era capaz de llegar a:

class MySet(set): 
    def copy(self): 
     return MySet(self) 
    def _make_binary_op(in_place_method): 
     def bin_op(self, other): 
      new = self.copy() 
      in_place_method(new, other) 
      return new 
     return bin_op 
    __rand__ = __and__ = _make_binary_op(set.__iand__) 
    intersection = _make_binary_op(set.intersection_update) 
    __ror__ = __or__ = _make_binary_op(set.__ior__) 
    union = _make_binary_op(set.update) 
    __sub__ = _make_binary_op(set.__isub__) 
    difference = _make_binary_op(set.difference_update) 
    __rxor__ = xor__ = _make_binary_op(set.__ixor__) 
    symmetric_difference = _make_binary_op(set.symmetric_difference_update) 
    del _make_binary_op 
    def __rsub__(self, other): 
     new = MySet(other) 
     new -= self 
     return new 

Esto simplemente sobrescribir todos los métodos con las versiones que devuelven su propio tipo. (¡Hay un montón de métodos!)

Quizás para su aplicación, puede salirse con la sobreescritura copy() y apegarse a los métodos in situ.

+0

Derecha, Python 2 no fue consistente aquí. Si crea una 'clase MySet (set): pass' en Python 2, entonces' print type (MySet(). Copy()) 'da' ', pero si crea una' clase MyDict (dict): pase', luego 'tipo de impresión (MyDict(). Copy())' da ''. – Cito

+0

Hay una manera de manejar al menos los métodos no especiales en una sola operación. Responderé a mi propia pregunta para ilustrar cómo (no puedo poner el código en un comentario). Pero todavía me quedan muchos gastos adicionales, con todos los métodos especiales para manejar uno por uno. – khinsen

0

Tal vez una metaclase hacer todo lo que envuelve monótona para que le sea más fácil:

class Perpetuate(type): 
    def __new__(metacls, cls_name, cls_bases, cls_dict): 
     if len(cls_bases) > 1: 
      raise TypeError("multiple bases not allowed") 
     result_class = type.__new__(metacls, cls_name, cls_bases, cls_dict) 
     base_class = cls_bases[0] 
     known_attr = set() 
     for attr in cls_dict.keys(): 
      known_attr.add(attr) 
     for attr in base_class.__dict__.keys(): 
      if attr in ('__new__'): 
       continue 
      code = getattr(base_class, attr) 
      if callable(code) and attr not in known_attr: 
       setattr(result_class, attr, metacls._wrap(base_class, code)) 
      elif attr not in known_attr: 
       setattr(result_class, attr, code) 
     return result_class 
    @staticmethod 
    def _wrap(base, code): 
     def wrapper(*args, **kwargs): 
      if args: 
       cls = args[0] 
      result = code(*args, **kwargs) 
      if type(result) == base: 
       return cls.__class__(result) 
      elif isinstance(result, (tuple, list, set)): 
       new_result = [] 
       for partial in result: 
        if type(partial) == base: 
         new_result.append(cls.__class__(partial)) 
        else: 
         new_result.append(partial) 
       result = result.__class__(new_result) 
      elif isinstance(result, dict): 
       for key in result: 
        value = result[key] 
        if type(value) == base: 
         result[key] = cls.__class__(value) 
      return result 
     wrapper.__name__ = code.__name__ 
     wrapper.__doc__ = code.__doc__ 
     return wrapper 

class MySet(set, metaclass=Perpetuate): 
    pass 

s1 = MySet([1, 2, 3, 4, 5]) 

s2 = MySet([1, 2, 3, 6, 7]) 

print(s1.union(s2)) 
print(type(s1.union(s2))) 

print(s1.intersection(s2)) 
print(type(s1.intersection(s2))) 

print(s1.difference(s2)) 
print(type(s1.difference(s2))) 
+0

Algunos comentarios: 1. Esto no lograría envolver un método llamado 'e()', pero envuelve '__getattribute __()', impidiendo almacenar objetos del tipo base en los atributos. 2. Esto tendrá un impacto de rendimiento severo, especialmente para recuperar atributos.Si almacena una lista en un atributo, se repetirá en cada acceso. Hay más problemas de rendimiento, tal vez demasiados para señalar. –

+0

@SvenMarnach: ¿Por qué no se puede cerrar 'e()'? –

+0

Porque para el nombre 'e', la condición' attr in ('__new __') 'se mantendrá. Es cierto que es un juego barato, pero hay más errores ocultos en este código. –

0

Como seguimiento a la respuesta de Sven, aquí es una solución de embalaje universal que se encarga de todo no especiales métodos. La idea es captar la primera búsqueda proveniente de una llamada a método e instalar un método contenedor que haga la conversión de tipo. En las búsquedas posteriores, el contenedor se devuelve directamente.

Advertencias:

1) Este es el engaño más magia que me gustaría tener en mi código.

2) Todavía había necesidad de envolver métodos especiales (__and__ etc.) de forma manual debido a que su operaciones de búsqueda no pasa por __getattribute__

import types 

class MySet(set): 

    def __getattribute__(self, name): 
     attr = super(MySet, self).__getattribute__(name) 
     if isinstance(attr, types.BuiltinMethodType): 
      def wrapper(self, *args, **kwargs): 
       result = attr(self, *args, **kwargs) 
       if isinstance(result, set): 
        return MySet(result) 
       else: 
        return result 
      setattr(MySet, name, wrapper) 
      return wrapper 
     return attr