본문 바로가기
Learning Python Design Patterns

The Singleton Design Pattern

by 자동매매 2023. 3. 29.

The Singleton Design Pattern

In the previous chapter, we explored design patterns and their classifications. As we are aware, design patterns can be classified under three main categories: structural, behavioral, and creational patterns.

In this chapter, we will go through the Singleton design pattern—one of the simplest and well-known Creational design patterns used in application development. This chapter will give you a brief introduction to the Singleton pattern, take you through a real-world example where this pattern can be used, and explain it in detail with the help of Python implementations. You will learn about the Monostate (or Borg) design pattern that is a variant of the Singleton design pattern.

In this chapter, we will cover the following topics in brief:

  • An understanding of the Singleton design pattern
  • A real-world example of the Singleton pattern
  • The Singleton pattern implementation in Python
  • The Monostate (Borg) pattern

At the end of the chapter, we have a short summary on Singletons. This will help you think independently about some of the aspects of the Singleton design pattern.

Understanding the Singleton design pattern

Singleton provides you with a mechanism to have one, and only one, object of a given type and provides a global point of access. Hence, Singletons are typically used in cases such as logging or database operations, printer spoolers, and many others, where there is a need to have only one instance that is available across the application to avoid conflicting requests on the same resource. For example, we may want to use one database object to perform operations on the DB to maintain data consistency or one object of the logging class across multiple services to dump log messages in a particular log file sequentially.

In brief, the intentions of the Singleton design pattern are as follows:

  • Ensuring that one and only one object of the class gets created
  • Providing an access point for an object that is global to the program
  • Controlling concurrent access to resources that are shared

The following is the UML diagram for Singleton:

 

A simple way of implementing Singleton is by making the constructor private and creating a static method that does the object initialization. This way, one object gets created on the first call and the class returns the same object thereafter.

In Python, we will implement it in a different way as there's no option to create private constructors. Let's take a look at how Singletons are implemented in the Python language.

 

파이썬에서 고전적인 Singleton 구현하기

Here is a sample code of the Singleton pattern in Python v3.5. In this example, we will do two major things:

  1. We will allow the creation of only one instance of the Singleton class.
  2. If an instance exists, we will serve the same object again.

The following code shows this:

 

class Singleton(object):

    def __new__(cls):
       if not hasattr(cls, 'instance'):
         cls.instance = super(Singleton, cls).__new__(cls)
       return cls.instance


s = object.__new__(Singleton)
print("Object created", s)

s1 = Singleton()
print("Object created", s1)

 

The output of the preceding snippet is given here:

<__main__.Singleton object at 0x000002A6D6DF2DA0>
<__main__.Singleton object at 0x000002A6D6DF2DA0>

 

In the preceding code snippet, we override the __new__ method (Python's special method to instantiate objects) to control the object creation. The s object gets created with the __new__ method, but before this, it checks whether the object already exists. The hasattr method (Python's special method to know if an object has a certain property) is used to see if the cls object has the instance property, which checks whether the class already has an object. Till the time the s1 object is requested, hasattr() detects that an object already exists and hence s1 allocates the existing object instance (located at 0x102078ba8).

 

싱글톤 패턴 지연 인스턴스화 ( Lazy instantiation )

One of the use cases for the Singleton pattern is lazy instantiation. For example, in the case of module imports, we may accidently create an object even when it's not needed. Lazy instantiation makes sure that the object gets created when it's actually needed. Consider lazy instantiation as the way to work with reduced resources and create them only when needed.

 

1. 초기화시 객체를 생성하지 않는다.             : s=Singleton()

2. getinstance() 메서드를 호출시 활성 객체 생성   : Singleton.getInstance()

 

This is how lazy instantiation is achieved.

 

class Singleton:
    
    __instance = None
    
    def __init__(self):
        if not Singleton.__instance:
            print(" __init__ method called..")
        else:
            print("Instance already created:", self.getInstance())
    
    @classmethod
    def getInstance(cls):
        if not cls.__instance:
            cls.__instance = Singleton()
        return cls.__instance

s = Singleton() ## class initialized, but object not created
print("Object created", Singleton.getInstance()) ## Gets created here
s1 = Singleton() ## instance already created

 

논의

 

