Як динамічно змінювати базовий клас примірників під час виконання?


77

У цій статті є фрагмент, що показує використання __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'

Трохи погуглити, здається, вказує на деякі несумісності між класами нового стилю та старого стилю щодо зміни ієрархії успадкування під час виконання. Зокрема: "Об'єкти класу нового стилю не підтримують присвоєння їх атрибуту бази " .

Моє запитання, чи можна змусити описаний вище приклад Friendly / Person працювати за допомогою класів нового стилю в Python 2.7+, можливо, за допомогою __mro__атрибута?

Застереження: Я повністю усвідомлюю, що це незрозумілий код. Я цілком усвідомлюю, що в реальних виробничих кодах такі трюки, як правило, межують з нечитабельними, це суто продуманий експеримент, і для веселощів дізнатися щось про те, як Python вирішує проблеми, пов'язані з множинним успадкуванням.


Також приємно читати це учням, якщо вони не знайомі з метакласом, типом (), ...: slideshare.net/gwiener/metaclasses-in-python :)
hkoosha

Ось мій варіант використання. Я імпортую бібліотеку, яка має клас 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

Чи не могли б ви New_Bуспадкувати від обох Bі New_A? Пам'ятайте, що Python підтримує множинне успадкування.
Адам Паркін,

2
Трохи погугливши, такий звіт про помилки python видався актуальним ... bugs.python.org/issue672115
mgilson

Відповіді:


39

Гаразд, знову ж таки, це не те, що ви зазвичай повинні робити, це лише для ознайомлення.

Там , де Python шукає метод на об'єкті примірника визначається __mro__атрибутом класу , який визначає , що об'єкт ( М енят R esolution O rder атрибута). Таким чином, якщо ми могли б змінити __mro__OF Person, ми отримаємо бажане поведінку. Щось на зразок:

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

Проблема в тому, що __mro__це атрибут readonly, і, отже, 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-satisfa 'чудова відповідь на тему' Що таке метаклас у Python? ' .


-1: навіщо змушувати новий клас успадковувати від Frielndly лише тоді, коли ви можете так само добре зберегти оригінальний `__mro__`, зателефонувавши: type('Person', (Friendly) + Person.__mro__, dict(Person.__dict__)) (а ще краще, додайте запобіжні заходи, щоб Friendly не закінчувався там двічі.) Є інші проблеми тут - щодо того, де насправді визначено та використовується клас "Person": ваша функція змінює його лише на поточному модулі - інші модулі, що працюють з Person, будуть недосконалими - вам краще виконати мавпочку на модулі визначено Person. (і навіть там є проблеми)
jsbueno

10
-1Ви спочатку повністю пропустили причину винятку. Ви можете із задоволенням модифікувати Person.__class__.__bases__Python 2 та 3, якщо тільки Personне успадковуєте object безпосередньо . Дивіться відповіді akaRem та Sam Gulve нижче. Це обхідне рішення обходить лише ваше власне нерозуміння проблеми.
Карл Сміт,

2
Це дуже глючне "рішення". Серед інших проблем цього коду він порушує __dict__атрибут отриманих об'єктів .
user2357112 підтримує Моніку

24

Я теж боровся з цим, і мене заінтригувало ваше рішення, але 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

Я не можу поручитися за наслідки, але те, що цей код робить те, що ви хочете, на 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"

Ми знаємо, що це можливо. Класно. Але ми ніколи не будемо ним користуватися!


3

Праворуч від кажана діють усі застереження щодо динамічного змішування з ієрархією класів.

Але якщо це потрібно зробити тоді, мабуть, є хак "deallocator differs from 'object" issue when modifying the __bases__ attributeдля нових класів стилів.

Ви можете визначити об’єкт класу

class Object(object): pass

Який отримує клас із вбудованого метакласу type. Ось і все, тепер ваші нові класи стилів можуть змінювати __bases__без проблем.

У моїх тестах це насправді працювало дуже добре, як усі існуючі (до зміни успадкування) його екземпляри та його похідні класи відчували ефект зміни, включаючи їх mroоновлення.


2

Мені потрібне було рішення для цього, яке:

  • Працює як з 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
Не зовсім впевнений, що це пов’язано із запитанням, оскільки насправді йшлося про динамічну зміну базових класів під час виконання. У будь-якому випадку, яка перевага цього в тому, щоб просто успадковувати Mapбезпосередньо та замінювати методи за необхідності (подібно до того, що collections.defaultdictробить стандарт )? У make_defaultтакому вигляді він може повертати лише один тип речей, то чому б просто не зробити DefaultMapідентифікатор верхнього рівня, а не викликати, make_defaultщоб змусити клас створити екземпляр?
Адам Паркін

Thanks for the feedback! (1) I landed here while trying to do dynamic inheritance, so I figured I could help someone who followed my same path. (2) I believe that one could use make_default to create a default version of some other type of dictionary-like class (Map). You can't inherit from Map directly because Map is not defined until runtime. In this case, you would want to inherit from dict directly, but we imagine that there could be a case where you wouldn't know what dictionary-like class to inherit from until runtime.
fredcallaway
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.