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)
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'
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 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.