class Singleton:
    
    __instance = None
    
    def __init__(self):
        if not Singleton.__instance:
            pass
        else:
            self.getInstance()
    
    @classmethod
    def getInstance(cls):
        if not cls.__instance:
            cls.__instance = Singleton()
        return cls.__instance


s0 = Singleton()
print('object s0', s0)
s1 = Singleton() 
print('object s1', s1)
s2 = Singleton.getInstance()
print('object s2', s2)
s3 = Singleton() 
print('object s3',s3)
s4 = Singleton.getInstance()
print('object s4',s4)

output

object s0 <__main__.Singleton object at 0x0000025D39F62A70>
object s1 <__main__.Singleton object at 0x0000025D39F62AD0>
object s2 <__main__.Singleton object at 0x0000025D39F62E00>
object s3 <__main__.Singleton object at 0x0000025D39F62E30>
object s4 <__main__.Singleton object at 0x0000025D39F62E00>

 

Module-level Singletons

All modules are Singletons by default because of Python's importing behavior. Python works in the following way:

  1. Checks whether a Python module has been imported.
  2. If imported, returns the object for the module. If not imported, imports and instantiates it.
  3. So when a module gets imported, it is initialized. However, when the same module is imported again, it's not initialized again, which relates to the Singleton behavior of having only one object and returning the same object.

 

The Monostate Singleton pattern

GoF Singleton design pattern은 클래스의 객체가 하나만 있어야 한다고 말합니다. 그러나 Alex Martelli에 따르면 일반적으로 프로그래머에게 필요한 것은 동일한 상태를 공유하는 인스턴스를 갖는 것입니다. 그는 개발자가 identity 보다는 state , behavior 에 대해 더 고민해야한다고 제안합니다.

이 개념은 동일한 상태를 공유하는 모든 개체를 기반으로 하기 때문에 Monostate Pattern이라고도 합니다. 

 

모노스테이트 패턴은 파이썬에서 매우 간단한 방법으로 달성할 수 있습니다 . 

다음 코드에서는 __dict__ 변수 (Python 의 특수 변수 )는 __shared_state  클래스 변수로 할당된다. 파이썬은 __dict__ 를 사용하여 클래스의 모든 객체의 상태를 저장합니다. 다음 코드에서는 생성된 모든 인스턴스에 의도적으로 __shared_state를 할당 합니다. 따라서 'b' 'b1'이라는 두 개의 인스턴스를 만들면 하나의 객체만 있는 Singleton과 달리, 두 개의 다른 객체를 얻습니다. 그러나 개체 상태(b.__dict__, b1.__dict__)는 동일합니다. 이제 객체 변수 x가 객체 b 에 대해 변경 되더라도 변경 사항은 모든 객체가 공유하는 __dict__ 변수로 복사되고 b1 조차도 x 설정을 1에서 4 로 변경합니다.

 

 

class Borg:
    __shared_state = {"Name": "John"}

    def __init__(self):
        self.x = 1
        self.__dict__ = self.__shared_state
        pass


b = Borg()
b1 = Borg()
b.x = 4

print("Borg Object 'b': ", b)  # b and b1 are distinct objects
print("Borg Object 'b1': ", b1)
print("Object State 'b':", b.__dict__)  # b and b1 share same state
print("Object State 'b1':", b1.__dict__)

 

The following is the output of the preceding snippet:

 

Borg Object 'b':  <__main__.Borg object at 0x000001B0014F2A40>
Borg Object 'b1':  <__main__.Borg object at 0x000001B0014F2AA0>
Object State 'b': {'Name': 'John', 'x': 4}
Object State 'b1': {'Name': 'John', 'x': 4}

 

Borg 패턴을 현하는 또 다른 방법은 new method 조정하는 것입니다.

 

class Borg(object):
     _shared_state = {}
     def __new__(cls, *args, **kwargs):
       obj = super(Borg, cls).__new__(cls, *args, **kwargs)
       obj.__dict__ = cls._shared_state
       return obj

 

Singletons and metaclasses

