A key feature of decorators is that they run right after the decorated function is defined. This is usually at import time.

What is the output of the following code?

registry = []

def register(func):
    print('running register(%s)' % func)
    registry.append(func)
    return func

@register
def f1():
    print('running f1()')

@register
def f2():
    print('running f2()')

def f3():
    print('running f3()')

def main():
    print('registry ->', registry)
    f1()
    f2()
    f3()

if __name__ == '__main__':
    main()

The result may surprise you.

running register(<function f1 at 0x1055ae378>)
running register(<function f2 at 0x1055ae400>)
registry -> [<function f1 at 0x1055ae378>, <function f2 at 0x1055ae400>]
running f1()
running f2()

Note that register runs twice before any other function in the module. It infers that decorators run right after the decorated function is defined. This is usually at import time, when a module is loaded by Python.

Save the above code in registration.py file and import it as a script:

>>> import registration
running register(<function f1 at 0x104679840>)
running register(<function f2 at 0x1046798c8>)

At this time, if you look at the variable registry, here is that you get:

>>> registration.registry
[<function f1 at 0x104679840>, <function f2 at 0x1046798c8>]

The function decorators are executed as soon as the module is imported, but the decorated functions only run when they are explicitly invoked.

Similar decorators are used in many Python Web frameworks to add functions to some central registry, for example, a registry mapping URL patterns to functions that generate HTTP responses.

Back to Refactoring Strategy, it’s difficult to compute the highest discount applicable. You must append all strategy functions to one list manually.

We can solve this problem with a registration decorator.

promos = []

def promotion(func):
    promos.append(func)
    return func

@promotion
def fidelityPromo(order):
    """
    5% discount for customers with 1000 or more fidelity points
    """
    return order.total() * 0.05 if order.customer.fidelity >= 1000 else 0

@promotion
def bulkItemPromo(order):
    """
    10% discount for each LineItem with 20 or more units
    """
    discount = 0
    for item in order.cart:
        if item.quantity >= 20:
            discount += item.total() * 0.1

    return discount

@promotion
def largeOrderPromo(order):
    """
    7% discount for orders with 10 or more distinct items
    """
    distinctItems = {item.product for item in order.cart}
    if len(distinctItems) >= 10:
        return order.total() * 0.07

    return 0

def bestPromo(order):
    return max(promo(order) for promo in promos)