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!
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']
tovalid_for
, as inwe 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 oftypelist
from within its body at runtime.