메타 클래스에 대한 간략한 소개부터 시작하겠습니다. 메타 클래스는 클래스의 클래스이며, 이는 클래스가 그것의 메타 클래스의 인스턴스임을 의미합니다. 메타 클래스를 사용하면 프로그래머는 미리 정의된 Python 클래스에서 자신 type의 클래스를 만들 수 있습니다. 예를 들어 MyClass 객체가 있는 경우 MyClass의 동작을 필요한 방식으로 재정의하는 메타클래스 MyKls를 만들 수 있습니다. 자세히 이해합시다.

 파이썬에서는 모든 것이 객체 입니다 . a=5라고 하면 type(a)<type 'int '>를 반환하며, 이는 a int 유형임을 의미합니다. 그러나 type (int) <type ' type'>을 반환하며, 이는 inttype유형의 클래스이므로 메타 클래스가 있음을 나타냅니다.

 

클래스의 정의는 메타 클래스에 의해 결정되므로 class A를 생성하면

A = type(name, bases, dict)

  • name: 클래스의 이름입니다 .
  • base: 기본 클래스입니다.
  • dict : 이것은 속성 변수입니다.

클래스에 미리 정의된 메타클래스(MetaKls)가 있는 경우

A = MetaKls(name, bases, dict)

 

샘플 메타 클래스 구현을 살펴 보겠습니다.

 

class MyInt(type):
    
    def __call__(cls, *args, **kwds):
        print("***** Here's My int *****", args)
        print("Now do whatever you want with these objects...")
        return type.__call__(cls, *args, **kwds)


class int(metaclass=MyInt):
    
    def __init__(self, x, y):
        self.x = x
        self.y = y

i = int(4,5)

 

The following is the output of the preceding code:

 

Python's special __call__ method gets called when an object needs to be created for an already existing class. In this code, when we instantiate the int class with int(4,5), the __call__ method of the MyInt metaclass gets called, which means that the metaclass now controls the instantiation of the object. Wow, isn't this great?!

The preceding philosophy is used in the Singleton design pattern as well. As the metaclass has more control over class creation and object instantiation, it can be used to create Singletons. (Note: To control the creation and initialization of a class, metaclasses override the __new__ and __init__ method.)

The Singleton implementation with metclasses can be explained better with the following example code:

class MetaSingleton(type):

 

class MetaSingleton(type):
    
    _instances = {}
    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super(MetaSingleton, cls).__call__(*args, **kwargs)
        return cls._instances[cls]

class Logger(metaclass=MetaSingleton):
    pass

logger1 = Logger()
logger2 = Logger()
print(logger1, logger2)

 

A real-world scenario – the Singleton pattern, part 1

As a practical use case, we will look at a database application to show the use of Singletons. Consider an example of a cloud service that involves multiple read and write operations on the database. The complete cloud service is split across multiple services that perform database operations. An action on the UI (web app) internally will call an API, which eventually results in a DB operation.

It's clear that the shared resource across different services is the database itself. So, if we need to design the cloud service better, the following points must be taken care of:

  • Consistency across operations in the database—one operation shouldn't result in conflicts with other operations
  • Memory and CPU utilization should be optimal for the handling of multiple operations on the database

A sample Python implementation is given here:

 

import sqlite3

class MetaSingleton(type):
    
    _instances = {}
    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            cls._instances[cls] = super(MetaSingleton, cls).__call__(*args, **kwargs)
        return cls._instances[cls]


class Database(metaclass=MetaSingleton):
    connection = None
    def connect(self):
        if self.connection is None:
            self.connection = sqlite3.connect("db.sqlite3")
            self.cursorobj = self.connection.cursor()
        return self.cursorobj

db1 = Database().connect()
db2 = Database().connect()

print ("Database Objects DB1", db1)
print ("Database Objects DB2", db2)

 

The output of the preceding code is given here:

 

In the preceding code, we can see following points being covered:

  1. We created a metaclass by the name of MetaSingleton. Like we explained in the previous section, the special __call__ method of Python is used in the metaclass to create a Singleton.
  2. The database class is decorated by the MetaSingleton class and starts acting like a Singleton. So, when the database class is instantiated, it creates only one object.
  3. When the web app wants to perform certain operations on the DB, it instantiates the database class multiple times, but only one object gets created. As there is only one object, calls to the database are synchronized. Additionally, this is inexpensive on system resources and we can avoid the situation of memory or CPU resource.

