5

I want to be able to ask a class's __init__ method what it's parameters are. The straightforward approach is the following:

cls.__init__.__func__.__code__.co_varnames[:code.co_argcount]

However, that won't work if the class has any decorators. It will give the parameter list for the function returned by the decorator. I want to get down to the original __init__ method and get those original parameters. In the case of a decorator, the decorator function is going to be found in the closure of the function returned by the decorator:

cls.__init__.__func__.__closure__[0]

However, it is more complicated if there are other things in the closure, which decorators may do from time to time:

def Something(test):
    def decorator(func):
        def newfunc(self):
            stuff = test
            return func(self)
        return newfunc
    return decorator

def test():
    class Test(object):
        @Something(4)
        def something(self):
            print Test
    return Test

test().something.__func__.__closure__
(<cell at 0xb7ce7584: int object at 0x81b208c>, <cell at 0xb7ce7614: function object at 0xb7ce6994>)

And then I have to decide if I want to the parameters from decorator or the parameters from the original function. The function returned by the decorator could have *args and **kwargs for its parameters. What if there are multiple decorators and I have to decide which is the one I care about?

So what is the best way to find a function's parameters even when the function may be decorated? Also, what is the best way to go down a chain of decorators back to the decorated function?

Update:

Here is effectively how I am doing this right now (names have been changed to protect the identity of the accused):

import abc
import collections

IGNORED_PARAMS = ("self",)
DEFAULT_PARAM_MAPPING = {}
DEFAULT_DEFAULT_PARAMS = {}

class DICT_MAPPING_Placeholder(object):
    def __get__(self, obj, type):
        DICT_MAPPING = {}
        for key in type.PARAMS:
            DICT_MAPPING[key] = None
        for cls in type.mro():
            if "__init__" in cls.__dict__:
                cls.DICT_MAPPING = DICT_MAPPING
                break
        return DICT_MAPPING

class PARAM_MAPPING_Placeholder(object):
    def __get__(self, obj, type):
        for cls in type.mro():
            if "__init__" in cls.__dict__:
                cls.PARAM_MAPPING = DEFAULT_PARAM_MAPPING
                break
        return DEFAULT_PARAM_MAPPING

class DEFAULT_PARAMS_Placeholder(object):
    def __get__(self, obj, type):
        for cls in type.mro():
            if "__init__" in cls.__dict__:
                cls.DEFAULT_PARAMS = DEFAULT_DEFAULT_PARAMS
                break
        return DEFAULT_DEFAULT_PARAMS

class PARAMS_Placeholder(object):
    def __get__(self, obj, type):
        func = type.__init__.__func__
        # unwrap decorators here
        code = func.__code__
        keys = list(code.co_varnames[:code.co_argcount])
        for name in IGNORED_PARAMS:
            try: keys.remove(name)
            except ValueError: pass
        for cls in type.mro():
            if "__init__" in cls.__dict__:
                cls.PARAMS = tuple(keys)
                break
        return tuple(keys)

class BaseMeta(abc.ABCMeta):
    def __init__(self, name, bases, dict):
        super(BaseMeta, self).__init__(name, bases, dict)
        if "__init__" not in dict:
            return
        if "PARAMS" not in dict:
            self.PARAMS = PARAMS_Placeholder()
        if "DEFAULT_PARAMS" not in dict:
            self.DEFAULT_PARAMS = DEFAULT_PARAMS_Placeholder()
        if "PARAM_MAPPING" not in dict:
            self.PARAM_MAPPING = PARAM_MAPPING_Placeholder()
        if "DICT_MAPPING" not in dict:
            self.DICT_MAPPING = DICT_MAPPING_Placeholder()


class Base(collections.Mapping):
    __metaclass__ = BaseMeta
    """
    Dict-like class that uses its __init__ params for default keys.

    Override PARAMS, DEFAULT_PARAMS, PARAM_MAPPING, and DICT_MAPPING
    in the subclass definition to give non-default behavior.

    """
    def __init__(self):
        pass
    def __nonzero__(self):
        """Handle bool casting instead of __len__."""
        return True
    def __getitem__(self, key):
        action = self.DICT_MAPPING[key]
        if action is None:
            return getattr(self, key)
        try:
            return action(self)
        except AttributeError:
            return getattr(self, action)
    def __iter__(self):
        return iter(self.DICT_MAPPING)
    def __len__(self):
        return len(self.DICT_MAPPING)

