Как динамически изменить базовый класс экземпляров во время выполнения?



в этой статье имеет фрагмент, показывающий использование __bases__ динамически изменять иерархию наследования некоторого кода Python, добавляя класс к существующей коллекции классов классов, от которых он наследует. Хорошо, это трудно читать, код, вероятно, яснее:



class Friendly:
def hello(self):
print 'Hello'

class Person: pass

p = Person()
Person.__bases__ = (Friendly,)
p.hello() # prints "Hello"


то есть Person не наследует от Friendly на исходном уровне, но это отношение наследования добавляется динамически во время выполнения путем модификации из класса человек. Однако, если вы измените Friendly и Person новые классы стилей (путем наследования от объекта), вы получите следующее сообщение об ошибке:



TypeError: __bases__ assignment: 'Friendly' deallocator differs from 'object'


немного погуглить на это, кажется,указать на некоторые несовместимости между классами new-style и old style в отношении изменения иерархии наследования во время выполнения. В частности: " объекты класса нового стиля не поддерживают присвоение их баз атрибут".



мой вопрос, Можно ли сделать приведенный выше пример дружественного / человека, используя классы нового стиля в Python 2.7+, возможно, с помощью ?



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

719   6  

6 ответов:

хорошо, опять же, это не то, что вы обычно должны делать, это только для информационных целей.

где Python ищет метод на объекте экземпляра определяется __mro__ атрибут класса, который определяет этот объект (M ethod R e, решение O атрибут rder). Таким образом, если бы мы могли изменить __mro__ на Person, мы получим желаемое поведение. Что-то вроде:

setattr(Person, '__mro__', (Person, Friendly, object))

в проблема в том, что __mro__ является атрибутом только для чтения, и поэтому setattr не будет работать. Может быть, если вы гуру Python, есть способ обойти это, но, очевидно, я не достигаю статуса Гуру, поскольку я не могу думать об этом.

возможным обходным путем является простое переопределение класса:

def modify_Person_to_be_friendly():
    # so that we're modifying the global identifier 'Person'
    global Person

    # now just redefine the class using type(), specifying that the new
    # class should inherit from Friendly and have all attributes from
    # our old Person class
    Person = type('Person', (Friendly,), dict(Person.__dict__)) 

def main():
    modify_Person_to_be_friendly()
    p = Person()
    p.hello()  # works!

то, что это не делает, это изменить любой ранее созданный Person экземпляров, чтобы иметь hello() метод. Например (просто изменение main()):

def main():
    oldperson = Person()
    ModifyPersonToBeFriendly()
    p = Person()
    p.hello()  
    # works!  But:
    oldperson.hello()
    # does not

если детали type вызов не ясно, а затем читать e-satis' отличный ответ на 'что такое метакласс в Python?- .

Я тоже боролся с этим, и был заинтригован вашим решением, но Python 3 забирает его у нас:

AttributeError: attribute '__dict__' of 'type' objects is not writable

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

обсуждение на этой странице и другие подобные ему предоставили намеки на то, что проблема присвоения __bases__ происходит только для классов без определенного суперкласса (т. е. чей единственный суперкласс является объектом). Я смог решить эту проблему (как для Python 2.7, так и для 3.2), определив классы, суперкласс которых мне нужно было заменить, как подклассы тривиального класса:

## T is used so that the other classes are not direct subclasses of object,
## since classes whose base is object don't allow assignment to their __bases__ attribute.

class T: pass

class A(T):
    def __init__(self):
        print('Creating instance of {}'.format(self.__class__.__name__))

## ordinary inheritance
class B(A): pass

## dynamically specified inheritance
class C(T): pass

A()                 # -> Creating instance of A
B()                 # -> Creating instance of B
C.__bases__ = (A,)
C()                 # -> Creating instance of C

## attempt at dynamically specified inheritance starting with a direct subclass
## of object doesn't work
class D: pass

D.__bases__ = (A,)
D()

## Result is:
##     TypeError: __bases__ assignment: 'A' deallocator differs from 'object'

Я не могу ручаться за последствия, но этот код делает то, что вы хотите на py2.7.2.

class Friendly(object):
    def hello(self):
        print 'Hello'

class Person(object): pass

# we can't change the original classes, so we replace them
class newFriendly: pass
newFriendly.__dict__ = dict(Friendly.__dict__)
Friendly = newFriendly
class newPerson: pass
newPerson.__dict__ = dict(Person.__dict__)
Person = newPerson

