python design pattern

1. Creation mode

1.1. Factory mode

  • Explanation:
    • Factory method, which is a method (or a function in authentic Python terms), returns different objects for different input parameters
    • If you find it impossible to track these objects because the code for creating objects is distributed in many different places, rather than just in one function / method, you should consider using the factory method pattern. Factory methods focus on creating objects in one place, making object tracking easier.
    • There are factory functions that determine which class to use
  • example:
    import xml.etree.ElementTree as etree
    import json
    
    # The JSON connector class parses the JSON file through parsed_ The data () method returns data in the form of a dictionary (dict).
    # Modifier property parsed_data() looks more like a regular variable than a method
    class JSONConnector:
        def __init__(self, filepath):
            self.data = dict()
            with open(filepath, mode='r', encoding='utf-8') as f:
                self.data = json.load(f)
    
        @property
        def parsed_data(self):
            return self.data
    
    # The XMLConnector class parses xml data
    class XMLConnector:
        def __init__(self, filepath):
            self.tree = etree.parse(filepath)
    
        @property
        def parsed_data(self):
            return self.tree
    
    # Function connection_factory is a factory method,
    # Returns an instance of JSONConnector or XMLConnector based on the extension of the input file path
    def connection_factory(filepath):
        if filepath.endswith('json'):
            connector = JSONConnector
        elif filepath.endswith('xml'):
            connector = XMLConnector
        else:
            raise ValueError('Cannot connect to {}'.format(filepath))
        return connector(filepath)
    
    # Function connect_to() pair connection_factory() is wrapped and exception handling is added
    def connect_to(filepath):
        factory = None
        try:
            factory = connection_factory(filepath)
        except ValueError as ve:
            print(ve)
        return factory
    
    
    # Note that although JSONConnector and XMLConnector have the same interface,
    # But for parsed_ The data returned by data () is not processed in a unified way.
    # For each connector, you need to use different Python code to handle it.
    def main():
        sqlite_factory = connect_to('data/person.sq3')
        print()
    
        xml_factory = connect_to('data/person.xml')
        xml_data = xml_factory.parsed_data
        liars = xml_data.findall(".//{}[{}='{}']".format('person',
                                                         'lastName', 'Liar'))
        print('found: {} persons'.format(len(liars)))
        for liar in liars:
            print('first name: {}'.format(liar.find('firstName').text))
            print('last name: {}'.format(liar.find('lastName').text))
            [print('phone number ({})'.format(p.attrib['type']),
                   p.text) for p in liar.find('phoneNumbers')]
    
        print()
    
        json_factory = connect_to('data/donut.json')
        json_data = json_factory.parsed_data
        print('found: {} donuts'.format(len(json_data)))
        for donut in json_data:
            print('name: {}'.format(donut['name']))
            print('price: ${}'.format(donut['ppu']))
            [print('topping: {} {}'.format(t['id'], t['type'])) for t in donut['topping']]
    
    if __name__ == '__main__':
        main()
    

1.2. Abstract factory pattern

  • Explanation:
    • An abstract factory is (logically) a set of factory methods, each of which is responsible for generating different kinds of objects
    • The factory method is usually used at the beginning because it is simpler. If it is later found that the application needs many factory methods, it is more reasonable to combine the process of creating a series of objects, so as to finally introduce the abstract factory.
    • A factory class is abstracted to handle all back-end classes. An entry determines which back-end class to use
  • example:
    # Frog frog encounters Bug bug and eats it
    class Frog:
        def __init__(self, name):
            self.name = name
    
        def __str__(self):
            return self.name
    
        # This simulates the interaction between the Frog and Bug classes
        def interact_with(self, obstacle):
            print('{} the Frog encounters {} and {}!'.format(self,
            obstacle, obstacle.action()))
    
    class Bug:
        def __str__(self):
            return 'a bug'
    
        def action(self):
            return 'eats it'
    
    # The class FrogWorld is an abstract factory whose main responsibility is to create the protagonists and obstacles of the game.
    # Distinguish creation methods and make their names generic (for example, make_character() and make)_ obstacle()),
    # This allows us to dynamically change the currently active factory (and therefore the currently active game) without any code changes.
    class FrogWorld:
        def __init__(self, name):
            print(self)
            self.player_name = name
    
        def __str__(self):
            return '\n\n\t------ Frog World -—'
    
        def make_character(self):
            return Frog(self.player_name)
    
        def make_obstacle(self):
            return Bug()
    
    # Wizard
    class Wizard:
        def __init__(self, name):
            self.name = name
    
        def __str__(self):
            return self.name
    
        # Interaction between Wizard and orc
        def interact_with(self, obstacle):
            print('{} the Wizard battles against {} and {}!'.format(self, obstacle, obstacle.action()))
    
    # Monster Ork
    class Ork:
        def __str__(self):
            return 'an evil ork'
    
        def action(self):
            return 'kills it'
    
    # Abstract factory, generate instance
    class WizardWorld:
        def __init__(self, name):
            print(self)
            self.player_name = name
    
        def __str__(self):
            return '\n\n\t------ Wizard World -—'
    
        def make_character(self):
            return Wizard(self.player_name)
    
        def make_obstacle(self):
            return Ork()
    
    # Class GameEnvironment is the main entrance of our game. It accepts factory as input,
    # Use it to create the world of games. The method play() will start the interaction between hero and observer
    class GameEnvironment:
        def __init__(self, factory):
            self.hero = factory.make_character()
            self.obstacle = factory.make_obstacle()
    
        def play(self):
            self.hero.interact_with(self.obstacle)
    
    # Function validate_age() prompts the user to provide a valid age. If the age is invalid, a tuple is returned with the first element set to False.
    # If the age is OK, the first element of the element is set to True, but what we really care about is the second element of the element, that is, the age provided by the user,
    def validate_age(name):
        try:
            age = input('Welcome {}. How old are you? '.format(name))
            age = int(age)
        except ValueError as err:
            print("Age {} is invalid, please try \
            again...".format(age))
            return (False, age)
        return (True, age)
    
    # This function requests the user's name and age, and determines which game to play according to the user's age
    def main():
        name = input("Hello. What's your name? ")
        valid_input = False
        while not valid_input:
            valid_input, age = validate_age(name)
        game = FrogWorld if age < 18 else WizardWorld
        environment = GameEnvironment(game(name))
        environment.play()
    
    if __name__ == '__main__':
        main()
    

