Use class method not instance method with the same

2020-02-07 06:58发布

I have the following snippet:

class Meta(type):
    def __getattr__(self, name):
        pass

class Klass(object):
    __metaclass__ = Meta

    def get(self, arg):
        pass

Now, if I do:

kls = Klass()
kls.get('arg')

everything works as expected (the instance method get is called).

But if I do:

Klass.get('arg')

again the instance method is found and an exception is given, since it is treated as an unbound method.

How can I make a call to Klass.get('arg') go through the __getattr__ defined in the metaclass? I need this because I want to proxy all methods called on a class to another object (this would be done in __getattr__).

1条回答
beautiful°
2楼-- · 2020-02-07 07:25

You'll have to look up the method on the type and pass in the first (self) argument manually:

type(Klass).get(Klass, 'arg')

This problem is the very reason that special method names are looked up using this path; custom classes would not be hashable or representable themselves if Python didn't do this.

You could make use of that fact; rather than use a get() method, use __getitem__, overloading [..] indexing syntax, and have Python do the type(ob).methodname(ob, *args) dance for you:

class Meta(type):
    def __getitem__(self, arg):
        pass

class Klass(object):
    __metaclass__ = Meta

    def __getitem__(self, arg):
        pass

and then Klass()['arg'] and Klass['arg'] work as expected.

However, if you have to have Klass.get() behave differently (and the lookup for this to be intercepted by Meta.__getattribute__) you have to explicitly handle this in your Klass.get method; it'll be called with one argument less if called on the class, you could make use of that and return a call on the class:

_sentinel = object()

class Klass(object):
    __metaclass__ = Meta

    def get(self, arg=_sentinel):
        if arg=_sentinel:
            if isinstance(self, Klass):
                raise TypeError("get() missing 1 required positional argument: 'arg'")
            return type(Klass).get(Klass, self)
        # handle the instance case ... 

You could also handle this in a descriptor that mimics method objects:

class class_and_instance_method(object):
    def __init__(self, func):
        self.func = func
    def __get__(self, instance, cls=None):
        if instance is None:
            # return the metaclass method, bound to the class
            type_ = type(cls)
            return getattr(type_, self.func.__name__).__get__(cls, type_)
        return self.func.__get__(instance, cls)

and use this as a decorator:

class Klass(object):
    __metaclass__ = Meta

    @class_and_instance_method
    def get(self, arg):
        pass

and it'll redirect look-ups to the metaclass if there is no instance to bind to:

>>> class Meta(type):
...     def __getattr__(self, name):
...         print 'Meta.{} look-up'.format(name)
...         return lambda arg: arg
... 
>>> class Klass(object):
...     __metaclass__ = Meta
...     @class_and_instance_method
...     def get(self, arg):
...         print 'Klass().get() called'
...         return 'You requested {}'.format(arg)
... 
>>> Klass().get('foo')
Klass().get() called
'You requested foo'
>>> Klass.get('foo')
Meta.get look-up
'foo'

Applying the decorator can be done in the metaclass:

class Meta(type):
    def __new__(mcls, name, bases, body):
        for name, value in body.iteritems():
            if name in proxied_methods and callable(value):
                body[name] = class_and_instance_method(value)
        return super(Meta, mcls).__new__(mcls, name, bases, body)

and you can then add methods to classes using this metaclass without having to worry about delegation:

>>> proxied_methods = ('get',)
>>> class Meta(type):
...     def __new__(mcls, name, bases, body):
...         for name, value in body.iteritems():
...             if name in proxied_methods and callable(value):
...                 body[name] = class_and_instance_method(value)
...         return super(Meta, mcls).__new__(mcls, name, bases, body)
...     def __getattr__(self, name):
...         print 'Meta.{} look-up'.format(name)
...         return lambda arg: arg
... 
>>> class Klass(object):
...     __metaclass__ = Meta
...     def get(self, arg):
...         print 'Klass().get() called'
...         return 'You requested {}'.format(arg)
... 
>>> Klass.get('foo')
Meta.get look-up
'foo'
>>> Klass().get('foo')
Klass().get() called
'You requested foo'
查看更多
登录 后发表回答