Python
decorators
21 May 2015 with @pybcn
https://github.com/theblackboxio/python-decorators-demo
Disclaimer
This speaker belongs to Java world, so do not
believe me.
Guillermo Blasco Jiménez
es.linkedin.com/in/guillermoblascojimenez
@theblackboxio
Motivation
def bar():
# preprocess things
# execute bar logic
# postprocess things
return # value
def foo():
# preprocess things
# execute foo logic
# postprocess things
return # value
Motivation
def bar():
# preprocess things
# execute bar logic
# postprocess things
return # value
def foo():
# preprocess things
# execute foo logic
# postprocess things
return # value
Often the same processing, therefore we have code
duplicated
Motivation by example: cache
def hardStuff1(x):
if x in cache:
return cache[x]
else:
v = # bar logic
cache[x] = v
return v
def hardStuff2(x):
if x in cache:
return cache[x]
else:
v = # foo logic
cache[x] = v
return v
Motivation by example: transactions
def transaction1():
tx = startTransaction()
try:
# execute bar logic
tx.commit()
except:
tx.rollback()
return # value
def transaction2():
tx = startTransaction()
try:
# execute foo logic
tx.commit()
except:
tx.rollback()
return # value
Motivation by examples
● Logging
● Retry
● Wrap exceptions
● Deprecation warnings
● Pre/post conditions
● Access authorization
● Profiling
Decorators basics
Objectives:
● isolate shared code
● do not change the way to call our function
Decorators basics: code isolation
def executeWithCache(x, hardStuffLogic):
if x in cache:
return cache[x]
else:
v = hardStuffLogic(x)
cache[x] = v
return v
executeWithCache(5, fibonacci) # before was: fibonacci(5)
executeWithCache(5, anotherCostlyFunction)
Function Reference!
Decorators basics: preserve calling
def executeWithCache(hardStuffLogic):
def wrapper(x):
if x in cache:
return cache[x]
else:
v = hardStuffLogic(x)
cache[x] = v
return v
return wrapper
fibonacci = executeWithCache(fibonacci)
anoherCostlyFunction = executeWithCache(anotherCostlyFunction)
Yes, a function that returns a function
Decorators basics: syntactic sugar
def fibonacci(x):
#...
fibonacci = executeWithCache(fibonacci)
@executeWithCache
def fibonacci(x):
# ...
Since Python 2.4
Theory
Decorator pattern is about adding functionality
to objects without change the functionality of
others.
Python decorators implements decorator
pattern applied to functions (that are objects in
Python, by the way)
Ramifications
● Decorators with parameters
● Decorators as functions or classes
● Nesting decorators
● Decorators applied to classes
● Preservation of function metadata
● Testing decorators
Decorators with
parameters I
@precondition(lambda x: x >= 0)
def fibonacci(x):
…
Decorators with
parameters II
def precondition(precond):
def wrapper1(f):
def wrapper2(x):
if not precond(x):
raise ValueError('Precondition failed')
return f(x)
return wrapper2
return wrapper1
Yes! A function that returns a function that returns a
function
Decorators as classes I
One clear problem with decorators: too many
function nesting
def precondition(precond):
def wrapper1(f):
def wrapper2(x):
if not precond(x):
raise ValueError('Precondition failed')
return f(x)
return wrapper2
return wrapper1
Agree with me that this is ugly
Decorators as classes II
Actually you just need a callable object that
returns a function to use it as decorator. Python
has three callable objects:
● Functions
● Classes (to build object instances)
● Objects with __call__
Decorators as classes III
class Precondition:
def __init__(self, checkFunction):
self.p = checkFunction
def __call__(self, f):
def wrapper(x):
if not self.p(x):
raise ValueError()
return f(x)
return wrapper
This makes the object instance
a callable object
@Precondition(lambda x: x >= 0)
def fibonacci(x):
…
Decorators as classes IV
# create new instance of decorator
dec = Precondition(lambda x: x>=0)
# apply as decorator
fibonacci = dec(fibonacci)
# the wrapper that “dec” returns is called
fibonacci(5)
Nesting decorators
@cache
@transaction
@notNullInput
def bar(x):
...
This is great!
Nesting decorators: first problem I
@memoryCache
@localFileCache
@hdfsCache
@transaction
@notNullInput
@notNullOutput
def bar(x):
...
Do not abuse, please
Nesting decorators: first problem II
# Make decorators more generics, do not forget OOP
@Cache(“memory”,”localFile”,”hdfs”)
@transaction
@notNull # checks input and output
def bar(x):
...
Nesting decorators: second problem I
@cache
@transaction
@notNullInput
def bar(x):
...
Order matters!
Nesting decorators: second problem II
@cache # executed first
@transaction # executed second
@notNullInput # executed third
def bar(x):
...
Why to cache and start a transaction for a possible invalid
computation?
Nesting decorators: second problem III
@notNullInput # executed first
@cache # executed second
@transaction # executed third
def bar(x):
...
Decorators applied to classes I
Decorators applied to functions, are callables
that takes a function and at the end returns a
function.
Decorators applied to classes, are callables
that takes a class and at the end returns a
class.
Decorators applied to classes II
# Prints a warning every time
# a method is called on class instance
@deprecated
class OldClass:
…
Decorators applied to classes III
def deprecated(clazz):
class DeprecatedClass:
# Black sorcery to bypass class
# methods dynamically
return DeprecatedClass
Decorators applied to classes IV
● Do not replace class inheritance with class decorators
● Hard to find actual use cases
● Hard to deal with dynamically generated classes
● Hard to deal with bound functions with function
decorators
● Related to metaclasses and metaprogramming
Preservation of function metadata I
>>> def foo():
…
>>> print foo.__name__
foo
>>> @cache
def foo():
…
>>> print foo.__name__
cache
Preservation of function metadata II
Metadata loss:
● __name__ and __doc__
● argument specification
● source code inspection
Implies that:
● Harder to debug
● Harder to profile
● Unreadable stack traces
Preservation of function metadata III
Use:
● functools.wraps for function decorators
● functools.update_wrapper for class decs.
More info:
Graham Dumpleton - How you implemented your Python
decorator is wrong
http://blog.dscpl.com.au/2014/01/how-you-implemented-
your-python.html
Preservation of function metadata IV
import functools
def function_wrapper(wrapped):
@functools.wraps(wrapped)
def _wrapper(*args, **kwargs):
return wrapped(*args, **kwargs)
return _wrapper
import functools
class function_wrapper(object):
def __init__(self, wrapped):
self.wrapped = wrapped
functools.update_wrapper(self, wrapped)
def __call__(self, *args, **kwargs):
return self.wrapped(*args, **kwargs)
Testing decorators I
def testCache():
times = 0
def mock(x):
times = times + 1
mock = cache(mock)
mock(1)
assert times == 1
mock(1)
assert times == 1
Use the old fashioned way to use
decorators as simply function
wrappers.
Use the wrapped function to check the
state of the decorator instance
Decorators in real world
● Python itself (@staticmethod)
● https://wiki.python.org/moin/PythonDecoratorLibrary
● Django (@permission_required)
How is this possible?
● Python functions are objects
● Python is a dynamic language, i.e. your code
can generate more code
There is something like decorators?
Well, yes. It is named Aspect Oriented
Programming (AOP). The problem is that if
your language does not support dynamic code
and/or your functions are not objects then you
have to cook manually decorators or use an
AOP framework (proxy classes nightmare).
Guillermo Blasco Jiménez
Thanks!
https://github.com/theblackboxio/python-decorators-demo
Questions?