1.3. Builder pattern

  • explain
    • Create an object composed of multiple parts, and its composition needs to be completed step by step. This object is complete only when all parts are created. This is where the builder design pattern comes into play.
    • The builder pattern separates the construction process of a complex object from its representation, so that the same construction process can be used to create multiple different representations
    • There are builders and commanders
  • Example 1 - configuring a computer:
    # coding: utf-8
    
    class Computer:
        def __init__(self, serial_number):
            self.serial = serial_number
            self.memory = None      # Unit: GB
            self.hdd = None         # Unit: GB
            self.gpu = None
    
        def __str__(self):
            info = ('Memory: {}GB'.format(self.memory),
                    'Hard Disk: {}GB'.format(self.hdd),
                    'Graphics Card: {}'.format(self.gpu))
            return '\n'.join(info)
    
    # A builder computer builder and a commander hardware engineer are introduced
    # And the process of assembling a computer step by step, so that different configurations are now supported
    class ComputerBuilder:
        def __init__(self):
            self.computer = Computer('AG23385193')
    
        def configure_memory(self, amount):
            self.computer.memory = amount
    
        def configure_hdd(self, amount):
            self.computer.hdd = amount
    
        def configure_gpu(self, gpu_model):
            self.computer.gpu = gpu_model
    
    
    class HardwareEngineer:
        def __init__(self):
            self.builder = None
    
        def construct_computer(self, memory, hdd, gpu):
            self.builder = ComputerBuilder()
            [step for step in (self.builder.configure_memory(memory),
                               self.builder.configure_hdd(hdd),
                               self.builder.configure_gpu(gpu))]
    
        @property
        def computer(self):
            return self.builder.computer
    
    
    def main():
        engineer = HardwareEngineer()
        engineer.construct_computer(hdd=500, memory=8, gpu='GeForce GTX 650 Ti')
        computer = engineer.computer
        print(computer)
    
    if __name__ == '__main__':
        main()
    
  • Example 2 - toast:
    # coding: utf-8
    
    from enum import Enum  # enumeration
    import time
    
    PizzaProgress = Enum('PizzaProgress', 'queued preparation baking ready') # Preparation stage
    PizzaDough = Enum('PizzaDough', 'thin thick') # Dough
    PizzaSauce = Enum('PizzaSauce', 'tomato creme_fraiche') # flavoring
    PizzaTopping = Enum('PizzaTopping', 'mozzarella double_mozzarella bacon ham mushrooms red_onion oregano') # mixed ingredients
    STEP_DELAY = 3          # Consider an example in seconds
    
    
    class Pizza:
        def __init__(self, name):
            self.name = name
            self.dough = None
            self.sauce = None
            self.topping = []
    
        def __str__(self):
            return self.name
    
        # You don't need to put this here, but the dough is the same, so it's important to improve the code utilization here
        def prepare_dough(self, dough):
            self.dough = dough
            print('preparing the {} dough of your {}...'.format(self.dough.name, self))
            time.sleep(STEP_DELAY)
            print('done with the {} dough'.format(self.dough.name))
    
    # Builder: making Margarita budiler,
    class MargaritaBuilder:
        def __init__(self):
            self.pizza = Pizza('margarita')
            self.progress = PizzaProgress.queued
            self.baking_time = 5        # Consider an example in seconds
    
        def prepare_dough(self):
            self.progress = PizzaProgress.preparation
            self.pizza.prepare_dough(PizzaDough.thin)
    
        def add_sauce(self):
            print('adding the tomato sauce to your margarita...')
            self.pizza.sauce = PizzaSauce.tomato
            time.sleep(STEP_DELAY)
            print('done with the tomato sauce')
    
        def add_topping(self):
            print('adding the topping (double mozzarella, oregano) to your margarita')
            self.pizza.topping.append([i for i in
                                       (PizzaTopping.double_mozzarella, PizzaTopping.oregano)])
            time.sleep(STEP_DELAY)
            print('done with the topping (double mozzarrella, oregano)')
    
        def bake(self):
            self.progress = PizzaProgress.baking
            print('baking your margarita for {} seconds'.format(self.baking_time))
            time.sleep(self.baking_time)
            self.progress = PizzaProgress.ready
            print('your margarita is ready')
    
    # Builder: making cream bacon pizza.
    class CreamyBaconBuilder:
        def __init__(self):
            self.pizza = Pizza('creamy bacon')
            self.progress = PizzaProgress.queued
            self.baking_time = 7        # Consider an example in seconds
    
        def prepare_dough(self):
            self.progress = PizzaProgress.preparation
            self.pizza.prepare_dough(PizzaDough.thick)
    
        def add_sauce(self):
            print('adding the crème fraîche sauce to your creamy bacon')
            self.pizza.sauce = PizzaSauce.creme_fraiche
            time.sleep(STEP_DELAY)
            print('done with the crème fraîche sauce')
    
        def add_topping(self):
            print('adding the topping (mozzarella, bacon, ham, mushrooms, red onion, oregano) to your creamy bacon')
            self.pizza.topping.append([t for t in
                                       (PizzaTopping.mozzarella, PizzaTopping.bacon,
                                        PizzaTopping.ham, PizzaTopping.mushrooms,
                                        PizzaTopping.red_onion, PizzaTopping.oregano)])
            time.sleep(STEP_DELAY)
            print('done with the topping (mozzarella, bacon, ham, mushrooms, red onion, oregano)')
    
        def bake(self):
            self.progress = PizzaProgress.baking
            print('baking your creamy bacon for {} seconds'.format(self.baking_time))
            time.sleep(self.baking_time)
            self.progress = PizzaProgress.ready
            print('your creamy bacon is ready')
    
    # The conductor is the Waiter. The core of Waiter class is construct_pizza method,
    # This method takes a builder as a parameter and performs all the preparation steps of pizza in the correct order.
    class Waiter:
        def __init__(self):
            self.builder = None
    
        def construct_pizza(self, builder):
            self.builder = builder
            [step() for step in (builder.prepare_dough,
                                 builder.add_sauce, builder.add_topping, builder.bake)]
    
        @property
        def pizza(self):
            return self.builder.pizza
    
    # Function validate_style() is similar to validate described in Chapter 1_ The age() function is used to ensure that the user provides valid input,
    # In the current case, this input is a character mapped to a pizza builder; The input character m indicates the use of the margaritabeuilder class,
    # Enter the character c and use the CreamyBaconBuilder class.
    # These mapping relationships are stored in the parameter builder. This function will return a tuple. If the input is valid, the first element of the tuple is set to True, otherwise it is False,
    def validate_style(builders):
        try:
            pizza_style = input('What pizza would you like, [m]argarita or [c]reamy bacon? ')
            builder = builders[pizza_style]()
            valid_input = True
        except KeyError as err:
            print('Sorry, only margarita (key m) and creamy bacon (key c) are available')
            return (False, None)
        return (True, builder)
    
    
    def main():
        builders = dict(m=MargaritaBuilder, c=CreamyBaconBuilder)
        valid_input = False
        while not valid_input:
            valid_input, builder = validate_style(builders)
        print()
        waiter = Waiter()
        waiter.construct_pizza(builder)
        pizza = waiter.pizza
        print()
        print('Enjoy your {}!'.format(pizza))
    
    if __name__ == '__main__':
        main()
    
  • Example 3 - Fluent Builder:
    • The builder pattern variant, which calls the builder method in a chain, is implemented by defining the builder itself as an internal class and returning itself from each of its setter methods. The method build() returns the final object. This model is called fluent builder.
    • code:
      # coding: utf-8
      
      class Pizza:
          def __init__(self, builder):
              self.garlic = builder.garlic
              self.extra_cheese = builder.extra_cheese
      
          def __str__(self):
              garlic = 'yes' if self.garlic else 'no'
              cheese = 'yes' if self.extra_cheese else 'no'
              info = ('Garlic: {}'.format(garlic), 'Extra cheese: {}'.format(cheese))
              return '\n'.join(info)
      
          class PizzaBuilder:
              def __init__(self):
                  self.extra_cheese = False
                  self.garlic = False
      
              def add_garlic(self):
                  self.garlic = True
                  return self
      
              def add_extra_cheese(self):
                  self.extra_cheese = True
                  return self
      
              def build(self):
                  return Pizza(self)
      
      if __name__ == '__main__':
          pizza = Pizza.PizzaBuilder().add_garlic().add_extra_cheese().build()
          print(pizza)
      
      