p = Person()
Person.__bases__ = (Friendly,)
p.hello()  # prints "Hello"

мы знаем, что это возможно. Прохладный. Но мы никогда им не воспользуемся!

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

но если это должно быть сделано, то, по-видимому, есть хак, который получает вокруг "deallocator differs from 'object" issue when modifying the __bases__ attribute для новых классов стилей.

вы можете определить объект класса

class Object(object): pass

который выводит класс из встроенного метакласса type. Вот и все, теперь ваши новые классы стилей можно изменить __bases__ без каких-либо проблем.

в моих тестах это на самом деле работал очень хорошо, так как все существующие (до изменения наследования) экземпляры его и его производных классов почувствовали эффект изменения, включая их mro получать обновления.

мне нужно решение для этого, которое:

  • работает как с Python 2 (>=2.7), так и с Python 3 (>=3.2).
  • позволяет изменять базы классов после динамического импорта зависимости.
  • позволяет изменять базы классов из кода модульного теста.
  • работает с типами, которые имеют собственный метакласс.
  • позволяет unittest.mock.patch для работы, как ожидалось.

вот что я придумал с:

def ensure_class_bases_begin_with(namespace, class_name, base_class):
    """ Ensure the named class's bases start with the base class.

        :param namespace: The namespace containing the class name.
        :param class_name: The name of the class to alter.
        :param base_class: The type to be the first base class for the
            newly created type.
        :return: ``None``.

        Call this function after ensuring `base_class` is
        available, before using the class named by `class_name`.

        """
    existing_class = namespace[class_name]
    assert isinstance(existing_class, type)

    bases = list(existing_class.__bases__)
    if base_class is bases[0]:
        # Already bound to a type with the right bases.
        return
    bases.insert(0, base_class)

    new_class_namespace = existing_class.__dict__.copy()
    # Type creation will assign the correct ‘__dict__’ attribute.
    del new_class_namespace['__dict__']

    metaclass = existing_class.__metaclass__
    new_class = metaclass(class_name, tuple(bases), new_class_namespace)

    namespace[class_name] = new_class

используется как это в приложении:

# foo.py

# Type `Bar` is not available at first, so can't inherit from it yet.
class Foo(object):
    __metaclass__ = type

    def __init__(self):
        self.frob = "spam"

    def __unicode__(self): return "Foo"

# … later …
import bar
ensure_class_bases_begin_with(
        namespace=globals(),
        class_name=str('Foo'),   # `str` type differs on Python 2 vs. 3.
        base_class=bar.Bar)

используйте это из кода модульного теста:

# test_foo.py

""" Unit test for `foo` module. """

import unittest
import mock

import foo
import bar

ensure_class_bases_begin_with(
        namespace=foo.__dict__,
        class_name=str('Foo'),   # `str` type differs on Python 2 vs. 3.
        base_class=bar.Bar)


class Foo_TestCase(unittest.TestCase):
    """ Test cases for `Foo` class. """

    def setUp(self):
        patcher_unicode = mock.patch.object(
                foo.Foo, '__unicode__')
        patcher_unicode.start()
        self.addCleanup(patcher_unicode.stop)

        self.test_instance = foo.Foo()

        patcher_frob = mock.patch.object(
                self.test_instance, 'frob')
        patcher_frob.start()
        self.addCleanup(patcher_frob.stop)

    def test_instantiate(self):
        """ Should create an instance of `Foo`. """
        instance = foo.Foo()

приведенные выше ответы хороши, если вам нужно изменить существующий класс во время выполнения. Однако, если вы просто хотите создать новый класс, который наследует другой класс, там намного чище решение. Я получил эту идею от https://stackoverflow.com/a/21060094/3533440, но я думаю, что приведенный ниже пример лучше иллюстрирует законный случай использования.

def make_default(Map, default_default=None):
    """Returns a class which behaves identically to the given
    Map class, except it gives a default value for unknown keys."""
    class DefaultMap(Map):
        def __init__(self, default=default_default, **kwargs):
            self._default = default
            super().__init__(**kwargs)

        def __missing__(self, key):
            return self._default

    return DefaultMap

DefaultDict = make_default(dict, default_default='wug')

d = DefaultDict(a=1, b=2)
assert d['a'] is 1
assert d['b'] is 2
assert d['c'] is 'wug'

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

Comments

    Ничего не найдено.