Decorator

Syntax

@dec
def foo(...):
  ...

# Still called by
>>> foo()

@ is a syntax sugar. Under the hood, it is:

foo = dec(foo)

>>> foo()

Attention

This syntax sugar assignment is executed during decorated function definition not on function call.

Decorator doesn’t change function name, foo is still foo. So the thing below @dec is the input of decorator. In above example, it accepts a func and return a new func. Decorator also accept arguments.

@dec(param=something)
def func(...):
  ...

>>> func()

It could be translated to:

true_dec = dec(param=something)
func = true_dec(func)

>>> func()

It reveals the logic under the hood is calculating function call dec(a=something) first which returns the new decorator for later process.

Chained decorators

@outside
@inside
def func(...):
  ...

Again, this will translate to thing below during func() definition. Intuitively, the rule is parsing the inside decorator first and then outside.

func = outside(inside(func))

Write a decorator

When writing a decorator dec, Let’s assume: it accepts a func and returns a func. So initially, it should looks like:

def dec(func):
  ...
  return newfunc

newfunc is a function which needs to be defined inside dec. So it looks like:

def dec(func):
    ...
    def newfunc(...):
        ...
    return newfunc

Since we return newfunc and newfunc is the function we will use later, this raise two considerations:

  1. newfunc is a wrapper(from the iteral meaning of decorator) of func, so func usually is called inside newfunc.

  2. Keep in mind newfunc is the function returned at the end. In definition, newfunc need to pass any arguments it accepts to func. So *args and **kwargs should be used.

#1 makes it look like:

def dec(func):
    ...
    def newfunc(...):
        ...
        func(...)
        ...
    return newfunc

Address #2:

def dec(func):
    ...
    def newfunc(*args, **kwargs):
        ...
        func(*args, **kwargs)
        ...
    return newfunc

Eventually, a runable decorator example shows below:

def dec(func):
    def newfunc(*arg, **kwargs):
        print('calling func')
        func(*arg, **kwargs)
        print('end func')
    return newfunc

@dec
def foo():
    print('abc')

>>> foo()
calling func
abc
end func

Write a decorator with parameters

Again, It runs the function with parameters first which return true decorator. So the definition looks like:

def dec(param):

  # Write a decorator definition inside and return it.

  return newdec

If we put true decorator definition inside, it will looks like. (... might involve using param).

def dec(param):
  ...
  def newdec(func)
    ...
    def newfunc(*arg, **kwargs)
      ...
      func(*arg, **kwargs)
      ...
    return newfunc
    ...
  ...
  return newdec

Class decorator

Class decorator is nothing more. If a function’s decorator is a function that accepts a function and returns a new function. Then class’s decorator is a function that accepts a class and returns a new class.

def dec(cls):
    class newcls(cls):
        attr = 100
    return newcls

@dec
class C:
    pass

>>> obj = C()
>>> C.attr
100

Make any callable become a decorator

Decorator syntax is nothing more than x = dec(x). dec has to be callable, clearly function is callable, so in above example decorator is a function/class that accepts X and return a new-X. However, pure function is not the only callable thing. When construct a instance, You use CLASS in a callable way.

class Dec1:

    def __init__(self, func):
        print('__init__')
        self.func = func

    def __call__(self, *args, **kwargs):
        print('__call__')
        self.func(*args, **kwargs)


@Dec1
def foo(a, b):
    print('foo', a, b)

>>> foo(2, 3)
# __init__
# __call__
# foo 2 3

In this example, we implicit have foo = Dec1(foo) on global level. foo become a instance of Dec1.

If func = dec(func) happend inside Class, on the left hand side, func become a class attribute. When you use func, instead of call func(), you need obj.func. This gives us a chance to sneak true function object inside return value of __get__, aka non-data descriptor. This is exactly logic under the hood of staticmethod.

class Dec2:

    def __init__(self, func):
        print('__init__')
        self.func = func

    def __get__(self, obj, type=None):
        print('__get__')
        return self.func


class C:

    @Dec2
    def func2(a, b):
        print('func2', a, b)

>>> obj = C()
>>> obj.func2(2, 3)

In this example, we implicit have func2 = Dec2(func2) inside class C. func2 is a class attribute in C. And most important func2 is a non-data descriptor defined by __get__ method in class Dec2.

When you write a decorator e.g. Dec, Dec has to be callable to achive Dec(...). But what is returned and how to use returned value is depend on you. If you want, you could have this(btw, `...` below isn’t emoji, but it kind of reflect correct feeling):

def dimensionality_reduction_attack(func):
  return 1

@dimensionality_reduction_attack
def very_funcy_function():
  ...

>>> very_funcy_function
1

Finally, I put simplest decorator here for comparasion.

def dec3(func):
    print("newfunc")
    def newfunc(*args, **kwargs):
        func(*args, **kwargs)
    return newfunc


@dec3
def func3(a, b):
    print('func3', a, b)

func3(2, 3)
# newfunc
# func3 2 3

Again, decorator is nothing more than X = dec(X), literally and visually.

Don’t use this code

This is LEGAL in Python.

@(lambda call: lambda func: (lambda *args, **kwargs: getattr(func(*args, **kwargs), call)()))('upper')
def greet():
  return 'hello world'

>>> greet()
'HELLO WORLD'