1.4. Prototype mode

  • Explanation:
    • Prototype design pattern helps us create clones of objects. Its simplest form is a clone() function, which accepts an object as an input parameter and returns a copy of the input object.
    • When we know that some parts of the object will be changed, but we want to keep the original object unchanged, we usually need a copy of the object.
  • Example - description of the book:
    # coding: utf-8
    
    import copy
    from collections import OrderedDict
    
    
    class Book:
        # Only three formal parameters are fixed: name, authors and price,
        # v but with the rest variable length list, the caller can pass in more parameters in the form of keywords (name = value).
        # self.__ dict__. The update (rest) line adds the rest content to the internal dictionary of the Book class and becomes part of it.
        def __init__(self, name, authors, price, **rest):
            '''rest Examples are: publisher, length, label, publication date'''
            self.name = name
            self.authors = authors
            self.price = price      # Unit: USD
            self.__dict__.update(rest)
    
        # We don't know the names of all the added parameters, but we need to access the internal dictionary to apply these parameters to__ str__ In (),
        # Moreover, the contents of the dictionary do not follow any specific order, so an OrderedDict is used to force the elements to be ordered,
        # Otherwise, each program execution will produce different output.
        def __str__(self):
            mylist = []
            ordered = OrderedDict(sorted(self.__dict__.items()))
            for i in ordered.keys():
                mylist.append('{}: {}'.format(i, ordered[i]))
                if i == 'price':
                    mylist.append('$')
                mylist.append('\n')
            return ''.join(mylist)
    
    # Prototype class implements the prototype design pattern. The core of prototype class is the clone() method, which uses the familiar copy Deepcopy() function to complete the real cloning work.
    # But the Prototype class does a little more than support cloning. It includes methods register() and unregister(), which are used to track cloned objects in a dictionary.
    class Prototype:
        def __init__(self):
            self.objects = dict()
    
        def register(self, identifier, obj):
            self.objects[identifier] = obj
    
        def unregister(self, identifier):
            del self.objects[identifier]
    
        def clone(self, identifier, **attr):
            found = self.objects.get(identifier)
            if not found:
                raise ValueError('Incorrect object identifier: {}'.format(identifier))
            obj = copy.deepcopy(found)
            obj.__dict__.update(attr)
            return obj
    
    
    def main():
        b1 = Book('The C Programming Language', ('Brian W. Kernighan', 'Dennis M.Ritchie'), price=118, publisher='Prentice Hall',
                  length=228, publication_date='1978-02-22', tags=('C', 'programming', 'algorithms', 'data structures'))
    
        prototype = Prototype()
        cid = 'k&r-first'
        prototype.register(cid, b1)
        b2 = prototype.clone(cid, name='The C Programming Language(ANSI)', price=48.99,
                             length=274, publication_date='1988-04-01', edition=2)
    
        for i in (b1, b2):
            print(i)
        print('ID b1 : {} != ID b2 : {}'.format(id(b1), id(b2)))
    
    if __name__ == '__main__':
        main()
    

2. Structural mode

Simple memory: appearance (decorator) agent (controller) sharing element (adapter)

2.1. Appearance mode

  • explain
    • The facade pattern helps to hide the internal complexity of the system and expose the necessary parts to the client through a simplified interface
    • The most common reason to use appearance patterns is to provide a single simple entry point for a complex system. With the appearance introduced, client code can use a system by simply calling a method / function. At the same time, the internal system will not lose any functions, and the appearance just encapsulates the internal system.
  • example
    # coding: utf-8
    
    from enum import Enum
    from abc import ABCMeta, abstractmethod
    
    State = Enum('State', 'new running sleeping restart zombie')
    
    
    class User:
        pass
    
    
    class Process:
        pass
    
    
    class File:
        pass
    
    
    class Server(metaclass=ABCMeta):
        @abstractmethod
        def __init__(self):
            pass
    
        def __str__(self):
            return self.name
    
        @abstractmethod
        def boot(self):
            pass
    
        @abstractmethod
        def kill(self, restart=True):
            pass
    
    
    class FileServer(Server):
        def __init__(self):
            '''Initialize the operation required by the file service process'''
            self.name = 'FileServer'
            self.state = State.new
    
        def boot(self):
            print('booting the {}'.format(self))
            '''Action required to start the file service process'''
            self.state = State.running
    
        def kill(self, restart=True):
            print('Killing {}'.format(self))
            '''Kill the operation required by the file service process'''
            self.state = State.restart if restart else State.zombie
    
        def create_file(self, user, name, permissions):
            '''Check the validity of access rights, user rights, and so on'''
    
            print("trying to create the file '{}' for user '{}' with permissions {}".format(name, user, permissions))
    
    
    class ProcessServer(Server):
        def __init__(self):
            '''Initialize the operation required by the process service process'''
            self.name = 'ProcessServer'
            self.state = State.new
    
        def boot(self):
            print('booting the {}'.format(self))
            '''Action required to start the process service process'''
            self.state = State.running
    
        def kill(self, restart=True):
            print('Killing {}'.format(self))
            '''Kill the operation required by the process service process'''
            self.state = State.restart if restart else State.zombie
    
        def create_process(self, user, name):
            '''Check user permissions and generate PID,wait'''
    
            print("trying to create the process '{}' for user '{}'".format(name, user))
    
    
    class WindowServer:
        pass
    
    
    class NetworkServer:
        pass
    
    
    class OperatingSystem:
        '''appearance'''
        def __init__(self):
            self.fs = FileServer()
            self.ps = ProcessServer()
    
        def start(self):
            [i.boot() for i in (self.fs, self.ps)]
    
        def create_file(self, user, name, permissions):
            return self.fs.create_file(user, name, permissions)
    
        def create_process(self, user, name):
            return self.ps.create_process(user, name)
    
    
    def main():
        os = OperatingSystem()
        os.start()
        os.create_file('foo', 'hello', '-rw-r-r')
        os.create_process('bar', 'ls /tmp')
    
    if __name__ == '__main__':
        main()
    

