Как динамически изменить базовый класс экземпляров во время выполнения? - PullRequest
66 голосов
/ 02 марта 2012

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

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

class Person: pass

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

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

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

Немного Googling на этот счет кажется указать некоторые несовместимости между классами нового и старого стилей в отношении изменения иерархии наследования во время выполнения.В частности: "Объекты класса нового стиля не поддерживают присваивание их атрибуту base " .

Мой вопрос, возможно ли сделать вышеуказанного Friendly / PersonПример работы с использованием классов нового стиля в Python 2.7+, возможно, с использованием атрибута __mro__?

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

Ответы [ 6 ]

33 голосов
/ 03 марта 2012

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

Где Python ищет метод для объекта экземпляра, определяется атрибутом __mro__ класса, который определяет этот объект ( M ethod R esolution 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-sat 'превосходный ответ' Что такое метаклассв Python? '.

20 голосов
/ 09 марта 2012

Я тоже боролся с этим и был заинтригован вашим решением, но 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'
6 голосов
/ 03 марта 2012

Я не могу ручаться за последствия, но этот код делает то, что вы хотите на 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"

Мы знаем, что это возможно.Здорово.Но мы никогда не будем его использовать!

2 голосов
/ 23 апреля 2013

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

Но если это должно быть сделано, то, очевидно, есть хак, который получает около 1003 * для новых классов стилей.

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

class Object(object): pass

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

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

1 голос
/ 21 января 2015

Мне нужно решение для этого:

  • Работает как с 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()
0 голосов
/ 05 января 2016

Приведенные выше ответы хороши, если вам нужно изменить существующий класс во время выполнения.Однако, если вы просто хотите создать новый класс, который наследует какой-то другой класс, есть гораздо более чистое решение.Я получил эту идею от 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.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...