Subclassing builtin types in Python 2 and Python 3
This isn't a general change for built-in types when moving from Python 2.x to 3.x -- list
and int
, for example, have the same behaviour in 2.x and 3.x. Only the set type was changed to bring it in line with the other types, as discussed in this bug tracker issue.
I'm afraid there is no really nice way to make it behave the old way. Here is some code I was able to come up with:
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
This will simply overwrite all methods with versions that return your own type. (There is a whole lot of methods!)
Maybe for your application, you can get away with overwriting copy()
and stick to the in-place methods.
Perhaps a metaclass to do all that humdrum wrapping for you would make it easier:
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 wrapperclass MySet(set, metaclass=Perpetuate): passs1 = 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)))
As a follow-up to Sven's answer, here is a universal wrapping solution that takes care of all non-special methods. The idea is to catch the first lookup coming from a method call, and install a wrapper method that does the type conversion. At subsequent lookups, the wrapper is returned directly.
Caveats:
1) This is more magic trickery than I like to have in my code.
2) I'd still need to wrap special methods (__and__
etc.) manually because their lookup bypasses __getattribute__
import typesclass 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