2.2. Decorator mode

  • explain
    • A python modifier is a specific change to Python syntax that extends the behavior of a class, method, or function without using inheritance.
  • introduce:
    ## 1. Direct use of recursion, function execution time
    def fibonacci(n):
        assert(n >= 0), 'n must be >= 0'
        return n if n in (0, 1) else fibonacci(n-1) + fibonacci(n-2)
    
    if __name__ == '__main__':
        from timeit import Timer
        t = Timer('fibonacci(8)', 'from __main__ import fibonacci')
        print(t.timeit())
    # Results more than 16s
    
    ## Cache calculation results using dict
    known = {0:0, 1:1}
    def fibonacci(n):
        assert(n >= 0), 'n must be >= 0'
        if n in known:
            return known[n]
        res = fibonacci(n-1) + fibonacci(n-2)
        known[n] = res
        return res
    
    if __name__ == '__main__':
        from timeit import Timer
        t = Timer('fibonacci(100)', 'from __main__ import fibonacci')
        print(t.timeit())
    # Results: more than 0.3s
    
    
    ## Use decorator
    
    # Functools Wraps () is a function that facilitates the creation of modifiers; Although not mandatory, it is recommended,
    # Because it can keep the document and signature of the modified function. In this case, the parameter list * args is required, because the modified function may have input parameters.
    import functools
    def memoize(fn):
        known = dict()
    
        @functools.wraps(fn)
        def memoizer(*args):
            if args not in known:
                known[args] = fn(*args)
            return known[args]
        return memoizer
    
  • example:
    # coding: utf-8
    
    import functools
    
    def memoize(fn):
        known = dict()
    
        @functools.wraps(fn)
        def memoizer(*args):
            if args not in known:
                known[args] = fn(*args)
            return known[args]
    
        return memoizer
    
    
    @memoize
    def nsum(n):
        '''Before return n Sum of two numbers'''
        assert(n >= 0), 'n must be >= 0'
        return 0 if n == 0 else n + nsum(n-1)
    
    
    @memoize
    def fibonacci(n):
        '''Returns the number of the Fibonacci sequence n number'''
        assert(n >= 0), 'n must be >= 0'
        return n if n in (0, 1) else fibonacci(n-1) + fibonacci(n-2)
    
    if __name__ == '__main__':
        from timeit import Timer
        measure = [{'exec': 'fibonacci(100)', 'import': 'fibonacci',
                    'func': fibonacci}, {'exec': 'nsum(200)', 'import': 'nsum',
                                         'func': nsum}]
        for m in measure:
            t = Timer('{}'.format(m['exec']), 'from __main__ import \
                {}'.format(m['import']))
            print('name: {}, doc: {}, executing: {}, time: \
                {}'.format(m['func'].__name__, m['func'].__doc__,
                           m['exec'], t.timeit()))
    

2.3. proxy pattern

  • Explanation:
    • Remote proxy: the local agent of objects that actually exist in different address spaces (for example, a network server).
    • Virtual agent: it is used for lazy initialization to delay the creation of a large amount of computation object until it is really needed.
    • Protection / protection agent: control access to sensitive objects.
    • Smart (Reference) proxy: performs additional actions when an object is accessed. Examples of such proxies include reference counting and
      Thread safety check.
  • Code 1: lazy initialization
    # coding: utf-8
    
    """
    LazyProperty Class is actually a descriptor (see the web page)[t.cn/RqrYBND]). Descriptor( descriptor)
    yes Python Overriding class property access methods in(__get__(),__set__()and__delete__())The default behavior is to make
     A recommendation mechanism used. LazyProperty Class only overrides__set__(),Because this is the only accessor it needs to override
     Law. In other words, we don't need to override all access methods.__get__()The property value accessed by the method is exactly what the underlying method wants
     The value to assign and use setattr()To assign values manually.__get__()The actual thing to do is to use values to
     Alternative methods! This means that not only are features loaded lazily, but they can only be set once.
    """
    
    class LazyProperty:
        def __init__(self, method):
            self.method = method
            self.method_name = method.__name__
            # print('function overriden: {}'.format(self.method))
            # print("function's name: {}".format(self.method_name))
    
        def __get__(self, obj, cls):
            if not obj:
                return None
            value = self.method(obj) # Get the value of method attribute of obj object?
            # print('value {}'.format(value))
            setattr(obj, self.method_name, value)
            return value
    
    
    class Test:
        def __init__(self):
            self.x = 'foo'
            self.y = 'bar'
            self._resource = None
    
        @LazyProperty
        def resource(self):
            print('initializing self._resource which is: {}'.format(self._resource))
            self._resource = tuple(range(5))    # Costly
            return self._resource
    
    
    def main():
        t = Test()
        print(t.x)
        print(t.y)
        # Do more...
        print(t.resource)
        print(t.resource)
    
    if __name__ == '__main__':
        main()
    
  • python file descriptor (file descriptor in some books)
    • reference resources: https://blog.csdn.net/hsc_1/article/details/81027546
class A:
    def __init__(self, val):
        self.val = val
    def __get__(self, instance, owner):
        print('self is ', self)
        print('instance is ', instance)
        print('owner is ', owner)
        return self.val

class B:
    a = A(10)

b = B()
print(b.a)

# descriptor
#     Defined in a class__ set__, __get__ And__ delete__ One or more of them are descriptor s.
#     __ get__ Three parameters are required for function definition, namely self, instance and owner.
#     __ set__ Three parameters are required for function definition, namely self, instance and value.

# Understand the following sentence to understand the essence of the file descriptor:
# self is very simple, which is an example of descriptor;
# Instance is the instance in the class where the descriptor is located;
# owner refers to the class of descriptor.

# It can also be seen that the printed b.a is not an instance object, but by calling a__ get__ The value returned by the () function.

# So I think it's more appropriate for this descriptor to be called a descriptor...

# Because this descriptor seems to be used to describe the instance...

# Summary: call the descriptor object (b.a) through the instance, and the descriptor object is not returned (you can try to print an instance object,
# See what is returned), but the descriptor call__ get__ So this a is like the descriptor of b.


class LazyProperty(object):
    def __init__(self, func):
        self.func = func

    def __get__(self, instance, owner):
        if instance is None:
            return self
        else:
            value = self.func(instance)
            setattr(instance, self.func.__name__, value)
            return value

class A:
    @LazyProperty
    def func(self):
        time.sleep(1)
        return 10
  • Code 2:
    # coding: utf-8
    
    """
    View user list: this operation does not require special permissions.
    Add new user: this operation requires the client to provide a special password.
    """
    
    class SensitiveInfo:
        def __init__(self):
            self.users = ['nick', 'tom', 'ben', 'mike']
    
        def read(self):
            print('There are {} users: {}'.format(len(self.users), ' '.join(self.users)))
    
        def add(self, user):
            self.users.append(user)
            print('Added user {}'.format(user))
    
    class Info:
        '''SensitiveInfo Protection agent for'''
    
        def __init__(self):
            self.protected = SensitiveInfo()
            self.secret = '0xdeadbeef'
    
        def read(self):
            self.protected.read()
    
        def add(self, user):
            sec = input('what is the secret? ')
            self.protected.add(user) if sec == self.secret else print("That's wrong!")
    
    def main():
        info = Info()
        while True:
            print('1. read list |==| 2. add user |==| 3. quit')
            key = input('choose option: ')
            if key == '1':
                info.read()
            elif key == '2':
                name = input('choose username: ')
                info.add(name)
            elif key == '3':
                exit()
            else:
                print('unknown option: {}'.format(key))
    
    if __name__ == '__main__':
        main()
    

