web-dev-qa-db-ja.com

実行時にインスタンスの基本クラスを動的に変更する方法は?

この記事 の既存のクラスコレクションにクラスを追加することにより、いくつかのPythonコードの継承階層を動的に変更する__bases__の使用法を示すスニペットがあります。継承元のクラスわかりました、それは読みにくいです、コードはおそらくより明確です:

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

class Person: pass

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

つまり、PersonはソースレベルでFriendlyを継承しませんが、この継承関係はPersonクラスの__bases__attributeの変更により実行時に動的に追加されます。ただし、FriendlyPersonを(オブジェクトから継承して)新しいスタイルクラスに変更すると、次のエラーが発生します。

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

これに関する少しのグーグルは、実行時の継承階層の変更に関して、新しいスタイルのクラスと古いスタイルのクラスの間で いくつかの非互換性を示す のようです。具体的には: 「新しいスタイルのクラスオブジェクトは、bases属性」への割り当てをサポートしていません

私の質問は、おそらく__mro__属性を使用して、Python 2.7+)の新しいスタイルのクラスを使用して、上記のFriendly/Personサンプルを動作させることは可能ですか?

免責事項:これは不明瞭なコードであることを完全に認識しています。実際の製品コードでは、このようなトリックは読めないことに境界を置く傾向があることを完全に理解しています。これは純粋に思考実験であり、ファンジーがPythonが多重継承に関連する問題をどのように扱うかについて何かを学ぶために

67
Adam Parkin

繰り返しますが、これは通常行うべきことではなく、情報提供のみを目的としています。

Pythonインスタンスオブジェクトのメソッドを探すには、そのオブジェクトを定義するクラスの___mro___属性によって決定されます([〜# 〜] m [〜#〜]ethod[〜#〜] r [〜#〜]esolution[〜#〜] o [〜#〜]rder属性)。したがって、Personの___mro___を変更できる場合、希望の動作が得られます。

_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呼び出しの詳細が明確でない場合は、「Pythonのメタクラスとは何か」に関する e-satisの優れた答え をお読みください。

33
Adam Parkin

私もこれに苦労しており、あなたのソリューションに興味をそそられましたが、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'
21
Mitchell Model

私は結果を保証することはできませんが、このコードは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"

これが可能であることを知っています。クール。しかし、決して使用しません!

6
akaRem

すぐに、クラス階層を動的にいじることのすべての警告が有効になります。

しかし、それを行う必要がある場合、明らかに、新しいスタイルクラスの"deallocator differs from 'object" issue when modifying the __bases__ attributeを回避するハックがあります。

クラスオブジェクトを定義できます

class Object(object): pass

組み込みメタクラスtypeからクラスを派生します。これで、新しいスタイルクラスで__bases__を問題なく変更できるようになりました。

私のテストでは、これは実際にすべての既存の(継承を変更する前の)インスタンスとその派生クラスが、mroの更新を含む変更の影響を感じたので非常にうまく機能しました。

2
Sam Gulve

このためのソリューションが必要でした:

  • 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()
1
bignose

上記の回答は、実行時に既存のクラスを変更する必要がある場合に適しています。ただし、他のクラスによって継承される新しいクラスを作成するだけの場合は、よりクリーンなソリューションがあります。 https://stackoverflow.com/a/21060094/353344 からこのアイデアを得ましたが、以下の例は正当なユースケースをよりよく示していると思います。

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のファンクターに非常に似ています。

0
fredcallaway