Create/imitate 不可变内置类型的可变子类

Create/imitate mutable subclass of immutable built-in type

问题:

我实现了一个具有相当复杂内部行为的 class,它伪装成一个 int 类型用于所有意图和目的。然后,作为最重要的樱桃,我真的希望我的 class 成功通过 isinstance() 和 issubclass() 检查 int。到目前为止我失败了。

这是我用来测试概念的一个小演示 class。我尝试从 objectint 继承它,虽然从 int 继承它使它通过了检查,但它也破坏了它的一些行为:

#class DemoClass(int):
class DemoClass(object):
    _value = 0
    def __init__(self, value = 0):
        print 'init() called'
        self._value = value
    def __int__(self):
        print 'int() called'
        return self._value + 2
    def __index__(self):
        print 'index() called'
        return self._value + 2
    def __str__(self):
        print 'str() called'
        return str(self._value + 2)
    def __repr__(self):
        print 'repr() called'
        return '%s(%d)' % (type(self).__name__, self._value)
    # overrides for other magic methods skipped as irrelevant

a = DemoClass(3)

print a         # uses __str__() in both cases
print int(a)    # uses __int__() in both cases
print '%d' % a  # __int__() is only called when inheriting from object

rng = range(10)
print rng[a]    # __index__() is only called when inheriting from object

print isinstance(a, int)
print issubclass(DemoClass, int)

本质上,从不可变的 class 继承会导致不可变的 class,并且 Python 将经常使用基础 class 原始值而不是我精心设计的魔法方法。不好。

我看过抽象基 classes,但他们似乎在做完全相反的事情:而不是让我的 class 看起来像一个内置的子class-在类型上,他们让一个 class 假装是一个超级 class 到一个。

使用 __new__(cls, ...) 似乎也不是解决方案。如果您只想在实际创建对象之前修改对象起始值,那很好,但我想规避 不变性诅咒。尝试使用 object.__new__() 也没有结果,因为 Python 只是抱怨说使用 object.__new__ 来创建一个 int 对象是不安全的。

尝试从 (int, dict) 继承我的 class 并使用 dict.__new__() 也不是很成功,因为 Python 显然不允许将它们组合成一个 class.

怀疑 可以使用 metaclasses 找到解决方案,但到目前为止也没有成功,主要是因为我的大脑根本就没有'不够弯曲以正确理解它们。我还在努力,但看起来我不会很快得到结果。

所以,问题是:即使我的 class 非常可变,是否有可能从不可变类型继承或模仿继承? Class 只要找到解决方案(假设它存在),继承结构对我来说并不重要。

这里的问题不是不变性,而是简单的继承。如果 DemoClass 是 int 的子类,则为每个 DemoClass 类型的对象构造一个真正的 int 并将直接使用而无需调用 __int__ 任何可以使用 int 的地方,试试 a + 2.

我宁愿在这里尝试简单地作弊 isinstance。我只想让 DemoClass 成为 object 的子类,并 隐藏 自定义函数后面的内置 isinstance

class DemoClass(object):
    ...

def isinstance(obj, cls):
    if __builtins__.isinstance(obj, DemoClass) and issubclass(int, cls):
        return True
    else:
        return __builtins__.isinstance(obj, cls)

然后我可以做:

>>> a = DemoClass(3)
init() called
>>> isinstance("abc", str)
True
>>> isinstance(a, DemoClass)
True
>>> isinstance(a, int)
True
>>> issubclass(DemoClass, int)
False

所以,如果我理解正确的话,你有:

def i_want_int(int_):
    # can't read the code; it uses isinstance(int_, int)

你想调用 i_want_int(DemoClass()),其中 DemoClass 可以通过 __int__ 方法转换为 int

如果您想继承 int,实例的值在创建时确定。

如果你不想在任何地方都写到 int 的转换(比如 i_want_int(int(DemoClass()))),我能想到的最简单的方法是为 i_want_int 定义包装器,进行转换:

def i_want_something_intlike(intlike):
    return i_want_int(int(intlike))

到目前为止,还没有提出替代解决方案,所以这是我最后使用的解决方案(大致基于 Serge Ballesta 的回答):

