This is the 16th day of my participation in the More text Challenge. For more details, see more text Challenge

A new column, Developing Python, will share some of my experiences with Python.

Python is a dynamic language, and design patterns are not as rich as Java. Many design patterns languages already support them, such as decorator patterns and iterator patterns. But learning about design patterns, and how they work in Python, can make your code more elegant.

Design patterns are mainly divided into three types: creation, structure and behavior.

All of the code below is in my Github directory.

Create a type

Common creative design patterns:

  • Factory: Solves object creation problems
  • Builder: Controls the creation of complex objects
  • Prototype pattern: Create a new instance from a clone of a Prototype
  • Singleton: A class can create only one object
  • Object Pool pattern: A group of instances of the same type are pre-allocated
  • Lazy Evaluation: Lazy Evaluation (Python property)

The factory pattern

  • Solve the object creation problem
  • Decouple the creation and use of objects
  • Includes factory methods and abstract factories
class Dog:
    def speak(self) :
        print("wang wang")

class Cat:
    def speak(self) :
        print("miao miao")

def animal_factory(name) :
    if name == 'dog':
        return Dog()
    elif name == 'cat':
        return Cat()
Copy the code

Structure mode

  • Used to control the construction of complex objects
  • Separation of creation and presentation. For example, if you want to buy a computer, factory mode gives you the computer you need directly, but construction mode allows you to define the configuration of the computer and gives it to you when it is finished.

Here I simulate a king of glory to create a new hero example.

class Hero:
    def __init__(self, name) :
        self.name = name
        self.blood = None
        self.attack = None
        self.job = None

    def __str__(self) :
        info = ("Name {}".format(self.name), "blood: {}".format(self.blood),
                "attack: {}".format(self.attack), "job: {}".format(self.job))
        return '\n'.join(info)


class HeroBuilder:
    def __init__(self) :
        self.hero = Hero("Monki")

    def configure_blood(self, amount) :
        self.hero.blood = amount

    def configure_attack(self, amount) :
        self.hero.attack = amount

    def configure_job(self, job) :
        self.hero.job = job

class Game:
    def __init__(self) :
        self.builder = None

    def construct_hero(self, blood, attack, job) :
        self.builder = HeroBuilder()
        self.builder.configure_blood(blood)
        self.builder.configure_attack(attack),
        self.builder.configure_job(job)

    @property
    def hero(self) :
        return self.builder.hero

game = Game()
game.construct_hero(5000.200."warrior")
hero = game.hero
print(hero)
Copy the code

The prototype pattern

  • Create a new instance by cloning the prototype
  • You can use the same stereotype to create a new example by modifying some of the properties
  • Purpose: The prototype pattern can be used for places where it is expensive to create instances

The singleton pattern

  • Singleton pattern: All objects created by a class are the same
  • Ptyhon modules are singletons that are imported only once
  • Create the singleton pattern by sharing the same instance
class Singleton:
    def __new__(cls, *args, **kwargs) :
        if not hasattr(cls, '_instance'):
            _instance = super().__new__(cls, *args, **kwargs)
            cls._instance = _instance
        return cls._instance


class MyClass(Singleton) :
    pass

c1 = MyClass()
c2 = MyClass()
print(c1 is c2) # true
Copy the code

In the above method, New is the method that actually creates the instance object, so override the New method of the base class to ensure that only one instance is generated when the object is created.

The singleton pattern is a common question in the interview. There are many other methods besides this implementation. Here are a few more examples:

Using decorators

def singleton(cls) :
    instances = {}
    def wrapper(*args, **kwargs) :
        if cls not in instances:
            instances[cls] = cls(*args, **kwargs)
        return instances[cls]
    return wrapper

@singleton
class Foo:
    pass
foo1 = Foo()
foo2 = Foo()
print(foo1 is foo2)  # True
Copy the code

Use metaclasses. Metaclasses are classes used to create class objects. Class objects must call the call method when creating instance objects, so you can always create one instance when calling call.