2.4. Controller mode

  • Full name: model view controller mode
  • Explanation:
    • The principle of Separation of Concerns (SoC) is one of the design principles related to software engineering. The idea behind the SoC principle is to divide an application into different parts, and each part solves a separate concern. The layers in hierarchical design (data access layer, business logic layer, presentation layer, etc.) are examples of concerns.
  • code
    """
    The user enters a number and then can see the famous quotes related to this number.
    Celebrity quotes are stored in a quotes Tuple. This data is usually stored in databases, files or other places,
    Only the model can access it directly.
    """
    
    quotes = ('A man is not complete until he is married. Then he is finished.',
              'As I said before, I never repeat myself.',
              'Behind a successful man is an exhausted woman.',
              'Black holes really suck...', 'Facts are stubborn things.')
    
    
    class QuoteModel:
        def get_quote(self, n):
            try:
                value = quotes[n]
            except IndexError as err:
                value = 'Not found!'
            return value
    
    
    class QuoteTerminalView:
        def show(self, quote):
            print('And the quote is: "{}"'.format(quote))
    
        def error(self, msg):
            print('Error: {}'.format(msg))
    
        def select_quote(self):
            return input('Which quote number would you like to see?')
    
    
    class QuoteTerminalController:
        def __init__(self):
            self.model = QuoteModel()
            self.view = QuoteTerminalView()
    
        def run(self):
            valid_input = False
            while not valid_input:
                n = self.view.select_quote()
                try:
                    n = int(n)
                except ValueError as err:
                    self.view.error("Incorrect index '{}'".format(n))
                else:
                    valid_input = True
            quote = self.model.get_quote(n)
            self.view.show(quote)
    
    
    def main():
        controller = QuoteTerminalController()
        while True:
            controller.run()
    
    if __name__ == '__main__':
        main()
    

2.5. Sharing element mode

  • Explanation:
    • The shared element design pattern minimizes memory usage and improves performance by introducing data sharing for similar objects
    • A Flyweight is a shared object that contains state independent immutable (also known as inherent) data. Variable (also known as non inherent) data that depends on the state should not be part of the sharing element, because this information of each object is different and cannot be shared. If the shared element requires non inherent data, it should be explicitly provided by the client code
    • For example, suppose we are designing a performance critical game, such as first person shooter (FPS) game. In FPS games, players (soldiers) share some states, such as external performance and behavior. For example, in the anti terrorist elite game, all soldiers of the same team (anti terrorist elite or terrorists) look the same (external performance). In the same game, all soldiers (of both teams) have some common actions, such as jumping, bow, etc. This means that we can create a shared element to contain all the common data. Of course, soldiers also have many variable data that vary from person to person. These data are not part of the sharing element, such as guns, health status and geographical location.
  • condition
    • Applications need to use a large number of objects.
    • There are too many objects and the cost of storing / rendering them is too high. Once the variable state in the object is removed (because it should be explicitly passed to the shared element by the client code when needed), multiple groups of different objects can be replaced by relatively fewer shared objects.
    • The object ID is not important for the application. Object sharing will cause the failure of ID comparison, so you can't rely on object ID (those objects that look different in client code will eventually have the same ID).
  • code
    # coding: utf-8
    """
    Construct a small forest of fruit trees, small enough to ensure that the entire output is read in a single terminal page.
    However, no matter how large the forest you construct, the memory allocation remains the same.
    """
    
    import random
    from enum import Enum
    
    # enumeration
    TreeType = Enum('TreeType', 'apple_tree cherry_tree peach_tree')
    
    
    class Tree:
        # pool is a class attribute (a variable shared by all instances of the class), (in other words, our cache)
        pool = dict()
    
        # Use special methods__ new__ (this method is called before _init _),
        # We transform the Tree class into a metaclass, which supports self reference. This means that cls refers to the Tree class.
        # When the client wants to create an instance of a Tree, it will use the Tree_ The type parameter passes the type of Tree.
        # The kind of tree is used to check whether a tree of the same kind has been created. If yes, return the previously created object;
        # Otherwise, add the new tree species to the pool and return the corresponding new object
        def __new__(cls, tree_type):
            obj = cls.pool.get(tree_type, None)
            if not obj:
                obj = object.__new__(cls)  # Instantiation, object Method (class)
                cls.pool[tree_type] = obj  # Add objects to the class's pool
                obj.tree_type = tree_type  # Set the properties of the object
            return obj
            # In metaclass programming, obj is the instantiated object of this class
            # cls is a class
    
        # The method render() is used to render a tree on the screen. Note that all variable (external) information not known to Xiangyuan needs to be explicitly passed by the client code.
        # In the current case, each tree uses a random age and a position in the form of X and y.
        # In order to make render() more useful, it is necessary to ensure that no tree is rendered on top of another tree.
        def render(self, age, x, y):
            print('render a tree of type {} and age {} at ({}, {})'.format(self.tree_type, age, x, y))
    
    
    # The main() function shows how we can use the meta pattern. The age of a tree is a random value between 1 and 30 years.
    # Coordinates use random values between 1 and 100. Although 18 trees were rendered, only 3 trees were allocated memory.
    # The last line of the output proves that we cannot rely on the ID of the object when using shared elements. The function id() returns the memory address of the object.
    # The Python specification does not require id() to return the memory address of the object, but requires id() to return a unique ID for each object,
    # However, CPython (the official implementation of Python) just uses the memory address of the object as the object uniqueness ID.
    # In our example, even if two objects look different, if they belong to the same family of shared elements (in this case, the family is defined by tree_type), they actually have the same ID.
    # Of course, the comparison of different ID S can still be used for objects of different families, but this is only feasible if the client knows the implementation details (usually not).
    def main():
        rnd = random.Random()
        age_min, age_max = 1, 30    # Unit: year
        min_point, max_point = 0, 100
        tree_counter = 0
    
        for _ in range(10):
            t1 = Tree(TreeType.apple_tree)
            t1.render(rnd.randint(age_min, age_max),
                      rnd.randint(min_point, max_point),
                      rnd.randint(min_point, max_point))
            tree_counter += 1
    
        for _ in range(3):
            t2 = Tree(TreeType.cherry_tree)
            t2.render(rnd.randint(age_min, age_max),
                      rnd.randint(min_point, max_point),
                      rnd.randint(min_point, max_point))
            tree_counter += 1
    
        for _ in range(5):
            t3 = Tree(TreeType.peach_tree)
            t3.render(rnd.randint(age_min, age_max),
                      rnd.randint(min_point, max_point),
                      rnd.randint(min_point, max_point))
            tree_counter += 1
    
        print('trees rendered: {}'.format(tree_counter))
        print('trees actually created: {}'.format(len(Tree.pool)))
    
        t4 = Tree(TreeType.cherry_tree)
        t5 = Tree(TreeType.cherry_tree)
        t6 = Tree(TreeType.apple_tree)
        print('{} == {}? {}'.format(id(t4), id(t5), id(t4) == id(t5)))
        print('{} == {}? {}'.format(id(t5), id(t6), id(t5) == id(t6)))
    
    if __name__ == '__main__':
        main()
    

