Python descriptors with arguments?

2k views Asked by At

TL;DR Python 2.7.5, when using descriptors as decorators, is there any way to pass in arguments (to the __init__ method)? OR How can I get access to a class instance's attributes using a method decorator with arguments (as here)? -- I believe this is not possible, though, hence the focus below on descriptors...


Long version

I have a class of objects, with different "type" attributes. Based on an instance's "type", I would like a method to be available or not. I know one way is to create multiple classes, but I'm trying to not have a bunch of if / else statements when creating these objects. For example, I have two objects A and B that are almost identical, except object B I don't want to have the get_start_date() method available. So essentially, what I want is that both A and B are instances of class MyObjects, but have a "type" attribute that is different.

type(A) == type(B)
A.genus_type != B.genus_type

I would use that .genus_type attribute to differentiate which methods are allowable and which ones not...

I was thinking I could use decorators with a whitelist, like:

def valid_for(whitelist):
    def wrap(f):
        def wrapper(*args, **kwargs):
            return f(*args, **kwargs)
    return wrapper
return wrap

class A(object):
    @valid_for(['typeB'])
    def do_something_cool(self):
        print 'blah'

But the problem was I did not have access to the actual class instance in the decorator, where I could test for the instance type attribute. Based on this SO question, I thought, "I can use descriptors!".

So I then tried:

class valid_for(object):
    """ descriptor to check the type of an item, to see
    if the method is valid for that type"""
    def __init__(self, func):
        self.f = func

    def __get__(self, instance, owner):
        def wrapper(*args):
            return self.f(instance, *args)
        return wrapper

But then I couldn't figure out how to get the ['typeB'] parameter passed into the descriptor...by default, Python passes in the called method as the argument to __init__. I could create hard-coded descriptors for each type and nest them, but then I wonder if I will run into this problem. Assuming I could overcome the nesting issue, it also seems less clean to do something like:

class A(object):
    @valid_for_type_b
    @valid_for_type_f
    @valid_for_type_g
    def do_something_cool(self):
        print 'blah'

Doing something like this just made my func equal to the list ['typeB']...

class valid_for(object):
    """ descriptor to check the type of an item, to see
    if the method is valid for that type"""
    def __init__(self, func, *args):
        self.f = func

    def __get__(self, instance, owner):
        def wrapper(*args):
            return self.f(instance, *args)
        return wrapper

class A(object):
    @valid_for(['typeB'])
    def do_something_cool(self):
        print 'blah'

And my func is not in the *args list, so I can't just do a simple swap of arguments (*args is empty).

I've been looking for hints here and here, but haven't found anything that seems like a clean or valid workaround. Is there a clean way to do this, or do I have to use multiple classes and just mix-in the various methods? Or, right now I am leaning towards an instance method that checks, but that seems less clean and reusable...

class A(object):
    def _valid_for(self, whitelist):
        if self.genus_type not in whitelist:
            raise Exception

    def do_something_cool(self):
        self._valid_for(['foo'])
        print 'blah'

I am using Python 2.7.5.


UPDATE 1

Per a suggestion in the comments, I tried:

def valid_for2(whitelist):
    def wrap(f):
        def wrapper(*args, **kwargs):
            import pdb
            pdb.set_trace()
            print args[0].genus_type
            return f(*args, **kwargs)
        return wrapper
    return wrap

But at this point, args[0]. just returns the args:

(Pdb) args[0]
args = (<FormRecord object at 0x112f824d0>,)
kwargs = {}
(Pdb) args[0].genus_type
args = (<FormRecord object at 0x112f824d0>,)
kwargs = {}

However, using functools as suggested does work -- so I will award the answer. There seems to be some black magic in functools that lets the arguments in?


UPDATE 2

So investigating jonrsharpe's suggestion more, his method also seems to work, but I have to explicitly use self instead of args[0]. Not sure why the behavior is different...

def valid_for2(whitelist):
    def wrap(f):
        def wrapper(self, *args, **kwargs):
            print self.genus_type
            return f(*args, **kwargs)
        return wrapper
    return wrap

results in the same output as with functools. Thanks!

1

There are 1 answers

0
unutbu On BEST ANSWER

If I understand your situation correctly, what you are looking for is a closure -- a function that can refer to the local namespace of an outer function.

Since you are passing ['typeB'] to valid_for, as in

@valid_for(['typeB'])

we should make valid_for a function that returns a decorator. The decorator in turn accepts a function (the nascent method) as input and returns another (wrapper) function.

Below wrapper is a closure which can access the value of typelist from within its body at runtime.


import functools
def valid_for(typelist):
    def decorator(func):
        @functools.wraps(func)
        def wrapper(self, *args):
            if self.genus_type in typelist:
                return func(self, *args)
            else:
                # handle this case
                raise NotImplementedError(
                    '{} not in {}'.format(self.genus_type, typelist))
        return wrapper
    return decorator

class A(object):
    def __init__(self):
        self.genus_type = 'typeA'
    @valid_for(['typeB'])
    def do_something_cool(self):
        print 'blah'

a = A()
try:
    a.do_something_cool()
except NotImplementedError as err:
    print(err)
    # typeA not in ['typeB']

a.genus_type = 'typeB'
a.do_something_cool()
# blah