5.7. Command — Python - from None to AI
EN: Command
PL: Polecenie
Type: object
The Command design pattern is a behavioral design pattern that turns a request into a stand-alone object that contains all information about the request. This transformation lets you pass requests as a method arguments, delay or queue a request's execution, and support undoable operations.
In Python, we can implement the Command pattern using classes. Here's a simple example:
First, we define a Command class that declares an abstract execute method:
>>> class Command: ... def execute(self): ... pass
Then, we define a ConcreteCommand class that implements the execute method:
>>> class ConcreteCommand(Command): ... def __init__(self, receiver): ... self._receiver = receiver ... ... def execute(self): ... self._receiver.action()
The Receiver class has the actual business logic that should be performed:
>>> class Receiver: ... def action(self): ... print("Receiver action is being performed.")
Finally, we have an Invoker class that calls the command:
>>> class Invoker: ... def __init__(self, command): ... self._command = command ... ... def call(self): ... self._command.execute() ... >>> receiver = Receiver() >>> command = ConcreteCommand(receiver) >>> invoker = Invoker(command) ... >>> invoker.call() Receiver action is being performed.
In this example, the Invoker calls the ConcreteCommand, which performs an action
on the Receiver.
5.7.1. Pattern
Receiver — The Object that will receive and execute the command
Invoker — Which will send the command to the receiver
Command Object — Itself, which implements an execute, or action method, and contains all required information
Client — The main application or module which is aware of the Receiver, Invoker and Commands
GUI Buttons, menus
Macro recording
Multi level undo/redo (See Tutorial)
Networking — send whole command objects across a network, even as a batch
Parallel processing or thread pools
Transactional behaviour — Rollback whole set of commands, or defer till later
Wizards
5.7.2. Problem
>>> class Light: ... def action(self, action: str): ... if action == 'on': ... print('Lights on') ... elif action == 'off': ... print('Lights off') >>> >>> >>> light = Light() >>> >>> light.action('on') Lights on >>> >>> light.action('off') Lights off
5.7.3. Solution
>>> from typing import Protocol >>> >>> >>> class Command(Protocol): ... def execute(self): ... >>> >>> >>> class Tasks: ... commands: list[Command] ... ... def __init__(self): ... self.commands = [] ... ... def add(self, command: Command): ... self.commands.append(command) ... ... def run(self): ... for command in self.commands: ... command.execute() >>> >>> >>> class LightsOnCommand: ... def execute(self): ... print('Lights on') >>> >>> class LightsOffCommand: ... def execute(self): ... print('Lights off') >>> >>> >>> task = Tasks() >>> task.add(LightsOnCommand()) >>> task.add(LightsOffCommand()) >>> task.add(LightsOnCommand()) >>> task.add(LightsOffCommand()) >>> task.add(LightsOnCommand()) >>> task.add(LightsOffCommand()) >>> task.run() Lights on Lights off Lights on Lights off Lights on Lights off
5.7.4. Case Study
Problem:
class Button: label: str def set_label(self, name): self.label = name def get_label(self): return self.label def click(self): ... if __name__ == '__main__': button = Button() button.set_label('My Button') button.click()
Solution:
Command pattern:
from abc import ABC, abstractmethod from dataclasses import dataclass class Command(ABC): @abstractmethod def execute(self) -> None: pass class Button: label: str command: Command def __init__(self, command: Command): self.command = command def set_label(self, name): self.label = name def get_label(self): return self.label def click(self): self.command.execute() class CustomerService: def add_customer(self) -> None: print('Add customer') @dataclass class AddCustomerCommand(Command): service: CustomerService def execute(self) -> None: self.service.add_customer() if __name__ == '__main__': service = CustomerService() command = AddCustomerCommand(service) button = Button(command) button.click() # Add customer
Composite commands (Macros):
from abc import ABC, abstractmethod from dataclasses import dataclass, field class Command(ABC): @abstractmethod def execute(self) -> None: pass class ResizeCommand(Command): def execute(self) -> None: print('Resize') class BlackAndWhiteCommand(Command): def execute(self) -> None: print('Black And White') @dataclass class CompositeCommand(Command): commands: list[Command] = field(default_factory=list) def add(self, command: Command) -> None: self.commands.append(command) def execute(self) -> None: for command in self.commands: command.execute() if __name__ == '__main__': composite = CompositeCommand() composite.add(ResizeCommand()) composite.add(BlackAndWhiteCommand()) composite.execute() # Resize # Black And White
Undoable commands:
from abc import ABC, abstractmethod from dataclasses import dataclass, field class Command(ABC): @abstractmethod def execute(self) -> None: pass class UndoableCommand(Command): @abstractmethod def unexecute(self) -> None: pass @dataclass class History: commands: list[UndoableCommand] = field(default_factory=list) def push(self, command: UndoableCommand) -> None: self.commands.append(command) def pop(self): return self.commands.pop() def size(self) -> int: return len(self.commands) @dataclass class HtmlDocument: content: str = '' def set_content(self, content): self.content = content def get_content(self): return self.content @dataclass class BoldCommand(UndoableCommand): document: HtmlDocument history: History = field(default_factory=History) previous_content: str | None = None def unexecute(self) -> None: self.document.set_content(self.previous_content) def apply(self, content): return f'<b>{content}</b>' def execute(self) -> None: current_content = self.document.get_content() self.previous_content = current_content self.document.set_content(self.apply(current_content)) self.history.push(self) @dataclass class UndoCommand(Command): history: History def execute(self) -> None: if self.history.size() > 0: self.history.pop().unexecute() if __name__ == '__main__': history = History() document = HtmlDocument('Hello World') # This should be onButtonClick or KeyboardShortcut BoldCommand(document, history).execute() print(document.get_content()) # <b>Hello World</b> # This should be onButtonClick or KeyboardShortcut UndoCommand(history).execute() print(document.get_content()) # Hello World
5.7.5. Further Reading
5.7.6. Use Case - 1
>>> from typing import Protocol >>> >>> >>> class Command(Protocol): ... def execute(self): """Execute command""" >>> >>> >>> class MorseCode: ... commands: list[Command] ... ... def __init__(self): ... self.commands = [] ... ... def add(self, command: Command): ... self.commands.append(command) ... ... def send(self): ... print('Sending message:') ... for command in self.commands: ... command.execute() ... print('STOP')
>>> class A: ... def execute(self): ... print('.-', end=' ') >>> >>> class B: ... def execute(self): ... print('-...', end=' ') >>> >>> class C: ... def execute(self): ... print('-.-.', end=' ') >>> >>> class D: ... def execute(self): ... print('-..', end=' ') >>> >>> class E: ... def execute(self): ... print('.', end=' ') >>> >>> class F: ... def execute(self): ... print('..-.', end=' ') >>> >>> class G: ... def execute(self): ... print('--.', end=' ') >>> >>> class H: ... def execute(self): ... print('....', end=' ') >>> >>> class I: ... def execute(self): ... print('..', end=' ') >>> >>> class J: ... def execute(self): ... print('.---', end=' ') >>> >>> class K: ... def execute(self): ... print('-.-', end=' ') >>> >>> class L: ... def execute(self): ... print('.-..', end=' ') >>> >>> class M: ... def execute(self): ... print('--', end=' ') >>> >>> class N: ... def execute(self): ... print('-.', end=' ') >>> >>> class O: ... def execute(self): ... print('---', end=' ') >>> >>> class P: ... def execute(self): ... print('.--.', end=' ') >>> >>> class Q: ... def execute(self): ... print('--.-', end=' ') >>> >>> class R: ... def execute(self): ... print('.-.', end=' ') >>> >>> class S: ... def execute(self): ... print('...', end=' ') >>> >>> class T: ... def execute(self): ... print('-', end=' ') >>> >>> class U: ... def execute(self): ... print('..-', end=' ') >>> >>> class V: ... def execute(self): ... print('...-', end=' ') >>> >>> class W: ... def execute(self): ... print('.--', end=' ') >>> >>> class X: ... def execute(self): ... print('-..-', end=' ') >>> >>> class Y: ... def execute(self): ... print('-.--', end=' ') >>> >>> class Z: ... def execute(self): ... print('--..', end=' ')
>>> message = MorseCode() >>> >>> message.add(S()) >>> message.add(O()) >>> message.add(S()) >>> >>> message.send() Sending message: ... --- ... STOP