【问题标题】:Subclassing builtin types in Python 2 and Python 3在 Python 2 和 Python 3 中子类化内置类型
【发布时间】:2011-11-02 15:48:00
【问题描述】:

在对内置类型进行子类化时,我注意到 Python 2 和 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)))

在 Python 2 中,所有返回值的类型都是 MySet。对于 Python 3,返回类型为 set。我找不到任何关于结果应该是什么的文档,也找不到任何关于 Python 3 更改的文档。

无论如何,我真正关心的是:在 Python 3 中是否有一种简单的方法来获得在 Python 2 中看到的行为,而无需重新定义内置类型的每一个方法?

【问题讨论】:

  • 在 Python 2 上,只有 s1 的类型与 s2 的类型无关。
  • 这有点类似于False + False0,而不是False(顺便说一下,boolint 的子类)。

标签: python python-3.x subclass built-in-types


【解决方案1】:

当从 Python 2.x 迁移到 3.x 时,这并不是内置类型的一般变化——例如,listint 在 2.x 和 3 中具有相同的行为。 X。仅更改了 set 类型以使其与其他类型保持一致,如 this bug tracker issue 中所述。

恐怕没有真正好的方法可以让它以旧方式运行。这是我能够想出的一些代码:

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

这将简单地用返回您自己类型的版本覆盖所有方法。 (有很多方法!)

也许对于您的应用程序,您可以避免覆盖 copy() 并坚持使用就地方法。

【讨论】:

  • 对,Python 2 在这里并不一致。如果您在 Python 2 中创建 class MySet(set): pass,则 print type(MySet().copy()) 将提供 <class '__main__.MySet'>,但如果您创建 class MyDict(dict): pass,则 print type(MyDict().copy()) 将提供 <type 'dict'>
  • 有一种方法可以在单个操作中至少处理非特殊方法。我将回答我自己的问题以说明如何(我不能将代码放入评论中)。但它仍然是我想要的更多开销,所有特殊方法都可以一一处理。
【解决方案2】:

也许为您完成所有单调包装的元类会更容易:

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)))

【讨论】:

  • 一些 cmets: 1. 这将无法包装一个名为 e() 的方法,但它确实包装了 __getattribute__(),从而防止将基本类型的对象存储在属性中。 2. 这将严重影响性能,尤其是在检索属性时。如果您将列表存储在属性中,则每次访问都会对其进行迭代。还有更多的性能问题,可能太多了,无法指出。
  • @SvenMarnach:为什么无法包装e()
  • 因为对于名称e,条件attr in ('__new__') 将成立。诚然,这很便宜,但这段代码中还有更多不为人知的错误。
  • @SvenMarnach:啊——不是没有逗号的元组。谢谢。
【解决方案3】:

作为 Sven 回答的后续,这里有一个通用的包装解决方案,可以处理所有非特殊方法。这个想法是捕获来自方法调用的第一个查找,并安装一个执行类型转换的包装器方法。在后续查找时,直接返回包装器。

注意事项:

1) 这比我喜欢在我的代码中包含更多的魔术。

2) 我仍然需要手动包装特殊方法(__and__ 等),因为它们的查找绕过了 __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

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2019-12-27
    • 2017-06-07
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2016-02-07
    • 2015-10-01
    相关资源
    最近更新 更多