2.6. Adapter mode

  • Explanation:
    • Adapter pattern is a structural design pattern that helps us achieve compatibility between two incompatible interfaces.
    • If we want to use an old component in a new system, or use a new component in an old system, it is rare that the two can communicate without any modification to the code. But it's not always possible to modify the code, either because we can't access it (for example, by providing it in a library outside the component), or because modifying the code itself is impractical. In these cases, we can write an additional code layer that contains all the modifications needed to enable communication between the two interfaces. This code layer is called adapter.
  • example:
    # External module: external py
    class Synthesizer:
        def __init__(self, name):
            self.name = name
    
        def __str__(self):
            return 'the {} synthesizer'.format(self.name)
    
        def play(self):
            return 'is playing an electronic song'
    
    
    class Human:
        def __init__(self, name):
            self.name = name
    
        def __str__(self):
            return '{} the human'.format(self.name)
    
        def speak(self):
            return 'says hello'
    
    
    # Own code adapter py
    from external import Synthesizer, Human
    
    class Computer:
        def __init__(self, name):
            self.name = name
    
        def __str__(self):
            return 'the {} computer'.format(self.name)
    
        def execute(self):
            return 'executes a program'
    
    # The client only knows how to call the execute() method, not play() 4 and speak().
    # Without changing the Synthesizer and Human classes, what can we do to make the code effective? Adapter is the Savior!
    # We create a general Adapter class to adapt some objects with different interfaces to a unified interface.
    # __ init__ The obj parameter of () method is the object we want to adapt, adapted_methods is a dictionary. The key in the key value pair is the method to be called by the client, and the value is the method that should be called.
    class Adapter:
        def __init__(self, obj, adapted_methods):
            self.obj = obj
            self.__dict__.update(adapted_methods)
    
        def __str__(self):
            return str(self.obj)
    
    
    def main():
        objects = [Computer('Asus')]
        synth = Synthesizer('moog')
        objects.append(Adapter(synth, dict(execute=synth.play)))
        human = Human('Bob')
        objects.append(Adapter(human, dict(execute=human.speak)))
    
        for i in objects:
            print('{} {}'.format(str(i), i.execute()))
    
    if __name__ == "__main__":
        main()
    

3. Behavioral model

Simple memory: the observer commands the interpreter to make the responsibility chain and state machine into a policy template

3.1. Observer mode

  • explain
    • The observer pattern describes the publish subscribe relationship between a single object (publisher, also known as moderator or or observable) and one or more objects (subscribers, also known as observers).
    • Subscribers will be notified when the publisher changes
  • code
    # The base class Publisher includes public functions such as adding, deleting and notifying observers.
    # Observers are saved in the list observers. The add() method registers a new observer or raises an error when the observer already exists.
    # The remove() method unregisters an existing observer or raises an error if the observer does not already exist.
    # Finally, the notify() method notifies all observers when changes occur.
    class Publisher:
        def __init__(self):
            self.observers = []
    
        def add(self, observer):
            if observer not in self.observers:
                self.observers.append(observer)
            else:
                print('Failed to add: {}'.format(observer))
    
        def remove(self, observer):
            try:
                self.observers.remove(observer)
            except ValueError:
                print('Failed to remove: {}'.format(observer))
    
        def notify(self):
            [o.exec(self) for o in self.observers]
    
    # The defaultformatter class inherits from Publisher and adds formatter specific functions.
    class DefaultFormatter(Publisher):
        # __ init__ The first thing () does is call the base class__ init__ () method
        # The DefaultFormatter instance has its own name so that we can track its status.
        def __init__(self, name):
            Publisher.__init__(self)
            self.name = name
            self._data = 0
    
        # __ str__ The () method returns information about the publisher name and_ Information about the data value.
        # type(self).__name is a convenient skill to obtain the class name and avoid hard coding the class name.
        def __str__(self):
            return "{}: '{}' has data = {}".format(type(self).__name__, self.name, self._data)
    
        # @property modifier to provide_ Read access mode of data variable. In this way, we can use object Data instead of object data(). 
        @property
        def data(self):
            return self._data
    
        # @setter this modifier will set the assignment operator (=) to_ Called when the data variable is assigned a new value.
        @data.setter
        def data(self, new_value):
            try:
                self._data = int(new_value)
            except ValueError as e:
                print('Error: {}'.format(e))
            else:
                self.notify()
    
    # The functions of the two observers: HexFormatter and BinaryFormatter are very similar.
    # The only difference is how to format the data values obtained from the publisher, that is, in hexadecimal and binary respectively.
    class HexFormatter:
        def exec(self, publisher):
            print("{}: '{}' has now hex data = {}".format(type(self).__name__,
                                                          publisher.name, hex(publisher.data)))
    
    class BinaryFormatter:
        def exec(self, publisher):
            print("{}: '{}' has now bin data = {}".format(type(self).__name__,
                                                          publisher.name, bin(publisher.data)))
    
    def main():
        df = DefaultFormatter('test1')
        print(df)
    
        print()
        hf = HexFormatter()
        df.add(hf)
        df.data = 3
        print(df)
    
        print()
        bf = BinaryFormatter()
        df.add(bf)
        df.data = 21
        print(df)
    
        print()
        df.remove(hf)
        df.data = 40
        print(df)
    
        print()
        df.remove(hf)
        df.add(bf)
        df.data = 'hello'
        print(df)
    
        print()
        df.data = 15.8
        print(df)
    
    if __name__ == '__main__':
        main()
    

3.2. Command mode

  • explain
    • Undo operation is supported
  • code
    import os
    
    verbose = True
    
    # files renaming
    class RenameFile:
        def __init__(self, path_src, path_dest):
            self.src, self.dest = path_src, path_dest
    
        def execute(self):
            if verbose:
                print("[renaming '{}' to '{}']".format(self.src, self.dest))
            os.rename(self.src, self.dest)
    
        def undo(self):
            if verbose:
                print("[renaming '{}' back to '{}']".format(self.dest, self.src))
            os.rename(self.dest, self.src)
    
    # create a file
    class CreateFile:
        def __init__(self, path, txt='hello world\n'):
            self.path, self.txt = path, txt
    
        def execute(self):
            if verbose:
                print("[creating file '{}']".format(self.path))
            with open(self.path, mode='w', encoding='utf-8') as out_file:
                out_file.write(self.txt)
    
        def undo(self):
            delete_file(self.path)
    
    # Rollback is not supported when reading files
    class ReadFile:
        def __init__(self, path):
            self.path = path
    
        def execute(self):
            if verbose:
                print("[reading file '{}']".format(self.path))
            with open(self.path, mode='r', encoding='utf-8') as in_file:
                print(in_file.read(), end='')
    
    # Deleting files also does not support rollback
    def delete_file(path):
        if verbose:
            print("deleting file '{}'".format(path))
        os.remove(path)
    
    
    def main():
        orig_name, new_name = 'file1', 'file2'
    
        commands = []
        for cmd in CreateFile(orig_name), ReadFile(orig_name), RenameFile(orig_name, new_name):
            commands.append(cmd)
    
        [c.execute() for c in commands]
    
        answer = input('reverse the executed commands? [y/n] ')
    
        if answer not in 'yY':
            print("the result is {}".format(new_name))
            exit()
    
        for c in reversed(commands):
            try:
                c.undo()
            except AttributeError as e:
                pass
    
    if __name__ == '__main__':
        main()
    

