views:

138

answers:

1

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}
+2  A: 

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
LOL I bumped you up just for having that rickroll decorator. Now on to reading your answer...
Eric Snow
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
I have been pursuing this topic in a more specific manner in http://stackoverflow.com/questions/3481872/decorated-for-python-decorators
Eric Snow