class Singleton(type) :
    def __call__(cls, *args, **kwargs) :
        if not hasattr(cls, '_instance'):
            cls._instance = super(Singleton, cls).__call__(*args, **kwargs)
        return cls._instance

class Foo(metaclass=Singleton) :
    pass

foo1 = Foo()
foo2 = Foo()
print(foo1 is foo2)  # True
Copy the code

structured

Common structural design patterns

  • Decorator pattern: Extend object functionality without subclassing
  • Proxy mode: to Proxy the actions of one object to another
  • Adapter pattern: ADAPTS unified interfaces through an indirection layer
  • Facade: Simplifies access to complex objects
  • Flyweight: Improves resource utilization through object reuse (pooling), such as connection pooling
  • Model-view-controller (MVC) : Decouples presentation logic and business logic

Decorator pattern

The Python language itself supports decorators.

  • Everything in Python is an object, and functions can be passed as arguments
  • A decorator is a function (class) that takes a function as an argument, adds a function and returns a new function.
  • Python uses decorators through @, syntax sugar

Example: Write a decorator that records the time a function takes:

import time

def log_time(func) :  Take a function as an argument
    def _log(*args, **kwargs) :
        beg = time.time()
        res = func(*args, **kwargs)
        print('use time: {}'.format(time.time() - beg))
        return res

    return _log

@log_time  # Decorator syntax sugar
def mysleep() :
    time.sleep(1)

mysleep()

# another way to write, equivalent to the above method of calling
def mysleep2() :
    time.sleep(1)

newsleep = log_time(mysleep2)
newsleep()
Copy the code

You can also use classes as decorators

class LogTime:
    def __call__(self, func) : Take a function as an argument
        def _log(*args, **kwargs) :
            beg = time.time()
            res = func(*args, **kwargs)
            print('use time: {}'.format(time.time()-beg))
            return res
        return _log

@LogTime()
def mysleep3() :
    time.sleep(1)

mysleep3()
Copy the code

You can also add arguments to class decorators

class LogTime2:
    def __init__(self, use_int=False) :
        self.use_int = use_int

    def __call__(self, func) : Take a function as an argument
        def _log(*args, **kwargs) :
            beg = time.time()
            res = func(*args, **kwargs)
            if self.use_int:
                print('use time: {}'.format(int(time.time()-beg)))
            else:
                print('use time: {}'.format(time.time()-beg))
            return res
        return _log

@LogTime2(True)
def mysleep4() :
    time.sleep(1)

mysleep4()

@LogTime2(False)
def mysleep5() :
    time.sleep(1)

mysleep5()
Copy the code

The proxy pattern

  • To proxy the operations of one object to another
  • The has A combination is usually used
from typing import Union


class Subject:
    def do_the_job(self, user: str) - >None:
        raise NotImplementedError()


class RealSubject(Subject) :
    def do_the_job(self, user: str) - >None:
        print(f"I am doing the job for {user}")


class Proxy(Subject) :
    def __init__(self) - >None:
        self._real_subject = RealSubject()

    def do_the_job(self, user: str) - >None:
        print(f"[log] Doing the job for {user} is requested.")
        if user == "admin":
            self._real_subject.do_the_job(user)
        else:
            print("[log] I can do the job just for `admins`.")


def client(job_doer: Union[RealSubject, Proxy], user: str) - >None:
    job_doer.do_the_job(user)


proxy = Proxy()
real_subject = RealSubject()
client(proxy, 'admin')
client(proxy, 'anonymous')
# [log] Doing the job for admin is requested.
# I am doing the job for admin
# [log] Doing the job for anonymous is requested.
# [log] I can do the job just for `admins`.
client(real_subject, 'admin')
client(real_subject, 'anonymous')
# I am doing the job for admin
# I am doing the job for anonymous
Copy the code

Adapter mode

  • To adapt interfaces of different objects to the same interface
  • Imagine a multi-purpose charging head that can charge different appliances and act as an adapter
  • The adapter pattern is used when we need to unify interfaces to different objects
class Dog:
    def __init__(self) :
        self.name = "Dog"

    def bark(self) :
        return "woof!"

