2012-03-02 2 views
55

This article имеет фрагмент, показывающий использование __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' 

Немного погуглить на это, кажется, indicate some incompatibilities между новым стилем и старыми классами стилей в отношении изменения иерархии наследования во время выполнения. В частности: "New-style class objects don't support assignment to their bases attribute".

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

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

+0

Это также хорошо для учащихся, чтобы прочитать это, если они не знакомы с метаклассом, type(), ...: http://www.slideshare.net/gwiener/metaclasses-in-python :) – loolooyyyy

+0

Вот мое использование дело. Я импортирую библиотеку, у которой класс B наследуется от класса A. – FutureNerd

+2

Вот мой фактический прецедент. Я импортирую библиотеку, у которой класс B наследуется от класса A. Я хочу создать наследование new_A из A с помощью new_A_method(). Теперь я хочу создать наследование new_B из ... ну, из B, как если бы B унаследовал от New_A, так что методы B, методы A и new_A_method() все доступны экземплярам New_B. Как я могу сделать это без обезглавливания существующего класса A? – FutureNerd

ответ

29

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

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

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

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

Возможным решением является просто переопределить класс:

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' excellent answer on 'What is a metaclass in Python?'.

+0

-1: зачем заставить новый класс наследовать от Frielndly только тогда, когда вы могли бы так же хорошо сохранить оригинал '__mro__', вызывая:' type ('Person', (Friendly) + Person .__ mro__, dict (Person .__ dict__)) '(и еще лучше добавить меры безопасности, чтобы Friendly не заканчивался дважды там). здесь есть и другие проблемы - что касается того, где фактически определен и используется класс «Личность»: ваша функция только меняет его на текущий модуль - другие модули, работающие с Person, будут неактуальны - вам лучше выполнить monkeypatch в модуле Person is определены. (и даже там есть проблемы) – jsbueno

+6

'-1' Вы полностью пропустили причину исключения в первую очередь. Вы можете с радостью изменить 'Person .__ class __.__ bases__' в Python 2 и 3, если' Person' не наследует непосредственно от 'object' ** ** **. См. Ответы akaRem и Sam Gulve ниже. Это обходное решение работает только вокруг вашего собственного непонимания проблемы. –

7

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

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

13

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

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

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

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

class Object(object): pass 

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

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

1

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

  • работает как с 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

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

+1

Не совсем уверен, что это касается вопроса, поскольку вопрос действительно касался динамически меняющихся базовых классов во время выполнения. В любом случае, в чем преимущество этого, просто наследуя от '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' '' Поскольку он '' 'make_default'', он может вернуть только один тип вещей, поэтому почему бы просто не сделать' '' DefaultMap'' идентификатор верхнего уровня, а не называть '' 'make_default'', чтобы получить класс для создания экземпляра? –

+0

Спасибо за отзыв! (1) Я приземлился здесь, пытаясь сделать динамическое наследование, поэтому решил, что смогу помочь кому-то, кто следовал бы по тому же пути. (2) Я считаю, что можно использовать 'make_default' для создания стандартной версии какого-либо другого типа словарного класса (« Карта »). Вы не можете наследовать от «Карты» напрямую, потому что «Карта» не определена до времени выполнения. В этом случае вы хотели бы наследовать от 'dict' напрямую, но мы предполагаем, что может быть случай, когда вы не знаете, какой класс, подобный словарю, наследуется до времени выполнения. – fredcallaway

Смежные вопросы