Создать/имитировать изменяемый подкласс неизменяемого встроенного типа

Проблема:

Я реализовал класс с довольно сложным внутренним поведением, который притворяется типом int во всех смыслах и целях. Затем, в качестве вишенки на торте, я действительно хотел, чтобы мой класс успешно прошел проверки isinstance() и issubclass() для int. Я потерпел неудачу до сих пор.

Вот небольшой демонстрационный класс, который я использую для проверки концепции. Я пытался наследовать его как от object, так и от int, и, хотя он наследуется от 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)

По сути, наследование от неизменяемого класса приводит к неизменяемому классу, и Python часто будет использовать необработанное значение базового класса вместо моих тщательно разработанных магических методов. Нехорошо.

Я смотрел на абстрактные базовые классы, но они, кажется, делают что-то совершенно противоположное: вместо того, чтобы сделать мой класс похожим на подкласс встроенного типа, они заставляют класс притворяться надклассом по отношению к нему.

Использование __new__(cls, ...) тоже не похоже на решение. Хорошо, если все, что вам нужно, это изменить начальное значение объекта перед его фактическим созданием, но я хочу уклониться от проклятия неизменности. Попытка использовать object.__new__() тоже не принесла результатов, так как Python просто пожаловался, что использовать object.__new__ для создания объекта int небезопасно.

Попытка унаследовать мой класс от (int, dict) и использовать dict.__new__() также не увенчалась успехом, так как Python явно не позволяет объединить их в один класс.

Я подозреваю, что решение может быть найдено с помощью метаклассов, но с ними пока тоже не получилось, в основном потому, что мои мозги просто недостаточно согнуты, чтобы правильно их понять. Я все еще пытаюсь, но не похоже, что я скоро получу результаты.

Итак, вопрос: возможно ли вообще наследовать или имитировать наследование от неизменяемого типа, даже если мой класс очень изменчив? Структура наследования классов для меня не имеет большого значения, пока найдено решение (при условии, что оно вообще существует).


person Lav    schedule 10.12.2015    source источник
comment
Что вы пытаетесь достичь? Зачем вам нужен атрибут _value, когда экземпляры ваших классов являются сами int?   -  person GingerPlusPlus    schedule 10.12.2015
comment
@GingerPlusPlus, это пример класса, а не настоящий, который я использую. И я пытаюсь добиться дополнительной совместимости, конечно. Я имею дело со сторонним кодом, который я не могу изменить или даже увидеть, и я хочу передать ему некоторые данные, где некоторые значения int заменены экземплярами моего класса псевдо-int, который необходим для получения некоторых интересных блестящих функций. И было бы очень хорошо, если бы по крайней мере isinstance() проверки были успешными, если они происходят в стороннем коде, так как это уменьшает количество изменений, которые я должен попросить сделать других ребят.   -  person Lav    schedule 10.12.2015
comment
тогда вам не нужно _value, потому что isinstance(self, int). Вместо того, чтобы делать self._value + anything, вы можете просто сделать super().__add__(anything).   -  person GingerPlusPlus    schedule 10.12.2015
comment
@GingerPlusPlus, это пример класса. Настоящий класс на самом деле использует dict, который преобразуется в целочисленное значение с помощью ряда динамически изменяющихся правил, определенных во внешнем одноэлементном классе — вы действительно хотите видеть все это в тексте вопроса? Я предоставил минимальный пример, а _value + 2 предназначен только для динамической оценки.   -  person Lav    schedule 10.12.2015


Ответы (3)


Проблема здесь не в неизменности, а просто в наследовании. Если DemoClass является подклассом int, истинный int создается для каждого объекта типа DemoClass и будет использоваться напрямую без вызова __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
person Serge Ballesta    schedule 10.12.2015
comment
Ой. Я не думал заменять isinstance во встроенных, но это определенно работает! Очень хакерское решение, но такова ситуация, и я все равно уже заменяю __builtins__.__import__, так что спасибо! Подождет немного, прежде чем принять - может быть, будет предоставлен менее хакерский ответ. - person Lav; 10.12.2015

Итак, если я правильно понял, у вас есть:

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))
person GingerPlusPlus    schedule 10.12.2015
comment
Ситуация немного сложнее, к сожалению. Я не вызываю функции, я импортирую модули с кодом на уровне модуля. - person Lav; 11.12.2015
comment
@Lav: Модули, похоже, имеют непитоновский дизайн, я бы не стал их использовать. - person GingerPlusPlus; 11.12.2015
comment
Может быть, но, боюсь, в моей ситуации это не вариант. Кроме того, очистка ввода функции мало помогает, когда функция может извлекать некоторые внешние данные во время своего выполнения или вызывать мои функции... Просто слишком много вариантов, поэтому любое решение, которое не является универсальным или встроенным хаком, просто не полетит. - person Lav; 11.12.2015

До сих пор не было предложено никаких альтернативных решений, поэтому вот решение, которое я использую в конце (во многом основанное на ответе Сержа Бальесты):

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

Можно игнорировать фальшивое наследование, указав необязательный параметр honest для вызовов isinstance(), issubclass() и type().

Примеры использования.

Сделать класс B фальшивым наследником класса 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

Заставьте класс B притворяться быть классом A:

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'>"

Заставьте класс B и всех его наследников притворяться классом A:

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

Очевидно, что этот хак никак не повлияет на вызовы super() и наследование атрибутов/методов, основная цель здесь — просто обмануть проверки isinstance() и type(inst) == class в ситуации, когда у вас нет возможности исправить их напрямую.

person Lav    schedule 18.12.2015