def forge_inheritances(disguise_heir = {}, disguise_type = {}, disguise_tree = {},
                       isinstance = None, issubclass = None, type = None):
    """
    Monkey patch isinstance(), issubclass() and type() built-in functions to create fake inheritances.

    :param disguise_heir: dict of desired subclass:superclass pairs; type(subclass()) will return subclass
    :param disguise_type: dict of desired subclass:superclass pairs, type(subclass()) will return superclass
    :param disguise_tree: dict of desired subclass:superclass pairs, type(subclass()) will return superclass for subclass and all it's heirs
    :param isinstance: optional callable parameter, if provided it will be used instead of __builtins__.isinstance as Python real isinstance() function.
    :param issubclass: optional callable parameter, if provided it will be used instead of __builtins__.issubclass as Python real issubclass() function.
    :param type: optional callable parameter, if provided it will be used instead of __builtins__.type as Python real type() function.
    """

    if not(disguise_heir or disguise_type or disguise_tree): return

    import __builtin__
    from itertools import chain

    python_isinstance = __builtin__.isinstance if isinstance is None else isinstance
    python_issubclass = __builtin__.issubclass if issubclass is None else issubclass
    python_type       = __builtin__.type if type is None else type

    def disguised_isinstance(obj, cls, honest = False):
        if cls == disguised_type: cls = python_type
        if honest:
            if python_isinstance.__name__ == 'disguised_isinstance':
                return python_isinstance(obj, cls, True)
            return python_isinstance(obj, cls)
        if python_type(cls) == tuple:
            return any(map(lambda subcls: disguised_isinstance(obj, subcls), cls))
        for subclass, superclass in chain(disguise_heir.iteritems(),
                                          disguise_type.iteritems(),
                                          disguise_tree.iteritems()):
            if python_isinstance(obj, subclass) and python_issubclass(superclass, cls):
                return True
        return python_isinstance(obj, cls)
    __builtin__.isinstance = disguised_isinstance

    def disguised_issubclass(qcls, cls, honest = False):
        if cls == disguised_type: cls = python_type
        if honest:
            if python_issubclass.__name__ == 'disguised_issubclass':
                return python_issubclass(qcls, cls, True)
            return python_issubclass(qcls, cls)
        if python_type(cls) == tuple:
            return any(map(lambda subcls: disguised_issubclass(qcls, subcls), cls))
        for subclass, superclass in chain(disguise_heir.iteritems(),
                                          disguise_type.iteritems(),
                                          disguise_tree.iteritems()):
            if python_issubclass(qcls, subclass) and python_issubclass(superclass, cls):
                return True
        return python_issubclass(qcls, cls)
    __builtin__.issubclass = disguised_issubclass

    if not(disguise_type or disguise_tree): return # No need to patch type() if these are empty

    def disguised_type(obj, honest = False, extra = None):
        if (extra is not None):
            # this is a call to create a type instance, we must not touch it
            return python_type(obj, honest, extra)
        if honest:
            if python_type.__name__ == 'disguised_type':
                return python_type(obj, True)
            return python_type(obj)
        for subclass, superclass in disguise_type.iteritems():
            if obj == subclass:
                return superclass
        for subclass, superclass in disguise_tree.iteritems():
            if python_isinstance(obj, subclass):
                return superclass
        return python_type(obj)
    __builtin__.type       = disguised_type

if __name__ == '__main__':
    class A(object): pass
    class B(object): pass
    class C(object): pass

    forge_inheritances(disguise_type = { C: B, B: A })

    print issubclass(B, A) # prints True
    print issubclass(C, B) # prints True
    print issubclass(C, A) # prints False - cannot link two fake inheritances without stacking

可以通过向 isinstance()issubclass()type() 调用提供可选的 honest 参数来忽略伪造的继承。

使用示例。

让 class B 成为 class 的假继承人 A:

class A(object): pass
class B(object): pass
forge_inheritances(disguise_heir = { B: A })
b = B()
print isinstance(b, A) # prints True
print isinstance(b, A, honest = True) # prints False

让classB假装成为classA:

class A(object): pass
class B(object): pass
forge_inheritances(disguise_type = { B: A})
b = B()
print type(b) # prints "<class '__main__.A'>"
print type(b, honest = True) # prints "<class '__main__.B'>"

让classB继承人伪装成classA:

class A(object): pass
class B(object): pass
class D(B): pass
forge_inheritances(disguise_tree = { B: A})
d = D()
print type(d) # prints "<class '__main__.A'>"

多层假继承可以通过堆叠调用来实现 forge_inheritances():

class A(object): pass
class B(object): pass
class C(object): pass
forge_inheritance(disguise_heir = { B: A})
forge_inheritance(disguise_heir = { C: B})
c = C()
print isinstance(c, A) # prints True

显然,这个 hack 不会以任何方式影响 super() 调用和 attribute/method 继承,这里的主要目的只是欺骗 isinstance()type(inst) == class 签入无法直接修复它们的情况。