web-dev-qa-db-ja.com

定義されたクラスを自動登録する方法

クラスの定義時にクラスのインスタンスを登録したいのですが。理想的には、以下のコードでうまくいくでしょう。

registry = {}

def register( cls ):
   registry[cls.__name__] = cls() #problem here
   return cls

@register
class MyClass( Base ):
   def __init__(self):
      super( MyClass, self ).__init__() 

残念ながら、このコードはエラーNameError: global name 'MyClass' is not definedを生成します。

MyClassをインスタンス化しようとしている#problem here行で起こっていますが、デコレータがまだ返されていないため、存在しません。

メタクラスなどを使ってこれの周りに何かありますか?

39
deft_code

はい、メタクラスはこれを行うことができます。メタクラスの___new___メソッドはクラスを返すので、返す前にそのクラスを登録するだけです。

_class MetaClass(type):
    def __new__(cls, clsname, bases, attrs):
        newclass = super(MetaClass, cls).__new__(cls, clsname, bases, attrs)
        register(newclass)  # here is your register function
        return newclass

class MyClass(object):
    __metaclass__ = MetaClass
_

前の例は、Python 2.xで機能します。Python 3.xでは、MyClassの定義が少し異なります(MetaClassは変更されていないため表示されません-super(MetaClass, cls)super()になる場合があることを除いて):

_#Python 3.x

class MyClass(metaclass=MetaClass):
    pass
_

Python 3.6以降、メタクラスの代わりに使用できる新しい___init_subclass___メソッド( PEP 487 を参照)も使用できます(@matuskoに感謝)以下の彼の答えのために):

_class ParentClass:
    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        register(cls)

class MyClass(ParentClass):
    pass
_

[編集:欠落したcls引数をsuper().__new__()に修正しました]

[編集:追加Python 3.xの例]]

[編集:引数の順序をsuper()に修正し、3.xの違いの説明を改善]

[編集:追加Python 3.6 ___init_subclass___の例]

47
dappawit

python 3.6なので、これを解決するためにメタクラスは必要ありません

python 3.6では、クラス作成のより簡単なカスタマイズが導入されました( PEP 487 )。

__init_subclass__指定されたクラスのすべてのサブクラスを初期化するフック。

提案には次の サブクラス登録 の例が含まれます

class PluginBase:
    subclasses = []

    def __init_subclass__(cls, **kwargs):
        super().__init_subclass__(**kwargs)
        cls.subclasses.append(cls)

この例では、PluginBase.subclassesには、継承ツリー全体のすべてのサブクラスのプレーンリストが含まれます。これはミックスインクラスとしてもうまく機能することに注意してください。

22
matusko

問題の原因は、実際には指定した行ではなく、___init___メソッドのsuper呼び出しです。 dappawitで提案されているようにメタクラスを使用すると、問題は解決しません。その答えの例が機能する理由は、dappawitがBaseクラスを省略してsuper呼び出しを省略し、例を簡略化したことです。次の例では、ClassWithMetaDecoratedClassも機能しません。

_registry = {}
def register(cls):
    registry[cls.__name__] = cls()
    return cls

class MetaClass(type):
    def __new__(cls, clsname, bases, attrs):
        newclass = super(cls, MetaClass).__new__(cls, clsname, bases, attrs)
        register(newclass)  # here is your register function
        return newclass

class Base(object):
    pass


class ClassWithMeta(Base):
    __metaclass__ = MetaClass

    def __init__(self):
        super(ClassWithMeta, self).__init__()


@register
class DecoratedClass(Base):
    def __init__(self):
        super(DecoratedClass, self).__init__()
_

問題はどちらの場合も同じです。 register関数はクラスオブジェクトが作成された後、名前にバインドされる前に(メタクラスによって、またはデコレータとして直接)呼び出されます。これは、superが危険にさらされる場所です(Python 2.x))。これは、super呼び出しでクラスを参照する必要があるためです。グローバル名を使用し、super呼び出しが呼び出されるまでにその名前にバインドされていると信頼することによってのみ合理的に実行されます。この場合、その信頼は誤った場所に置かれます。

ここではメタクラスは間違った解決策だと思います。メタクラスは、カスタム動作が共通するclassesのファミリを作成するためのものであり、クラスがカスタム動作が共通するインスタンスのファミリを作成するためのものです。クラスの関数を呼び出すだけです。文字列の関数を呼び出すクラスを定義したり、クラスの関数を呼び出すメタクラスを定義したりしないでください。

したがって、問題は、(1)クラス作成プロセスでフックを使用してクラスのインスタンスを作成することと、(2)superを使用することの間の基本的な非互換性です。