3.3. Interpreter mode

  • explain
    • The main idea behind the simple language interpreter is to let users express their ideas and non experts
    • I prefer to use yaml format for parsing
  • code
    # coding: utf-8
    
    """
    Create an internal DSL Control a smart house
     The form of an event is command -> receiver -> arguments. The parameters section is optional.
    Not all events require parameters. Examples of events that do not require any parameters are shown below.
    open -> gate
    ->Symbols are used to mark the end of one part of an event and declare the beginning of the next part.
    Pyparsing yes python A module in
    
    Use Bakos-Noel form( Backus-Naur Form,BNF)Notation to define syntax
    event ::= command token receiver token arguments
    command ::= word+
    word ::= a collection of one or more alphanumeric characters
    token ::= ->
    receiver ::= word+
    arguments ::= word+
    
    Actual code:
    word = Word(alphanums)
    command = Group(OneOrMore(word))
    token = Suppress("->")
    device = Group(OneOrMore(word))
    argument = Group(OneOrMore(word))
    event = command + token + device + Optional(token + argument)
    """
    
    from pyparsing import Word, OneOrMore, Optional, Group, Suppress, alphanums
    
    class Gate:
        def __init__(self):
            self.is_open = False
    
        def __str__(self):
            return 'open' if self.is_open else 'closed'
    
        def open(self):
            print('opening the gate')
            self.is_open = True
    
        def close(self):
            print('closing the gate')
            self.is_open = False
    
    
    class Garage:
        def __init__(self):
            self.is_open = False
    
        def __str__(self):
            return 'open' if self.is_open else 'closed'
    
        def open(self):
            print('opening the garage')
            self.is_open = True
    
        def close(self):
            print('closing the garage')
            self.is_open = False
    
    
    class Aircondition:
        def __init__(self):
            self.is_on = False
    
        def __str__(self):
            return 'on' if self.is_on else 'off'
    
        def turn_on(self):
            print('turning on the aircondition')
            self.is_on = True
    
        def turn_off(self):
            print('turning off the aircondition')
            self.is_on = False
    
    
    class Heating:
        def __init__(self):
            self.is_on = False
    
        def __str__(self):
            return 'on' if self.is_on else 'off'
    
        def turn_on(self):
            print('turning on the heating')
            self.is_on = True
    
        def turn_off(self):
            print('turning off the heating')
            self.is_on = False
    
    
    class Boiler:
        def __init__(self):
            self.temperature = 83  # in celsius
    
        def __str__(self):
            return 'boiler temperature: {}'.format(self.temperature)
    
        def increase_temperature(self, amount):
            print("increasing the boiler's temperature by {} degrees".format(amount))
            self.temperature += amount
    
        def decrease_temperature(self, amount):
            print("decreasing the boiler's temperature by {} degrees".format(amount))
            self.temperature -= amount
    
    
    class Fridge:
        def __init__(self):
            self.temperature = 2  # In degrees Celsius
    
        def __str__(self):
            return 'fridge temperature: {}'.format(self.temperature)
    
        def increase_temperature(self, amount):
            print("increasing the fridge's temperature by {} degrees".format(amount))
            self.temperature += amount
    
        def decrease_temperature(self, amount):
            print("decreasing the fridge's temperature by {} degrees".format(amount))
            self.temperature -= amount
    
    
    def main():
        word = Word(alphanums)
        command = Group(OneOrMore(word))
        token = Suppress("->")
        device = Group(OneOrMore(word))
        argument = Group(OneOrMore(word))
        event = command + token + device + Optional(token + argument)
    
        gate = Gate()
        garage = Garage()
        airco = Aircondition()
        heating = Heating()
        boiler = Boiler()
        fridge = Fridge()
    
        tests = ('open -> gate',
                 'close -> garage',
                 'turn on -> aircondition',
                 'turn off -> heating',
                 'increase -> boiler temperature -> 5 degrees',
                 'decrease -> fridge temperature -> 2 degrees')
        open_actions = {'gate': gate.open,
                        'garage': garage.open,
                        'aircondition': airco.turn_on,
                        'heating': heating.turn_on,
                        'boiler temperature': boiler.increase_temperature,
                        'fridge temperature': fridge.increase_temperature}
        close_actions = {'gate': gate.close,
                         'garage': garage.close,
                         'aircondition': airco.turn_off,
                         'heating': heating.turn_off,
                         'boiler temperature': boiler.decrease_temperature,
                         'fridge temperature': fridge.decrease_temperature}
    
        """
        obtain Pyparsing The easiest way to parse the results is to use parseString()Method, which returns a
        individual ParseResults Instance, which is actually a parse tree that can be regarded as a nested list. For example, execute print(event.
        parseString('increase -> boiler temperature -> 3 degrees'))The results obtained are as follows.
        [['increase'], ['boiler', 'temperature'], ['3', 'degrees']]
        """
        for t in tests:
            if len(event.parseString(t)) == 2:  # No parameters
                cmd, dev = event.parseString(t)
                cmd_str, dev_str = ' '.join(cmd), ' '.join(dev)
                if 'open' in cmd_str or 'turn on' in cmd_str:
                    open_actions[dev_str]()
                elif 'close' in cmd_str or 'turn off' in cmd_str:
                    close_actions[dev_str]()
            elif len(event.parseString(t)) == 3:  # With parameters
                cmd, dev, arg = event.parseString(t)
                cmd_str, dev_str, arg_str = ' '.join(cmd), ' '.join(dev), ' '.join(arg)
                num_arg = 0
                try:
                    num_arg = int(arg_str.split()[0])  # Extract value part
                except ValueError as err:
                    print("expected number but got: '{}'".format(arg_str[0]))
                if 'increase' in cmd_str and num_arg > 0:
                    open_actions[dev_str](num_arg)
                elif 'decrease' in cmd_str and num_arg > 0:
                    close_actions[dev_str](num_arg)
    
    if __name__ == '__main__':
        main()
    

3.4. Responsibility chain model

  • explain
    • (1) There is an object chain (linked list, tree, or any other convenient data structure).
    • (2) We start by sending the request to the first object in the chain.
    • (3) Object determines whether it wants to process the request.
    • (4) Object forwards the request to the next object.
    • (5) Repeat the process until the end of the chain is reached.
  • example:
    #!/usr/bin/python
    #coding:utf8
    
    class Handler:
        def successor(self, successor):
            self.successor = successor
    
    class ConcreteHandler1(Handler):
        def handle(self, request):
            if request > 0 and request <= 10:
                print("in handler1")
            else:
                self.successor.handle(request)
    
    class ConcreteHandler2(Handler):
        def handle(self, request):
            if request > 10 and request <= 20:
                print("in handler2")
            else:
                self.successor.handle(request)
    
    class ConcreteHandler3(Handler):
        def handle(self, request):
            if request > 20 and request <= 30:
                print("in handler3")
            else:
                print('end of chain, no handler for {}'.format(request))
    
    class Client:
        def __init__(self):
            h1 = ConcreteHandler1()
            h2 = ConcreteHandler2()
            h3 = ConcreteHandler3()
    
            h1.successor(h2)
            h2.successor(h3)
    
            requests = [2, 5, 14, 22, 18, 3, 35, 27, 20]
            for request in requests:
                h1.handle(request)
    
    if __name__ == "__main__":
        client = Client()
    