print Base.PARAMS
# ()
print dict(Base())
# {}

At this point Base reports uninteresting values for the four contants and the dict version of instances is empty. However, if you subclass you can override any of the four, or you can include other parameters to the __init__:

class Sub1(Base):
    def __init__(self, one, two):
        super(Sub1, self).__init__()
        self.one = one
        self.two = two

Sub1.PARAMS
# ("one", "two")
dict(Sub1(1,2))
# {"one": 1, "two": 2}

class Sub2(Base):
    PARAMS = ("first", "second")
    def __init__(self, one, two):
        super(Sub2, self).__init__()
        self.first = one
        self.second = two

Sub2.PARAMS
# ("first", "second")
dict(Sub2(1,2))
# {"first": 1, "second": 2}
efotinis
  • 14,565
  • 6
  • 31
  • 36
Eric Snow
  • 1,198
  • 8
  • 21
  • 1
    Why the heck would you want that? –  Jul 30 '10 at 21:50
  • 4
    The fact that this is so difficult should indicate to you that it's not the right thing to do. – Katriel Jul 30 '10 at 22:31
  • I want to be able to use objects of a class as dicts and have explicit controls on what keys are exposed through `__getitem__` and `__iter__`. The parameters of `__init__` make great default keys so I am pulling those programatically. I am just trying to address corner cases, like when descriptors get involved. – Eric Snow Jul 31 '10 at 00:09
  • could you give a quick code example of exactly what it is that you would like do? Specifically, I would be interested in how you would map the arguments of `__init__` to the exposed keys. I ask because we may well be able to work around this problem instead of working through it. – aaronasterling Aug 13 '10 at 08:55
  • @aaron, it's not quite quick, but I have updated my post with an essentials-only version of what I am doing. In PARAMS_Placeholder is where I was running into trouble. For now I am getting by because I am not using decorators. However, I can't do that forever. – Eric Snow Aug 13 '10 at 14:37
  • @Eric, I deleted my answer because I realized that I was assuming that the closures would form a tree: They can form a cyclic graph. I have code written that can deal with this but I can not figure out how to deal with decorators that replace a method with a discriptor because nothing need be contained in closure in that case. If you haven't seen it, I would recommend the solution in the accepted answer of http://stackoverflow.com/questions/3232024/introspection-to-get-decorator-names-on-a-method. – aaronasterling Aug 15 '10 at 01:50
  • @aaron, no problem. The link is a pretty good one too. Unfortunately they basically say that there isn't a good way to do it, which I don't believe. The way I did it works fine, it's just overly complicated. I was looking for any easier way. – Eric Snow Aug 15 '10 at 02:52
  • 1
    This is not possible in general, since there's no requirement that a decorator retains a reference to the decorated function at all. – jchl Aug 16 '10 at 11:22

1 Answers1

3

Consider this decorator:

def rickroll(old_function):
    return lambda junk, junk1, junk2: "Never Going To Give You Up"

class Foo(object):
    @rickroll
    def bar(self, p1, p2):
        return p1 * p2

print Foo().bar(1, 2)

In it, the rickroll decorator takes the bar method, discards it, replaces it with a new function that ignores its differently-named (and possibly numbered!) parameters and instead returns a line from a classic song.

There are no further references to the original function, and the garbage collector can come and remove it any time it likes.

In such a case, I cannot see how you could find the parameter names p1 and p2. In my understanding, even the Python interpreter itself has no idea what they used to be called.

Oddthinking
  • 24,359
  • 19
  • 83
  • 121
  • That's a great point. I had definitely not considered a decorator that returns an entirely different, unrelated function. You are right about a lack of references. It make sense, but at the same time seems unintuitive that the function defined in the class would disappear. You've given me something to think about. Thanks! – Eric Snow Aug 17 '10 at 16:00
  • I have been pursuing this topic in a more specific manner in http://stackoverflow.com/questions/3481872/decorated-for-python-decorators – Eric Snow Aug 17 '10 at 16:22