これを解決する1つの方法は、superを使用しないことです。 superは難しい問題を解決しますが、他の問題を引き起こします(これはそのうちの1つです)。複雑な多重継承スキームを使用している場合、superの問題はsuperを使用しない場合の問題よりも優れており、super次に、superを使用する必要があります。これらの条件のいずれにも該当しない場合は、super呼び出しを直接の基本クラス呼び出しに置き換えるだけで、実際に妥当な解決策になる場合があります。

別の方法は、registerをクラス作成にフックしないことです。各クラス定義の後にregister(MyClass)を追加することは、それらの前に_@register_または___metaclass__ = Registered_(またはメタクラスを呼び出すもの)を追加することとほぼ同じです。ただし、一番下の行は、クラスの一番上にあるNice宣言よりも自己文書化が少ないため、これはあまり気分が良くありませんが、実際には妥当な解決策である可能性があります。

最後に、不愉快なハックに頼ることができますが、おそらく機能します。問題は、名前がモジュールのグローバルスコープで検索されていることです直前そこにバインドされています。したがって、次のようにしてカンニングすることができます。

_def register(cls):
    name = cls.__name__
    force_bound = False
    if '__init__' in cls.__dict__:
        cls.__init__.func_globals[name] = cls
        force_bound = True
    try:
        registry[name] = cls()
    finally:
        if force_bound:
            del cls.__init__.func_globals[name]
    return cls
_

これはどのように機能するかです:

  1. 最初に、___init___が_cls.__dict___内にあるかどうかを確認します(常にtrueになる___init___属性があるかどうかとは対照的です)。別のクラスから___init___メソッドを継承している場合は、おそらく問題ありません(スーパークラスwillが通常の方法でその名前にすでにバインドされているため)。 doは_object.__init___では機能しないため、クラスがデフォルトの___init___を使用している場合は、このようなことは避けたいです。
  2. ___init___メソッドを検索して、それを_func_globals_ディクショナリにします。これは、グローバルルックアップ(super呼び出しで参照されるクラスを見つけるなど)が行われる場所です。これは通常、___init___メソッドが最初に定義されたモジュールのグローバルディクショナリです。そのような辞書はaboutであり、registerが戻るとすぐに_cls.__name___が挿入されるので、それを自分で早く挿入します。
  3. 最後にインスタンスを作成し、レジストリに挿入します。これは、インスタンスの作成で例外がスローされるかどうかに関係なく、作成したバインディングを確実に削除するためのtry/finallyブロックにあります。これが必要になることはほとんどありません(とにかく名前がリバウンドされる時間の99.999%から)が、いつか他の奇妙な魔法がひどく相互作用する可能性を最小限に抑えるために、このような奇妙な魔法をできるだけ絶縁することをお勧めしますそれ。

このバージョンのregisterは、デコレータとして呼び出されても、メタクラス(まだメタクラスの適切な使用法ではないと思います)によって呼び出されても機能します。それでも失敗するいくつかのあいまいなケースがあります:

  1. しないが___init___メソッドを持っているが、_self.someMethod_を呼び出すメソッドを継承し、定義されているクラスでsomeMethodがオーバーライドされている奇妙なクラスを想像できますsuperを呼び出します。おそらくありそうもない。
  2. ___init___メソッドは、元々別のモジュールで定義されていて、クラスブロックで___init__ = externally_defined_function_を実行してクラスで使用されている場合があります。ただし、他のモジュールの_func_globals_属性。これは、一時的なバインディングによって、そのモジュール内のこのクラスの名前の定義が上書きされることを意味します(エラー)。再び、ありそうもない。
  3. おそらく私が考えていない他の奇妙なケース。

ハックを追加して、これらの状況で少し堅牢にすることもできますが、Pythonの性質は、この種のハックが可能であり、完全に弾丸にすることは不可能であることの両方です証明。

11
Ben

pythonでは、ここでの答えはうまくいきませんでした。__metaclass__が機能しませんでした。

以下は、定義時にクラスのすべてのサブクラスを登録するコードです。

registered_models = set()

class RegisteredModel(type):
    def __new__(cls, clsname, superclasses, attributedict):
        newclass = type.__new__(cls, clsname, superclasses, attributedict)
        # condition to prevent base class registration
        if superclasses:
            registered_models.add(newclass)
        return newclass

class CustomDBModel(metaclass=RegisteredModel):
    pass

class BlogpostModel(CustomDBModel):
    pass

class CommentModel(CustomDBModel):
   pass

# prints out {<class '__main__.BlogpostModel'>, <class '__main__.CommentModel'>}
print(registered_models)
1
hlidka

Baseクラスを直接呼び出すと、(super()を使用する代わりに)機能するはずです。

  def __init__(self):
        Base.__init__(self)
0
Andreas Jung