Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

python decorator to modify variable in current scope

Goal: Make a decorator which can modify the scope that it is used in.

If it worked:

class Blah(): # or perhaps class Blah(ParentClassWhichMakesThisPossible)

    def one(self):
        pass

    @decorated
    def two(self):
        pass

>>> Blah.decorated
["two"]

Why? I essentially want to write classes which can maintain specific dictionaries of methods, so that I can retrieve lists of available methods of different types on a per class basis. errr.....

I want to do this:

class RuleClass(ParentClass):
    @rule
    def blah(self):
        pass

    @rule
    def kapow(self):
        pass

    def shazam(self):

class OtherRuleClass(ParentClass):
    @rule
    def foo(self):
        pass

    def bar(self):
        pass

>>> RuleClass.rules.keys()
["blah", "kapow"]
>>> OtherRuleClass.rules.keys()
["foo"]
like image 552
AlexH Avatar asked Dec 12 '22 22:12

AlexH


2 Answers

You can do what you want with a class decorator (in Python 2.6) or a metaclass. The class decorator version:

def rule(f):
    f.rule = True
    return f

def getRules(cls):
    cls.rules = {}
    for attr, value in cls.__dict__.iteritems():
        if getattr(value, 'rule', False):
            cls.rules[attr] = value
    return cls

@getRules
class RuleClass:
    @rule
    def foo(self):
        pass

The metaclass version would be:

def rule(f):
    f.rule = True
    return f

class RuleType(type):
    def __init__(self, name, bases, attrs):
        self.rules = {}
        for attr, value in attrs.iteritems():
            if getattr(value, 'rule', False):
                self.rules[attr] = value
        super(RuleType, self).__init__(name, bases, attrs)

class RuleBase(object):
    __metaclass__ = RuleType

class RuleClass(RuleBase):
    @rule
    def foo(self):
        pass

Notice that neither of these do what you ask for (modify the calling namespace) because it's fragile, hard and often impossible. Instead they both post-process the class -- through the class decorator or the metaclass's __init__ method -- by inspecting all the attributes and filling the rules attribute. The difference between the two is that the metaclass solution works in Python 2.5 and earlier (down to 2.2), and that the metaclass is inherited. With the decorator, subclasses have to each apply the decorator individually (if they want to set the rules attribute.)

Both solutions do not take inheritance into account -- they don't look at the parent class when looking for methods marked as rules, nor do they look at the parent class rules attribute. It's not hard to extend either to do that, if that's what you want.

like image 110
Thomas Wouters Avatar answered Dec 31 '22 04:12

Thomas Wouters


Problem is, at the time the decorated decorator is called, there is no object Blah yet: the class object is built after the class body finishes executing. Simplest is to have decorated stash the info "somewhere else", e.g. a function attribute, then a final pass (a class decorator or metaclass) reaps that info into the dictionary you desire.

Class decorators are simpler, but they don't get inherited (so they wouldn't come from a parent class), while metaclasses are inherited -- so if you insist on inheritance, a metaclass it will have to be. Simplest-first, with a class decorator and the "list" variant you have at the start of your Q rather than the "dict" variant you have later:

import inspect

def classdecorator(aclass):
  decorated = []
  for name, value in inspect.getmembers(aclass, inspect.ismethod):
    if hasattr(value, '_decorated'):
      decorated.append(name)
      del value._decorated
  aclass.decorated = decorated
  return aclass

def decorated(afun):
  afun._decorated = True
  return afun

now,

@classdecorator
class Blah(object):

    def one(self):
        pass

    @decorated
    def two(self):
        pass

gives you the Blah.decorated list you request in the first part of your Q. Building a dict instead, as you request in the second part of your Q, just means changing decorated.append(name) to decorated[name] = value in the code above, and of course initializing decorated in the class decorator to an empty dict rather than an empty list.

The metaclass variant would use the metaclass's __init__ to perform essentially the same post-processing after the class body is built -- a metaclass's __init__ gets a dict corresponding to the class body as its last argument (but you'll have to support inheritance yourself by appropriately dealing with any base class's analogous dict or list). So the metaclass approach is only "somewhat" more complex in practice than a class decorator, but conceptually it's felt to be much more difficult by most people. I'll give all the details for the metaclass if you need them, but I'd recommend sticking with the simpler class decorator if feasible.

like image 25
Alex Martelli Avatar answered Dec 31 '22 04:12

Alex Martelli