Consider that instead of having one webapp, we have a clustered setup with multiple web apps but only one DB. Now, this is not a good situation for Singletons because, with every web app addition, a new Singleton gets created and a new object gets added that queries the database. This results in unsynchronized database operations and is heavy on resources. In such cases, database connection pooling is better than implementing Singletons.

 

A real-world scenario – the Singleton pattern, part 2

Let's consider another scenario where we implement health check services (such as Nagios) for our infrastructure. We create the HealthCheck class, which is implemented as a Singleton. We also maintain a list of servers against which the health check needs to run. If a server is removed from this list, the health check software should detect it and remove it from the servers configured to check.

In the following code, the hc1 and hc2 objects are the same as the class in Singleton.

Servers are added to the infrastructure for the health check with the addServer() method. First, the iteration of the health check runs against these servers. The changeServer() method removes the last server and adds a new one to the infrastructure scheduled for the health check. So, when the health check runs in the second iteration, it picks up the changed list of servers.

All this is possible with Singletons. When the servers get added or removed, the health check must be the same object that has the knowledge of the changes made to the infrastructure:

class HealthCheck:

 

class HealthCheck:
    
    _instance = None
    def __new__(cls, *args, **kwargs):
        if not HealthCheck._instance:
            HealthCheck._instance = super(HealthCheck, cls).__new__(cls, *args, **kwargs)
        return HealthCheck._instance
    
    def __init__(self):
        self._servers = []
    
    def addServer(self):
        self._servers.append("Server 1")
        self._servers.append("Server 2")
        self._servers.append("Server 3")
        self._servers.append("Server 4")
    
    def changeServer(self):
        self._servers.pop()
        self._servers.append("Server 5")

hc1 = HealthCheck()
hc2 = HealthCheck()

hc1.addServer()
print("Schedule health check for servers (1)..")
for i in range(4):
    print("Checking ", hc1._servers[i])


hc2.changeServer()
print("Schedule health check for servers (2)..")
for i in range(4):
    print("Checking ", hc2._servers[i])

 

The output of the code is as follows:

 

Drawbacks of the Singleton pattern

While Singletons are used in multiple places to good effect, there can be a few gotchas with this pattern. As Singletons have a global point of access, the following issues can occur:

  • Global variables can be changed by mistake at one place and, as the developer may think that they have remained unchanged, the variables get used elsewhere in the application.
  • Multiple references may get created to the same object. As Singleton creates only one object, multiple references can get created at this point to the same object.
  • All classes that are dependent on global variables get tightly coupled as a change to the global data by one class can inadvertently impact the other class.

As part of this chapter, you learned a lot on Singletons. Here are a few points that we should remember about Singletons:

  • There are many real-world applications where we need to create only one object, such as thread pools, caches, dialog boxes, registry settings, and so on. If we create multiple instances for each of these applications, it will result in the overuse of resources. Singletons work very well in such situations.
  • Singleton; a time-tested and proven method of presenting a global point of access without many downsides.
  • Of course, there are a few downsides; Singletons can have an inadvertent impact working with global variables or instantiating classes that are resource-intensive but end up not utilizing them.

Summary

In this chapter, you learned about the Singleton design pattern and the context in which it's used. We understood that Singletons are used when there is a need to have only one object for a class.

We also looked at various ways in which Singletons can be implemented in Python. The classical implementation allowed multiple instantiation attempts but returned the same object.

We also discussed the Borg or Monostate pattern, which is a variation of the Singleton pattern. Borg allows the creation of multiple objects that share the same state unlike the single pattern described by GoF.

We went on to explore the webapp application where Singleton can be applied for consistent database operations across multiple services.

Finally, we also looked at situations where Singletons can go wrong and what situations developers need to avoid.

At the end of this chapter, we're now comfortable enough to take the next step and study other creational patterns and benefit from them.

In the next chapter, we'll take a look at another creational pattern and the Factory design pattern. We'll cover the Factory method and Abstract Factory patterns and understand them in the Python implementation.

'Learning Python Design Patterns' 카테고리의 다른 글

The Proxy Pattern  (0) 2023.03.29
The Façade Pattern  (0) 2023.03.29
The Factory Pattern  (0) 2023.03.29
Introduction to Design Patterns  (0) 2023.03.29
Index  (0) 2023.03.29

댓글