class Cat:
    def __init__(self) :
        self.name = "Cat"

    def meow(self) :
        return "meow!"

class Adapter:
    def __init__(self, obj, **adapted_methods) :
        The adapter class receives the adapter method
        self.obj = obj
        self.__dict__.update(adapted_methods)

    def __getattr__(self, item) :
        return getattr(self.obj, item)

objects = []
dog = Dog()
objects.append(Adapter(dog, make_noise=dog.bark))
cat = Cat()
objects.append(Adapter(cat, make_noise=cat.meow))
for obj in objects:
    print("a {} goes {}".format(obj.name, obj.make_noise()))

Copy the code

Behavior type

Common behavioral design patterns:

  • Iterator pattern: Iterates objects through a uniform interface
  • Observer mode: When an object changes, the Observer performs the corresponding action
  • Strategy: Different strategies are used for inputs of different sizes

Iterator pattern

  • Python has built-in support for the iterator pattern
  • For example, we can use for to traverse various Interable data types
  • Python implements __next__ and __iter__ to implement iterators
class NumberWords:
    """Counts by word numbers, up to a maximum of five"""

    _WORD_MAP = (
        "one"."two"."three"."four"."five".)def __init__(self, start, stop) :
        self.start = start
        self.stop = stop

    def __iter__(self) :  # this makes the class an Iterable
        return self

    def __next__(self) :  # this makes the class an Iterator
        if self.start > self.stop or self.start > len(self._WORD_MAP):
            raise StopIteration
        current = self.start
        self.start += 1
        return self._WORD_MAP[current - 1]


for number in NumberWords(start=1, stop=2) :print(number)
Copy the code

Observer mode

  • Publish-subscribe is a common way to do this
  • Publish and subscribe is used to decouple the logic
  • This can be done in ways such as callbacks, which are called when an event occurs
class Publisher: # the publisher
    def __init__(self) :
        self.observers = [] # observer

    def add(self, observer) : # Join observer
        if observer not in self.observers:
            self.observers.append(observer)
        else:
            print("Failed to add: {}".format(observer))

    def remove(self, observer) : # Remove observer
        try:
            self.observers.remove(observer)
        except ValueError:
            print("Failed to remove: {} ".format(observer))

    def notify(self) : # Call observer's callback
        [o.notify_by(self) for o in self.observers]

class Formatter(Publisher) :

    def __init__(self, name) :
        super().__init__()
        self.name = name
        self._data = 0

    @property
    def data(self) :
        return self._data

    @data.setter
    def data(self, new_value) :
        self._data = int(new_value)
        self.notify() # data executes notify after it is legally assigned

class BinaryFormatter: # the subscriber
    def notify_by(self, publisher) :
        print("{}: {} has new bin data={}".format(type(self).__name__, publisher.name, bin(publisher.data)))

df = Formatter('formatter')  # the publisher
bf1 = BinaryFormatter()  # the subscriber
bf2 = BinaryFormatter()  # the subscriber
df.add(bf1) # Add subscriber
df.add(bf2) # Add subscriber
df.data = 3 Call the subscriber's notify_by when setting
Copy the code

The strategy pattern

  • Take different strategies based on different inputs
  • For example, if you buy more than 10 items, you can get 20% off. If you buy more than 20 items, you can get 30% off
  • The unified interface is exposed externally, and different policies are used internally
class Order:
    def __init__(self, price, discount_strategy=None) :
        self.price = price
        self.discount_strategy = discount_strategy

    def price_after_discount(self) :
        if self.discount_strategy:
            discount = self.discount_strategy(self)
        else:
            discount = 0
        return self.price - discount

    def __repr__(self) :
        return "Price: {}, price after discount: {}".format(self.price, self.price_after_discount())

def ten_percent_discount(order) :
    return order.price * 0.10


def on_sale_discount(order) :
    return order.price * 0.25 + 20


order0 = Order(100)
order1 = Order(100, discount_strategy=ten_percent_discount)
order2 = Order(1000, discount_strategy=on_sale_discount)
print(order0)
print(order1)
print(order2)
Copy the code

The resources

Github.com/faif/python…