3.5. State mode

  • explain
    • state
    • state transition
    • Process status
  • Code 1: use state_machine module
    from state_machine import State, Event, acts_as_state_machine, after, before, InvalidStateTransition
    
    @acts_as_state_machine
    class Process:
        created = State(initial=True)
        waiting = State()
        running = State()
        terminated = State()
        blocked = State()
        swapped_out_waiting = State()
        swapped_out_blocked = State()
    
        wait = Event(from_states=(created, running, blocked,
                                  swapped_out_waiting), to_state=waiting)
        run = Event(from_states=waiting, to_state=running)
        terminate = Event(from_states=running, to_state=terminated)
        block = Event(from_states=(running, swapped_out_blocked),
                      to_state=blocked)
        swap_wait = Event(from_states=waiting, to_state=swapped_out_waiting)
        swap_block = Event(from_states=blocked, to_state=swapped_out_blocked)
    
        def __init__(self, name):
            self.name = name
    
        @after('wait')
        def wait_info(self):
            print('{} entered waiting mode'.format(self.name))
    
        @after('run')
        def run_info(self):
            print('{} is running'.format(self.name))
    
        @before('terminate')
        def terminate_info(self):
            print('{} terminated'.format(self.name))
    
        @after('block')
        def block_info(self):
            print('{} is blocked'.format(self.name))
    
        @after('swap_wait')
        def swap_wait_info(self):
            print('{} is swapped out and waiting'.format(self.name))
    
        @after('swap_block')
        def swap_block_info(self):
            print('{} is swapped out and blocked'.format(self.name))
    
    def transition(process, event, event_name):
        try:
            event()
        except InvalidStateTransition as err:
            print('Error: transition of {} from {} to {} failed'.format(process.name,
                                                                        process.current_state, event_name))
    def state_info(process):
        print('state of {}: {}'.format(process.name, process.current_state))
    
    def main():
        RUNNING = 'running'
        WAITING = 'waiting'
        BLOCKED = 'blocked'
        TERMINATED = 'terminated'
    
        p1, p2 = Process('process1'), Process('process2')
        [state_info(p) for p in (p1, p2)]
    
        print()
        transition(p1, p1.wait, WAITING)
        transition(p2, p2.terminate, TERMINATED)
        [state_info(p) for p in (p1, p2)]
    
        print()
        transition(p1, p1.run, RUNNING)
        transition(p2, p2.wait, WAITING)
        [state_info(p) for p in (p1, p2)]
    
        print()
        transition(p2, p2.run, RUNNING)
        [state_info(p) for p in (p1, p2)]
    
        print()
        [transition(p, p.block, BLOCKED) for p in (p1, p2)]
        [state_info(p) for p in (p1, p2)]
    
        print()
        [transition(p, p.terminate, TERMINATED) for p in (p1, p2)]
        [state_info(p) for p in (p1, p2)]
    
    if __name__ == '__main__':
        main()
    
  • Code 2: native implementation
    """
    The state pattern allows us to change an object's behavior at runtime
    — something that Python naturally excels at!
    This example has a very simple radio. It has an AM/FM toggle switch,
    and a scan button to scan to the next station.
    """
    
    class State(object):
       """Base state. This is to share functionality"""
    
       def scan(self):
           """Scan the dial to the next station"""
           self.pos += 1
           if self.pos == len(self.stations):
               self.pos = 0
           print "Scanning... Station is", self.stations[self.pos], self.name
    
    class AmState(State):
       def __init__(self, radio):
           self.radio = radio
           self.stations = ["1250", "1380", "1510"]
           self.pos = 0
           self.name = "AM"
    
       def toggle_amfm(self):
           print "Switching to FM"
           self.radio.state = self.radio.fmstate
    
    class FmState(State):
       def __init__(self, radio):
           self.radio = radio
           self.stations = ["81.3", "89.1", "103.9"]
           self.pos = 0
           self.name = "FM"
    
       def toggle_amfm(self):
           print "Switching to AM"
           self.radio.state = self.radio.amstate
    
    class Radio(object):
       """A radio.
       It has a scan button, and an AM/FM toggle switch."""
    
       def __init__(self):
           """We have an AM state and an FM state"""
    
           self.amstate = AmState(self)
           self.fmstate = FmState(self)
           self.state = self.amstate
    
       def toggle_amfm(self):
           self.state.toggle_amfm()
       def scan(self):
           self.state.scan()
    
    # Test our radio out
    radio = Radio()
    actions = [radio.scan] * 2 + [radio.toggle_amfm] + [radio.scan] * 2
    actions = actions * 2
    for action in actions:
       action()
    
  • Refer to the architecture code of the underlying network in the monitoring system

3.6. Strategy mode

  • explain
    • Decide which strategy to use according to different scenarios, features or variables
  • code
    # coding: utf-8
    # Checks whether all strings in a string are unique
    
    import time
    SLOW = 3  # In seconds
    LIMIT = 5   # Number of characters
    WARNING = 'too bad, you picked the slow algorithm :('
    
    
    def pairs(seq):
        n = len(seq)
        for i in range(n):
            yield seq[i], seq[(i + 1) % n]
    
    # Method 1: sort, and then judge whether the adjacent two elements are equal
    def allUniqueSort(s):
        if len(s) > LIMIT:
            print(WARNING)
            time.sleep(SLOW)
        srtStr = sorted(s)
        for (c1, c2) in pairs(srtStr):
            if c1 == c2:
                return False
        return True
    
    # Method 2: use the collection in python to process
    def allUniqueSet(s):
        if len(s) < LIMIT:
            print(WARNING)
            time.sleep(SLOW)
        return True if len(set(s)) == len(s) else False
    
    
    def allUnique(s, strategy):
        return strategy(s)
    
    
    def main():
        while True:
            word = None
            while not word:
                word = input('Insert word (type quit to exit)> ')
                if word == 'quit':
                    print('bye')
                    return
    
                strategy_picked = None
                strategies = {'1': allUniqueSet, '2': allUniqueSort}
                while strategy_picked not in strategies.keys():
                    strategy_picked = input('Choose strategy: [1] Use a set, [2] Sort and pair> ')
    
                    try:
                        strategy = strategies[strategy_picked]
                        print('allUnique({}): {}'.format(word, allUnique(word, strategy)))
                    except KeyError as err:
                        print('Incorrect option: {}'.format(strategy_picked))
    
    if __name__ == '__main__':
        main()
    

3.7. Template mode

  • explain
    • Reduce code redundancy
    • Implement the invariant part of an algorithm at one time, and leave the variable behavior to subclasses to implement.
  • code
    """
    Send a piece of text to a function that generates a banner containing the text.
    Banners have many styles, such as dots or dashes around text. The banner generator has a default style,
    But we should be able to use our own style.
    """
    
    from cowpy import cow
    
    def dots_style(msg):
        msg = msg.capitalize()
        msg = '.' * 10 + msg + '.' * 10
        return msg
    
    def admire_style(msg):
        msg = msg.upper()
        return '!'.join(msg)
    
    def cow_style(msg):
        msg = cow.milk_random_cow(msg)
        return msg
    
    def generate_banner(msg, style=dots_style):
        print('-- start of banner --')
        print(style(msg))
        print('-- end of banner --\n\n')
    
    
    def main():
        msg = 'happy coding'
        [generate_banner(msg, style) for style in (dots_style, admire_style, cow_style)]
    
    if __name__ == '__main__':
        main()
    

reference resources:
Proficient in python design patterns
Twenty three design patterns and their implementation in python

Tags: Python Design Pattern

Posted by will on Thu, 12 May 2022 06:57:41 +0300