1

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__).

4
  • Have you thought about overriding __getattribute__ and doing some checking in that logic? Commented Feb 24, 2015 at 16:08
  • Which __getattribute__? Commented Feb 24, 2015 at 16:46
  • Met.__getattribute__. Since __getattribute__ is called before dict lookup (if I'm not mistaken). Commented Feb 24, 2015 at 21:10
  • 1
    It could be, but I don't want to add extra checks on all method calls as it introduces quite an overhead. Commented Feb 25, 2015 at 13:45

1 Answer 1

5

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'
Sign up to request clarification or add additional context in comments.

12 Comments

The get method was just a name, it doesn't do anything near __getitem__. But thank you for pointing that solution too. So isn't there any other solution to this but that "ugly" call? I am trying to build an interface for underlying operations and I was looking for something prettier...P.S.: The link you've posted doesn't find the "#" part.
I've edited my question, added my actual concern to it (sorry for not being specific from the beginning, wanted to make the example as simple as I could).
@AndreiHorak: link fixed (it was originally a Python 3 link and I manually adjusted it for 2, but forgot the section title had changed).
@AndreiHorak: your changed question is now very different but the solution should be clear; type(self)[arg] or Klass[arg] in the Klass.get() method will translate to Meta.__getitem__.
@AndreiHorak: in other words, you renamed the method and the problem went away.
|

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.