| Author: | Matt Wilson <matt@tplus1.com> |
|---|
Decorators are functions that wrap around other functions. Sometimes a decorator monkeys with the stuff going into or out of the decorated function.
Decorators depend on two fun facts...
map applies a function to every element of a sequence.
>>> map(len, ['a', 'bb', 'ccc']) [1, 2, 3] >>> from string import upper >> map(upper, ['a', 'bb', 'ccc']) ['A', 'BB', 'CCC']
>>> def add_n_to_x(n): ... def add_to_x(x): ... return n + x ... return add_to_x ... >>> add3_to_x = add_n_to_x(3)
What is add3_to_x?
>>> type(add3_to_x) <type 'function'>
What will add3_to_x(8) return?
>>> add3_to_x(8) 11
What about add_n_to_x(7)(3)?
>>> add_n_to_x(7)(3) 10
Ruby won't get this until 1.9, so feel smug.
>>> def f(a, b): ... "Returns a minus b" ... return a - b ... >>> from functools import partial >>> subtract_one = partial(f, b=1)
People argue over whether this is currying or not.
functools.partial returns a new object based on the original function, except one or more parameters are set to fixed values.
>>> subtract_one(5) 4
>>> subtract_one_from_three = \ ... partial(subtract_one, a=3)
>>> subtract_one_from_three() 2
"What you do at Initech is, you take the specifications from the customers and you bring them down to the software engineers."
>>> def smykowsky(f): ... ... def h(*args, **kwargs): ... return f(*args, **kwargs) ... ... return h
smykowsky mangles the function signature of the function he decorates.
f requires exactly two parameters:
>>> f(3, 1)
2
>>> f(1, 2, 3) # kaboom!
Traceback (most recent call last):
...
TypeError: f() takes exactly 2 arguments (3 given)
We can inspect the function f to find out the parameters it requires:
>>> import inspect >>> inspect.getargspec(f) (['a', 'b'], None, None, None)
Now see what happens when smykowski gets involved:
>>> h = smykowsky(f) >>> inspect.getargspec(h) ([], 'args', 'kwargs', None)
>>> @smykowsky ... def g(): ... return True
Is a shortcut for:
>>> def g(): ... return True ... >>> g = smykowsky(g)
The inner function is trapped forever!
>>> def as_string(f): ... "convert results of function f to string." ... ... def g(*args, **kwargs): ... return str(f(*args, **kwargs)) ... return g
What will as_string(f)(3, 1) + "abc" return?
>>> as_string(f)(3, 1) + "abc" '2abc'
Panicked developers sometimes do this:
>>> from decoratortalk import log
>>> def gross(a, b):
... log.debug("a is %s and b is %s." % (a, b))
... y = f(a, b)
... log.debug("y is %s." % y)
... return y
That logging can be moved into a decorator instead...
>>> from decoratortalk import debuglogger
>>> f(3, 1)
2
>>> logging_f = debuglogger(f)
>>> logging_f(3, 1)
args is (3, 1) and kwargs is {}.
results from calling f is 2.
2
I'll revise this in the next slide:
>>> def add1(f): ... "Return a new function returns 1 + f(...)" ... ... def g(*args, **kwargs): ... return 1 + f(*args, **kwargs) ... ... return g
Instead of writing add1 from scratch, it would be much nicer to be able to do something like:
>>> from decoratortalk import add_n >>> add1 = add_n(1)
We could reuse add_n to make other decorators too:
>>> subtract5 = add_n(-5)
>>> def add_n(n): ... "Return a decorator that adds n." ... def dec(f): ... def h(*args, **kwargs): ... return n + f(*args, **kwargs) ... return h ... return dec
Two problems with decorators so far:
You have to write your decorator with a parameter f.
>>> from decorator import decorator >>> def smykowsky2(f, *args, **kwargs): ... return f(*args, **kwargs)
>>> dec = decorator(smykowsky2) >>> f2 = dec(f) >>> from inspect import getargspec as g >>> g(f) == g(f2) True
>>> print f.__doc__ Returns a minus b >>> f3 = smykowsky(f) >>> print f3.__doc__ None >>> print f2.__doc__ Returns a minus b
Functions often coerce parameters into certain types, sort of like this:
>>> def j(x): ... if isinstance(x, (list, tuple)): ... xlist = x ... else: ... xlist = [x] ... ... # Now do the real stuff
>>> def as_list(f, x): ... "Assure the arg x is a list." ... if isinstance(x, (list, tuple)): ... xlist = x ... else: ... xlist = [x] ... return f(xlist)
>>> from decorator import decorator >>> @decorator(as_list) ... def j2(x): ... print type(x), x ...
>>> j2(99) <type 'list'> [99]
>>> j2([1, 2]) <type 'list'> [1, 2]
The decorator module version also offers a better approach for decorator factories:
>>> class AddX(object): ... def __init__(self, x): ... self.x = x ... def call(self, f, *args, **kwargs): ... return self.x + f(*args, **kwargs) ... >>> AddX = decorator(AddX)
>>> @AddX(13) ... def f(a, b): ... return a + b
What will f(1, 1) return?
>>> f(1, 1) 15
Developers working on Python 3k
Both classes and instances can call methods decorated as classmethods.
>>> class C(object): ... @classmethod ... def spam(cls): ... return "delicious!" ...
Now classes and instances can call these methods.
>>> C.spam() 'delicious!' >>> c1 = C() >>> c1.spam() 'delicious!'
>>> d = {}
>>> d.fromkeys('abc', 99)
{'a': 99, 'c': 99, 'b': 99}
>>> dict.fromkeys('def', 98)
{'e': 98, 'd': 98, 'f': 98}
An instance made other new instances.
If you decorate with @staticmethod, your method won't get an extra parameter like self or cls. Some reasons to use staticmethod:
staticmethods useful when you want to add functions to a class:
>>> class C(object): ... g1 = f ... g2 = staticmethod(f) >>> i = C()
What will happen if I call i.g1(3, 1)?
>>> i.g1(3, 1) ------------------------------------------------------------ Traceback (most recent call last): File "<ipython console>", line 1, in <module> TypeError: f() takes exactly 2 arguments (3 given)
However, g2 will work fine:
>>> i.g2(9, 8) 1
They look like attributes, but are really methods.
>>> class Rectangle(object): ... def calculate_area(self): ... return self.height * self.width ... ... area = property(calculate_area)
>>> r = Rectangle() >>> r.height, r.width = 3, 4 >>> r.calculate_area() 12 >>> r.area 12
>>> getattr(r, 'area') 12
>>> class Square(object): ... @property ... def area(self): ... return self.side ** 2 ... >>> sq = Square() >>> sq.side = 3 >>> sq.area 9
Specify setters and deleters as properties:
class C(object):
@property
def x(self):
return self._x
@x.setter
def x(self, value):
self._x = value
Subclasses can tweak getters:
class D(C):
@C.x.getter
def x(self):
return self._x * 2
@x.setter
def x(self, value):
self._x = value / 2
A decorator doesn't HAVE to tweak inputs or return values.
You can use a decorator to tell another system about the presence of a function.
>>> functions = list() >>> def register_function(f): ... functions.append(f) ... return f ...
Dispatching on parameters rather than types
Four possible procedures total {A, B, C, D}
| ... | Dog | Cat |
| Standard | A | C |
| Premium | B | D |
>>> def groom1(pet, package): ... "Groom according to pet and package types" ... if isinstance(pet, Dog): ... if isinstance(package, Standard): ... A() ... else: ... B()
... elif isinstance(pet, Cat): ... if isinstance(package, Standard): ... C() ... else: ... D()
>>> def groom2(pet, package):
... dispatcher = {(Dog, Standard):A,
... (Dog, Premium):C,
... (Cat, Standard):B,
... (Cat, Premium):D}
... key = (type(pet), type(package))
... f = dispatcher[key]
... f()
We have one function that is going to keep growing longer and longer and longer as we add more branches.
We can make groom a method rather than a function. Dog.groom and Cat.groom still need to test the type of package.
>>> class Cat(Pet): ... ... def groom(self, package): ... if isinstance(package, Standard): ... C() ... else: ... D()
When I call mittens.groom(x), Python runs Cat.groom(mittens, x).
mittens is effectively a parameter that python dispatches on.
Since mittens's type is the only parameter python looks at in order to find the right method to run, this is called "single dispatch".
In 1987, common lisp introduced generic methods. Generic methods can dispatch based on the types and values of any of the parameter.
(defgeneric groom (pet grooming-package))
(defmethod groom ((pet dog)
(grooming-package standard-groom))
"cheapskate dog owner"
(A))
But you came here for python instead...
>>> from peak.rules import abstract, when, after >>> @abstract ... def groom3(pet, package): ... "Like defgeneric" ... >>> @when(groom3, (Cat, Premium)) ... def snootycat(x1, x2): ... D()
>>> groom3(Cat(), Premium()) wash dry hairball medicine A ridiculous haircut
Now the vet wants to know if your dog is young or old. Young dogs get different treatments than old dogs.
Will you subclass by this dimension too?
Imagine that the premium package for dogs is really just the standard package plus something else:
>>> def B(): ... A() ... print 'toothbrushing'
PEAK Rules can first run one rule, then run another, so this is not necessary.
First make a rule that applies to dogs getting the standard or premium package:
>>> @when(groom3, (Dog, Package)) ... def everydog(x1, x2): ... A() ...
Remember Package is the superclass of Standard and Premium. So this rule will match any grooming.
Now make a rule that runs AFTER the first rule.
>>> def B2(): ... "Like B, but doesn't call A first" ... print 'toothbrushing'
>>> @after(groom3, (Dog, Premium)) ... def fancydog(x1, x2): ... B2()
>>> groom3(Dog(), Standard()) wash flea dip dry
>>> groom3(Dog(), Premium()) wash flea dip dry toothbrushing
You can do more than test on types! Anything goes!
>>> @abstract ... def f(i): ... "abstract!" ...
>>> @when(f, "i % 2") ... def f(i): ... print "%d must be an odd number." % i ...
PEAK-Rules isn't magic. If you give it a parameter that matches two "when" rules, it will fuss.
If you give it a parameter that matches NO rules, it will fuss.
Stuff that I can talk about if time permits.
I can test all the code samples in the text file because of the doctest module.
Doctests in a function's docstring guarantee that the function works and provide examples of intended use.
You can store really long doctests in a separate file.
>>> @as_string ... @add1 ... def g(a, b): ... return a + b ... >>> g(3, 4) '8'
>>> @add1
... @as_string
... def g(a, b):
... return a + b
...
>>> g(3, 4)
Traceback (most recent call last):
...
TypeError: unsupported operand type(s) for +: 'int' and 'str'
Apply that debuglogger to every method in a class:
>>> from inspect import getmembers, ismethod >>> from decoratortalk import debuglogger >>> def debug_everything(C): ... "danger -- this alters the C param." ... for name, value in getmembers(C, ismethod): ... setattr(C, name, debuglogger(value)) ... return C ...
In 2.5, you have to apply it like this:
>>> debug_everything(C) #2.5
In 2.6, you can do this:
>>> @debug_everything ... class C(object): ... def f1(self, a): ... return a + 1 ...
Here's how it works:
>>> c = C()
>>> c.f1(99)
args is (<__main__.C object at 0x8366fec>, 99) and kwargs is {}.
results from calling f1 is 100.
100