简体   繁体   中英

Difficulty getting decorator to work inside class in python

I am trying to create a decorator that creates an association between a function and associated text it will execute in response to. Here is some working code that illustrates what I mean:

# WORKING CODE
mapping = {}

def saying(text):
        def decorator(function):
            mapping[text] = function
            return function
        return decorator

@saying("hi")
def hi():
    print "hello there"

@saying("thanks")
@saying("gracias")
def thanks():
    print "you're welcome"

mapping["hi"]() #running this line will print "hello there"
mapping["thanks"]() #running this line will print "you're welcome"

The issue occurs when I attempt to add these methods to a class. Something like this:

#NON-WORKING CODE:
class politeModule(object):
    def __init__(self):
        self.mapping = {}

    @saying("hi")
    def hi(self):
        print "hello there"

    @saying("thanks")
    @saying("gracias")
    def thanks(self):
        print "you're welcome"

module = politeModule()
module.mapping["hi"]()
module.mapping["thanks"]()

The issue is, I don't know where to put the decorator so it can access mapping and also work. I understand there are a lot of StackOverflow questions and articles about this. I tried to implement some of the solutions described in this blog post, but repeatedly got stuck on scoping issues and accessing the mapping dictionary from inside the decorator

The problem here is that you need to access self to append self.mapping , and the decorator (as has been previously mentioned) does not have access to the self object since it is run during class definition before the instance is even created.

You could store you variable mapping at the class level instead of the instance level. You could then have your decorator add an attribute to each function it decorates, and then use a class decorator to search for functions that have that attribute and add them to cls.mapping:

method decorator

def saying(text):
    def decorator(function):
        if hasattr(function,"mapping_name"):
            function.mapping_name.append(text)
        else:
            function.mapping_name=[text]
        return function
    return decorator

(I have used a list here since otherwise, when you call the decorator twice (as with the 'thanks', 'gracias' example) the mapping_name would be overwritten if it were just a string.)

class decorator

def map_methods(cls):
    cls.mapping = {}
    for item in cls.__dict__.values():
        if hasattr(item, "mapping_name"):
            for x in item.mapping_name:
                cls.mapping[x] = item
    return cls

You would then have to decorate the whole class with the map_methods decorator as follows:

@map_methods
class politeModule(object):

    @saying("hi")
    def hi(self):
        print ("hello there")

    @saying("thanks")
    @saying("gracias")
    def thanks(self):
        print ("you're welcome")

(also note, we no longer want to write self.mapping=[] so I have removed your init.

Alternative method

Alternatively, you could use a meta-class for this kind of thing, however I think the more important question to ask is why you want to do this. Although there might be a reason to do this, there is probably a better way round whatever the original problem is.

An important Note

You won't be able to call the function with the method you have used in the original post eg module.mapping["hi"]() . Note that module.mapping["hi"] returns a function, which you then call, so there is no object which will be passed to the first argument self , so you must instead write module.mapping["hi"](module) . One way round this problem is you could write your init as follows:

def __init__(self):
    self.mapping = { k: functools.partial(m, self) for k,m in self.mapping.items() }

This will mean mapping is now an instance variable rather than a class variable. You will now also be able to call your function with module.mapping["hi"]() since functools.partial binds self to the first argument. Don't forget to add in import functools to the top of your script.

You will need a 2 times initialization.

At class initialization, the decorators should be used to attach names to the specified methods. Optionally after the class is fully defined, it could receive a new mapping attribute mapping names to method names.

A member initialization, each member should receive a mapping attribute mapping names to bound methods.

I would use a base class and a decorator for that:

class PoliteBase(object):
    def __init__(self):
        """Initializes "polite" classes, meaning subclasses of PoliteBase

This initializer will be called by subclasse with no explicit __init__ method,
but any class with a __init__ method will have to call this one explicitely
for proper initialization"""
        cls = self.__class__              # process this and all subclasses
        if not hasattr(cls, "mappings"):  # add a mapping attribute TO THE CLASS if not
            cls.mappings = {}             #  present
            for m in cls.__dict__:        # and feed it with "annotated" methods and names
                o = getattr(cls, m)
                if callable(o) and hasattr(o, "_names"):    # only process methods
                    for name in o._names:                   #   with a _name attribute
                        cls.mappings[name] = m              # map the name to the method
                                                            #  name

        self.mappings = { k: getattr(self, m)         # now set the mapping for instances
                  for k,m in cls.mappings.iteritems() }  # mapping name to a bound method

If a subclass does not define an __init__ method, the base class one will be used, but if a subclass defines one, it will have to explicitely call this one.

def saying(name):
    """Decorator used in combination with "PoliteBase" subclasses

It just adds a _name attribute to the decorated method containing the associated
names. This attribute will later be processed at instance initialization time."""
    @functools.wraps(f)
    def wrapper(f):
        try:
            f._names.append(name)
        except Exception as e:
            f._names = [name]
        return f
    return wrapper

Once this is done you can define polite classes:

class politeClass(PoliteBase):
    def __init__(self):
        self.mapping = {}

    @saying("hi")
    def hi(self):
        print "hello there"

    @saying("thanks")
    @saying("gracias")
    def thanks(self):
        print "you're welcome"

obj = politeClass()
obj.mapping["hi"]()
obj.mapping["thanks"]()

I renamed your module object because a module is a different thing in Python sense (it is the script file itself)

Register decorator

First off, when using a decorator as a register for functions, a good option is to write a class for you decorator so it can be used to both register and access registered functions.

class RegisterDecorator(object):
    def __init__(self):
        self._register = {}

    def __getitem__(self, item):
        return self._register[item]

    def register(self, text):
        def wrapper(f):
            self._register[text] = f
            return f
        return wrapper

saying = RegisterDecorator()

@saying.register('hello')
def f():
    print('Hello World')

saying['hello']() # prints 'Hello World'

Registering methods from a class

The above will work fine for registering methods. Although, it will only register the unbound methods. This means you have to pass the self argument manually.

saying = Saying()

class PoliteModule(object):
    @saying.register("hi")
    def hi(self):
        print("hello there")

saying['hi'](PoliteModule()) # prints: 'hello there'

saying['hi']() # TypeError: hi() missing 1 required positional argument: 'self'

Registering a bound method

Registering a bound method cannot be done at class instantiation, because no instance exists yet. You will have to create an instance and register its bound method.

saying = Saying()

class PoliteModule(object):
    def hi(self):
        print("hello there")

politeInstance = PoliteModule()

saying.register("hi")(politeInstance.hi)

saying["hi"]() # prints: hello there

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM