Skip to main content
  1. Readings/
  2. Books/
  3. Fluent Python: Clear, Concise, and Effective Programming – Luciano Ramalho/

Chapter 10. Design Patterns with First-Class Functions

··1090 words·6 mins
  • design pattern: general recipe for solving common design problems

  • language independent; however practically some languages already have inbuilt recipes for some of these patterns

    e.g. Generators in python are the inbuilt version of the Iterator pattern.

  • in the context of languages that support first-class functions, the fact that we can leverage functions as first-class objects is useful to make code simpler.

    the following classic patterns might need a rethink because functions can do the same work as classes while improving readability and reducing boilerplate:

    1. Strategy Pattern

    2. Command Pattern

    3. Template Method

    4. Visitor Pattern

What’s New in This Chapter #

Case Study: Refactoring Strategy #

  • the objective of this case study is to see how we can leverage functions as first-class objects.

Classic Strategy #

  • what it is:
    • “Define a family of algorithms, encapsulate each one, and make them interchangeable. Strategy lets the algorithm vary independently from clients that use it.”
    • participants:
      1. Context

        • Provides a service by delegating some computation to interchangeable components that implement alternative algorithms.
        • in the example, this is the Order
      2. Strategy

        • interface common to the components that implement the different algorithms.

          it’s the Promotion abstract class

      3. Concrete Strategy

        • one of the concrete classes that implement the abstract class

Function-Oriented Strategy #

  • useful characteristics form the class-based implementation:

    1. concrete strategies have a single useful method

    2. strategy instances are stateless and hold no instance attributes

    It’s because of these reasons, we can consider replacing the concrete strategies with simple functions, and removing the abstract class.

  • “strategy objects often make good flyweights” is the advice, wherein the cons of the Strategy pattern, which is its runtime cost (e.g. when instantiating the strategy) is addressed by using a Flyweight pattern.

    now we end up getting more boilerplate

  • the python way of using first class functions works well in general because:

    • in most cases, concrete strategies don’t need to hold internal state because they deal with data injected by the context \(\implies\) good enough to use plain old functions.

    • a function is more lightweight than an instance of a user-defined class + we can just create each function once and use it.

Choosing the Best Strategy using MetaStrategy: Simple Approach #

  • Once you get used to the idea that functions are first-class objects, it naturally follows that building data structures holding functions often makes sense.

Finding Strategies in a Module #

  • Modules are also first-class objects

  • globals() returns the current global symbol table. We can inspect attributes of the class object and get the function attributes defined within it like so: promos = [func for _, func in inspect.getmembers(promotions, inspect.isfunction)]

  • I see this as a “pull method” almost where we try to pull together attributes that might make sense. Naturally a registration decorator approach makes more sense already so that we can do a “pull method” approach

Decorator-Enhanced Strategy Pattern #

Here’s the example:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
Promotion = Callable[[Order], Decimal]

promos: list[Promotion] = [] # NB: this is a module global

# this is a registration decorator, it simply registers this function
def promotion(promo: Promotion) -> Promotion:
        promos.append(promo)
        return promo

def best_promo(order: Order) -> Decimal:
        """Compute the best discount available"""
        return max(promo(order) for promo in promos)

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

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

@promotion
def large_order(order: Order) -> Decimal:
        """7% discount for orders with 10 or more distinct items"""
        distinct_items = {item.product for item in order.cart}
        if len(distinct_items) >= 10:
                return order.total() * Decimal('0.07')
        return Decimal(0)

Advantages of using this decorator:

  1. promo strategy functions don’t need special names, flexibility in naming

  2. the registration decorator also becomes a highlighting of the purpose of the function being decorated

    • also makes it easy to just comment out the decorator
  3. registration can be done from any other module, anywhere in the system as long as we use the same registering decorator

The Command Pattern #

  • The goal of Command Pattern is to decouple an object that invokes an operation (the invoker) from the provider object that implements it (the receiver).

    put a Command object between the two, implementing an interface with a single method, execute, which calls some method in the receiver to perform the desired operation.

    • Invoker doesn’t need to know the interface of the receiver

    • different receivers can be adapted through different Command subclasses

    • the invoker is configed with a concrete command and calls its execute method to operate it.

  • some pointers from the example:

    • we have commands and command receivers.

      Command receivers are the objects that implement the action specific to a command.

      There can be multiple receivers that may respond to a command.

  • “Commands are an object-oriented repalcement for callbacks”. Nice. Depends on use-case but we could directly implement the callbacks if we want.

    How to use simple callback functions directly?

    Instead of giving the invoker a Command instance, we can simply give it a function. Instead of calling command.execute(), the invoker can just call command(). The MacroCommand can be implemented with a class implementing __call__. Instances of MacroCommand would be callables, each holding a list of functions for future invocation.

  • if we need more complex command usage (e.g. with undo) then we just need to keep necessary state, we could put it within classes like MacroCommand and we can use a closure to hold the internal state of a function between calls.

Chapter Summary #

  • the GOF book’s patterns should be seen as steps in the design process of a system rather than end-points or structures that have to be implemented.

    this will allow us to not mindlessly add in boilerplate or structures that actually would have better ways of getting implemented if we had thought about the language’s idioms

    • In python’s case, functions or callable objects provide a more natural way of implementing callbacks in Python than mimicking the Strategy or the Command patterns

Further Reading #

  • not many options available for python and design patterns in pythonic fashion, there’s a list here in this book

  • see design patterns in python from europython 2011 talk

  • funfact: If functions have a __call__ method, and methods are also callable, do __call__ methods also have a __call__ method?

    YES!!!