Using decorators with argument in Python

J

Jigar Tanna

I am new to Python and Django, was going through the concept of
decorators
where I came across a special case of using arguments with decorators
Below is the code for memoization where I was looking at the
concept...

cache = {}
def get_key(function, *args, **kw) :
key = '%s. %s: ' % (function. __module__,function. __name__)
hash_args = [ str(arg) for arg in args]
hash_kw = [' %s: %s' % (k, hash(v) )
for k, v in kw.items() ]
return ' %s:: %s: : %s' % (key, hash_args, hash_kw)

def memoize(get_key=get_key, cache=cache) :
def _memoize( function) :
print function
def __memoize(*args, **kw) :
key = get_key(function, *args, **kw)
try:
return cache[key]
except KeyError:
cache[key] = function( *args, **kw)
return cache[key]
return __memoize
return _memoize

@memoize()
def factory(n) :
return n * n

# testcase
#print factory(3)
#
#

coming across to certain views from people, it is not a good practice
to use
decorators with arguments (i.e. @memoize() ) and instead it is good to
just
use @memoize. Can any of you guys explain me advantages and
disadvantages of
using each of them
 
L

Lie Ryan

coming across to certain views from people, it is not a good practice
to use
decorators with arguments (i.e. @memoize() ) and instead it is good to
just
use @memoize. Can any of you guys explain me advantages and
disadvantages of
using each of them

Simplicity is one, using @decor() means you have at least three-level
nested functions, which means the code is likely to be very huge and
perhaps unnecessarily.

However, there is nothing wrong with using decorators with arguments;
except that if you have a simpler alternative, then why use the more
complex ones?
 
I

Ian Kelly

coming across to certain views from people, it is not a good practice
to use
decorators with arguments (i.e. @memoize() ) and instead it is good to
just
use @memoize. Can any of you guys explain me advantages and
disadvantages of
using each of them

The main concern I think is not with how the decorators are used but
how they are designed. An argument-less decorator will normally be
used as @memoize, and @memoize() will likely not work. A decorator
with arguments that are all optional will normally be used as
@memoize(), and @memoize will likely not work. This naturally leads
to some confusion: do I need parentheses to use this particular
decorator or not?

As a personal design goal I try to make my decorators either take at
least one required argument or take no arguments at all. This way
it's either @memoize or @memoize(foo), but never just the confusing
@memoize().

Cheers,
Ian
 
J

John Posner

Simplicity is one, using @decor() means you have at least three-level
nested functions, which means the code is likely to be very huge and
perhaps unnecessarily.

Bruce Eckel pointed out (
http://www.artima.com/weblogs/viewpost.jsp?thread=240808) that the
result of "decoration" need not be a function. Instead, it can be an
object (instance of a user-defined class) that's callable (because the
class implements a __call__ method).