Python decorators

  • 1.
    Python decorators 21 May 2015with @pybcn https://github.com/theblackboxio/python-decorators-demo
  • 2.
    Disclaimer This speaker belongsto Java world, so do not believe me. Guillermo Blasco Jiménez es.linkedin.com/in/guillermoblascojimenez @theblackboxio
  • 3.
    Motivation def bar(): # preprocessthings # execute bar logic # postprocess things return # value def foo(): # preprocess things # execute foo logic # postprocess things return # value
  • 4.
    Motivation def bar(): # preprocessthings # execute bar logic # postprocess things return # value def foo(): # preprocess things # execute foo logic # postprocess things return # value Often the same processing, therefore we have code duplicated
  • 5.
    Motivation by example:cache def hardStuff1(x): if x in cache: return cache[x] else: v = # bar logic cache[x] = v return v def hardStuff2(x): if x in cache: return cache[x] else: v = # foo logic cache[x] = v return v
  • 6.
    Motivation by example:transactions def transaction1(): tx = startTransaction() try: # execute bar logic tx.commit() except: tx.rollback() return # value def transaction2(): tx = startTransaction() try: # execute foo logic tx.commit() except: tx.rollback() return # value
  • 7.
    Motivation by examples ●Logging ● Retry ● Wrap exceptions ● Deprecation warnings ● Pre/post conditions ● Access authorization ● Profiling
  • 8.
    Decorators basics Objectives: ● isolateshared code ● do not change the way to call our function
  • 9.
    Decorators basics: codeisolation def executeWithCache(x, hardStuffLogic): if x in cache: return cache[x] else: v = hardStuffLogic(x) cache[x] = v return v executeWithCache(5, fibonacci) # before was: fibonacci(5) executeWithCache(5, anotherCostlyFunction) Function Reference!
  • 10.
    Decorators basics: preservecalling def executeWithCache(hardStuffLogic): def wrapper(x): if x in cache: return cache[x] else: v = hardStuffLogic(x) cache[x] = v return v return wrapper fibonacci = executeWithCache(fibonacci) anoherCostlyFunction = executeWithCache(anotherCostlyFunction) Yes, a function that returns a function
  • 11.
    Decorators basics: syntacticsugar def fibonacci(x): #... fibonacci = executeWithCache(fibonacci) @executeWithCache def fibonacci(x): # ... Since Python 2.4
  • 12.
    Theory Decorator pattern isabout adding functionality to objects without change the functionality of others. Python decorators implements decorator pattern applied to functions (that are objects in Python, by the way)
  • 13.
    Ramifications ● Decorators withparameters ● Decorators as functions or classes ● Nesting decorators ● Decorators applied to classes ● Preservation of function metadata ● Testing decorators
  • 14.
    Decorators with parameters I @precondition(lambdax: x >= 0) def fibonacci(x): …
  • 15.
    Decorators with parameters II defprecondition(precond): def wrapper1(f): def wrapper2(x): if not precond(x): raise ValueError('Precondition failed') return f(x) return wrapper2 return wrapper1 Yes! A function that returns a function that returns a function
  • 16.
    Decorators as classesI One clear problem with decorators: too many function nesting def precondition(precond): def wrapper1(f): def wrapper2(x): if not precond(x): raise ValueError('Precondition failed') return f(x) return wrapper2 return wrapper1 Agree with me that this is ugly
  • 17.
    Decorators as classesII Actually you just need a callable object that returns a function to use it as decorator. Python has three callable objects: ● Functions ● Classes (to build object instances) ● Objects with __call__
  • 18.
    Decorators as classesIII class Precondition: def __init__(self, checkFunction): self.p = checkFunction def __call__(self, f): def wrapper(x): if not self.p(x): raise ValueError() return f(x) return wrapper This makes the object instance a callable object @Precondition(lambda x: x >= 0) def fibonacci(x): …
  • 19.
    Decorators as classesIV # create new instance of decorator dec = Precondition(lambda x: x>=0) # apply as decorator fibonacci = dec(fibonacci) # the wrapper that “dec” returns is called fibonacci(5)
  • 20.
  • 21.
    Nesting decorators: firstproblem I @memoryCache @localFileCache @hdfsCache @transaction @notNullInput @notNullOutput def bar(x): ... Do not abuse, please
  • 22.
    Nesting decorators: firstproblem II # Make decorators more generics, do not forget OOP @Cache(“memory”,”localFile”,”hdfs”) @transaction @notNull # checks input and output def bar(x): ...
  • 23.
    Nesting decorators: secondproblem I @cache @transaction @notNullInput def bar(x): ... Order matters!
  • 24.
    Nesting decorators: secondproblem II @cache # executed first @transaction # executed second @notNullInput # executed third def bar(x): ... Why to cache and start a transaction for a possible invalid computation?
  • 25.
    Nesting decorators: secondproblem III @notNullInput # executed first @cache # executed second @transaction # executed third def bar(x): ...
  • 26.
    Decorators applied toclasses I Decorators applied to functions, are callables that takes a function and at the end returns a function. Decorators applied to classes, are callables that takes a class and at the end returns a class.
  • 27.
    Decorators applied toclasses II # Prints a warning every time # a method is called on class instance @deprecated class OldClass: …
  • 28.
    Decorators applied toclasses III def deprecated(clazz): class DeprecatedClass: # Black sorcery to bypass class # methods dynamically return DeprecatedClass
  • 29.
    Decorators applied toclasses IV ● Do not replace class inheritance with class decorators ● Hard to find actual use cases ● Hard to deal with dynamically generated classes ● Hard to deal with bound functions with function decorators ● Related to metaclasses and metaprogramming
  • 30.
    Preservation of functionmetadata I >>> def foo(): … >>> print foo.__name__ foo >>> @cache def foo(): … >>> print foo.__name__ cache
  • 31.
    Preservation of functionmetadata II Metadata loss: ● __name__ and __doc__ ● argument specification ● source code inspection Implies that: ● Harder to debug ● Harder to profile ● Unreadable stack traces
  • 32.
    Preservation of functionmetadata III Use: ● functools.wraps for function decorators ● functools.update_wrapper for class decs. More info: Graham Dumpleton - How you implemented your Python decorator is wrong http://blog.dscpl.com.au/2014/01/how-you-implemented- your-python.html
  • 33.
    Preservation of functionmetadata IV import functools def function_wrapper(wrapped): @functools.wraps(wrapped) def _wrapper(*args, **kwargs): return wrapped(*args, **kwargs) return _wrapper import functools class function_wrapper(object): def __init__(self, wrapped): self.wrapped = wrapped functools.update_wrapper(self, wrapped) def __call__(self, *args, **kwargs): return self.wrapped(*args, **kwargs)
  • 34.
    Testing decorators I deftestCache(): times = 0 def mock(x): times = times + 1 mock = cache(mock) mock(1) assert times == 1 mock(1) assert times == 1 Use the old fashioned way to use decorators as simply function wrappers. Use the wrapped function to check the state of the decorator instance
  • 35.
    Decorators in realworld ● Python itself (@staticmethod) ● https://wiki.python.org/moin/PythonDecoratorLibrary ● Django (@permission_required)
  • 36.
    How is thispossible? ● Python functions are objects ● Python is a dynamic language, i.e. your code can generate more code
  • 37.
    There is somethinglike decorators? Well, yes. It is named Aspect Oriented Programming (AOP). The problem is that if your language does not support dynamic code and/or your functions are not objects then you have to cook manually decorators or use an AOP framework (proxy classes nightmare).
  • 38.