9

staticmethod特にオブジェクトの動作に関連しているため、Pythonの記述子プロトコルの微妙な点を理解するのに少し助けが必要です。簡単な例から始めて、それを繰り返し拡張し、各ステップでの動作を調べます。

class Stub:
    @staticmethod
    def do_things():
        """Call this like Stub.do_things(), with no arguments or instance."""
        print "Doing things!"

この時点では、これは期待どおりに動作しますが、ここで行われていることは少し微妙です。を呼び出すときStub.do_things()、do_thingsを直接呼び出していません。代わりに、実際に呼び出すように、必要な関数を独自の記述子プロトコル内にラップStub.do_thingsしたインスタンスを参照します。これは、最初に必要な関数を返し、その後呼び出されます。staticmethodstaticmethod.__get__

>>> Stub
<class __main__.Stub at 0x...>
>>> Stub.do_things
<function do_things at 0x...>
>>> Stub.__dict__['do_things']
<staticmethod object at 0x...>
>>> Stub.do_things()
Doing things!

ここまでは順調ですね。次に、クラスのインスタンス化をカスタマイズするために使用されるデコレータでクラスをラップする必要があります。デコレータは、新しいインスタンス化を許可するか、キャッシュされたインスタンスを提供するかを決定します。

def deco(cls):
    def factory(*args, **kwargs):
        # pretend there is some logic here determining
        # whether to make a new instance or not
        return cls(*args, **kwargs)
    return factory

@deco
class Stub:
    @staticmethod
    def do_things():
        """Call this like Stub.do_things(), with no arguments or instance."""
        print "Doing things!"

さて、当然、この部分はそのままでstaticmethodsを壊すと予想されます。これは、クラスがデコレータの背後に隠されているためです。つまり、Stubクラスではありませんが、そのインスタンスは、呼び出したときにfactoryインスタンスを生成できます。Stubそれはそう:

>>> Stub
<function factory at 0x...>
>>> Stub.do_things
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'function' object has no attribute 'do_things'
>>> Stub()
<__main__.Stub instance at 0x...>
>>> Stub().do_things
<function do_things at 0x...>
>>> Stub().do_things()
Doing things!

これまでのところ、私はここで何が起こっているのかを理解しています。staticmethods私の目標は、クラスがラップされていても、期待どおりに機能する能力を回復することです。運が良ければ、Python stdlibにはfunctoolsと呼ばれるものが含まれています。これは、この目的のためだけにいくつかのツールを提供します。つまり、関数をラップする他の関数のように動作させます。そこで、デコレータを次のように変更します。

def deco(cls):
    @functools.wraps(cls)
    def factory(*args, **kwargs):
        # pretend there is some logic here determining
        # whether to make a new instance or not
        return cls(*args, **kwargs)
    return factory

今、物事は面白くなり始めています:

>>> Stub
<function Stub at 0x...>
>>> Stub.do_things
<staticmethod object at 0x...>
>>> Stub.do_things()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'staticmethod' object is not callable
>>> Stub()
<__main__.Stub instance at 0x...>
>>> Stub().do_things
<function do_things at 0x...>
>>> Stub().do_things()
Doing things!

待って……なに? functoolsstaticmethodをラッピング関数にコピーしますが、呼び出し可能ではありませんか?なぜだめですか?ここで何が恋しかったですか?

私はこれを少し遊んでいて、実際にこの状況で機能するように自分で再実装staticmethodすることを思いつきましたが、なぜそれが必要だったのか、これがこの問題の最善の解決策であるのかどうかはよくわかりません。完全な例は次のとおりです。

class staticmethod(object):
    """Make @staticmethods play nice with decorated classes."""

    def __init__(self, func):
        self.func = func

    def __call__(self, *args, **kwargs):
        """Provide the expected behavior inside decorated classes."""
        return self.func(*args, **kwargs)

    def __get__(self, obj, objtype=None):
        """Re-implement the standard behavior for undecorated classes."""
        return self.func

def deco(cls):
    @functools.wraps(cls)
    def factory(*args, **kwargs):
        # pretend there is some logic here determining
        # whether to make a new instance or not
        return cls(*args, **kwargs)
    return factory

@deco
class Stub:
    @staticmethod
    def do_things():
        """Call this like Stub.do_things(), with no arguments or instance."""
        print "Doing things!"

実際、期待どおりに機能します。

>>> Stub
<function Stub at 0x...>
>>> Stub.do_things
<__main__.staticmethod object at 0x...>
>>> Stub.do_things()
Doing things!
>>> Stub()
<__main__.Stub instance at 0x...>
>>> Stub().do_things
<function do_things at 0x...>
>>> Stub().do_things()
Doing things!

装飾されたクラス内でstaticmethodを期待どおりに動作させるには、どのようなアプローチを取りますか?これが最善の方法ですか?__call__これが大騒ぎせずに機能するために、組み込みのstaticmethodがそれ自体で実装されないのはなぜですか?

ありがとう。

4

2 に答える 2

4

The problem is that you're changing the type of Stub from a class to a function. This is a pretty serious violation and it's not much surprise that things are breaking.

The technical reason that your staticmethods are breaking is that functools.wraps works by copying __name__, __doc__ and __module__ etc. (source: http://hg.python.org/cpython/file/3.2/Lib/functools.py) from the wrapped instance to the wrapper, while updating the wrapper's __dict__ from the wrapped instance's __dict__. It should be obvious now why staticmethod doesn't work - its descriptor protocol is being invoked on a function instead of a class, so it gives up on returning a bound callable and just returns its non-callable self.

w.r.t. actually doing what you're interested in (some kind of Singleton?), you probably want your decorator to return a class with a __new__ that has the required behaviour. You don't need to be worried about __init__ being called unwanted, as long as your wrapper class __new__ doesn't actually return a value of the wrapper class type, rather an instance of the wrapped class:

def deco(wrapped_cls):
    @functools.wraps(wrapped_cls)
    class Wrapper(wrapped_cls):
        def __new__(cls, *args, **kwargs):
            ...
            return wrapped_cls(*args, **kwargs)
    return Wrapper

Note the distinction between the wrapped_cls argument to the decorator (that becomes closed over in the wrapper class) and the cls argument to Wrapper.__new__.

Note that it's perfectly OK to use functools.wraps on a class wrapping a class - just not on a class wrapping a function!

You can also modify the wrapped class, in which case you don't need functools.wraps:

def deco(wrapped_cls):
    def __new__(cls, *args, **kwargs)
        ...
        return super(wrapped_cls, cls)(*args, **kwargs)
    wrapped_cls.__new__ = classmethod(__new__)
    return wrapped_cls

Note however that this method will end up invoking __init__ on existing instances, so you'll have to work around that (e.g. by wrapping __init__ to short-circuit on existing instances).

As an addendum: it might be possible to make your function-wrapping-a-class decorator work in the cases you know about with a lot of effort, but you'll still run into problems - for example, isinstance(myObject, Stub) has no chance of working as Stub is no longer a type!

于 2012-06-25T00:32:03.450 に答える
2

you almost did what i yould have done:

def deco(cls):
    class factory(cls):
        def __new__(cls_factory, *args, **kwargs):
            # pretend there is some logic here determining
            # whether to make a new instance or not
            return cls.__new__(*args, **kwargs)
    return factory

that should do it. Problem may be that __init__ is called also on old instances returned by __new__.

于 2012-06-24T20:42:04.453 に答える