Investigating how this fact fit in with the current thread, I came up
with an alternative to the three levels of "def" (pronounced "three
levels of death"). Following is code for two decorators:

* the first one encloses the output of a function with lines of "#"
characters, and is used like this:

@enclose
myfun(...

* the second one encloses the output of a function with lines of a
user-specified character, and is used like this:

@enclose("&")
myfun(...

Here's the Python2 code for each one:

################## decorator to be called with no argument

class enclose:
"""
class that can be used as a function decorator:
prints a line of "#" before/after the function's output
"""
def __init__(self, funarg):
self.func = funarg
def __call__(self, *args, **kwargs):
print "\n" + "#" * 50
self.func(*args, **kwargs)
print "#" * 50 + "\n"

################## decorator to be called with argument

def enclose(repeat_char):
"""
function that returns a class that can be used as a decorator:
prints a line of <repeat_char> before/after the function's output
"""
class _class_to_use_as_decorator:
def __init__(self, funarg):
self.func = funarg
def __call__(self, *args, **kwargs):
print "\n" + repeat_char * 50
self.func(*args, **kwargs)
print repeat_char * 50 + "\n"

return _class_to_use_as_decorator


Best,
John
 
I

Ian Kelly

How about just having one bit of code that works either way?

How would you adapt that code if you wanted to be able to decorate a
function that takes arguments?

This also won't work if the argument to the decorator is itself a
callable, such as in the OP's example.
 
E

Ethan Furman

John said:
Investigating how this fact fit in with the current thread, I came up
with an alternative to the three levels of "def" (pronounced "three
levels of death"). Following is code for two decorators:

* the first one encloses the output of a function with lines of "#"
characters, and is used like this:

@enclose
myfun(...

* the second one encloses the output of a function with lines of a
user-specified character, and is used like this:

@enclose("&")
myfun(...

Here's the Python2 code for each one:

[snippety-snip]

How about just having one bit of code that works either way?

8<------------------------------------------------------------------
class enclose(object):
def __init__(self, char='#'):
self.char = char
if callable(char): # was a function passed in directly?
self.char = '#' # use default char
self.func = char
def __call__(self, func=None):
if func is None:
return self._call()
self.func = func
return self
def _call(self):
print("\n" + self.char * 50)
self.func()
print(self.char * 50 + '\n')

if __name__ == '__main__':
@enclose
def test1():
print('Spam!')

@enclose('-')
def test2():
print('Eggs!')

test1()
test2()
8<------------------------------------------------------------------

Output:

##################################################
Spam!
##################################################


--------------------------------------------------
Eggs!
--------------------------------------------------

8<------------------------------------------------------------------


~Ethan~
 
I

Ian Kelly

8<----------------------------------------------------------------
class enclose(object):
   func = None
   def __init__(self, char='#'):
       self.char = char
       if callable(char):  # was a function passed in directly?
           self.char = '#' # use default char
           self.func = char
   def __call__(self, func=None, *args, **kwargs):
       if self.func is None:
           self.func = func
           return self
       if func is not None:
           args = (func, ) + args
       return self._call(*args, **kwargs)
   def _call(self, *args, **kwargs):
       print("\n" + self.char * 50)
       self.func(*args, **kwargs)
       print(self.char * 50 + '\n')
if __name__ == '__main__':
   @enclose
   def test1():
       print('Spam!')
   @enclose('-')
   def test2():
       print('Eggs!')
   @enclose
   def test3(string):
       print(string)
   @enclose('^')
   def test4(string):
       print(string)
   test1()
   test2()
   test3('Python')
   test4('Rules!  ;)')
8<----------------------------------------------------------------


@enclose
def test5(string, func):
print(func(string))
test5('broken', func=str.upper)
 
E

Ethan Furman

Ian said:
How would you adapt that code if you wanted to be able to decorate a
function that takes arguments?

8<----------------------------------------------------------------
class enclose(object):
func = None
def __init__(self, char='#'):
self.char = char
if callable(char): # was a function passed in directly?
self.char = '#' # use default char
self.func = char
def __call__(self, func=None, *args, **kwargs):
if self.func is None:
self.func = func
return self
if func is not None:
args = (func, ) + args
return self._call(*args, **kwargs)
def _call(self, *args, **kwargs):
print("\n" + self.char * 50)
self.func(*args, **kwargs)
print(self.char * 50 + '\n')
if __name__ == '__main__':
@enclose
def test1():
print('Spam!')
@enclose('-')
def test2():
print('Eggs!')
@enclose
def test3(string):
print(string)
@enclose('^')
def test4(string):
print(string)
test1()
test2()
test3('Python')
test4('Rules! ;)')
8<----------------------------------------------------------------
This also won't work if the argument to the decorator is itself a
callable, such as in the OP's example.

Indeed. In that case you need two keywords to __init__, and the
discipline to always use the keyword syntax at least for the optional
function paramater. On the bright side, if one forgets, it blows up
pretty quickly.

Whether it's worth the extra effort depends on the programmer's tastes,
of course.

8<----------------------------------------------------------------
class enclose(object):
func = None
pre_func = None
def __init__(self, dec_func=None, opt_func=None):
if opt_func is None:
if dec_func is not None: # was written without ()'s
self.func = dec_func
else:
self.pre_func = opt_func
def __call__(self, func=None, *args, **kwargs):
if self.func is None:
self.func = func
return self
if func is not None:
args = (func, ) + args
if self.pre_func is not None:
self.pre_func()
return self._call(*args, **kwargs)
def _call(self, *args, **kwargs):
print("\n" + '~' * 50)
self.func(*args, **kwargs)
print('~' * 50 + '\n')

if __name__ == '__main__':
def some_func():
print('some func here')
@enclose
def test1():
print('Spam!')
@enclose(opt_func=some_func)
def test2():
print('Eggs!')
@enclose
def test3(string):
print(string)
@enclose(opt_func=some_func)
def test4(string):
print(string)
test1()
test2()
test3('Python')
test4('Rules! ;)')
8<----------------------------------------------------------------

~Ethan~
 
E

Ethan Furman

Ian said:
@enclose
def test5(string, func):
print(func(string))
test5('broken', func=str.upper)

Yes, that is a limitation -- one loses the func keyword for the
decorated function. If I were to actually use this, I'd probably go
with '_func' as the keyword.

~Ethan~

PS
Thanks for the code review!
 
J

John Posner

On 2:59 PM, Ethan Furman wrote:

def __call__(self, func=None):
if func is None:
return self._call()
self.func = func
return self
def _call(self):
print("\n" + self.char * 50)
self.func()
print(self.char * 50 + '\n')

I believe the "if" block should be:

if func is None:
self._call()
return

Or perhaps the _call() method should be revised:

def _call(self):
print("\n" + self.char * 50)
retval = self.func()
print(self.char * 50 + '\n')
return retval

-John
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

Forum statistics

Threads
473,769
Messages
2,569,582
Members
45,068
Latest member
MakersCBDIngredients

Latest Threads

Top