Skip to content

Singleton Pattern

Singletonu anlatmaya geçmeden önce pythonda özel bir kaç magic methoddan bahsetmekte fayda olduğunu düşünüyorum. Bildiğimiz üzere pythonda her şey bir nesnedir ve nesneler belirli yaşam döngüleri ve çalışma mekanizmalarına sahiptir. Temelde nesneler inşa edilir/oluşturulur (construction), başlatılır (initialization) ve son bulur (destruction) nesnelerin oluşma aşamasındaki davranışlarını kontrol etmek için __new__ methodundan yararlanırız, bu method sayesinde nesneyi başlatmadan önce kontroller gerçekleştirebilir veya yeni özellikler ekleyebiliriz. __new__ methodu static method olup, genellikle sınıfın örneğini döner, hemen küçük bir örnek üzerinden küçük bir işlevini görelim.

class CarBuilder(object):
    def add_engine(self):
        print("Engine added")
        

class CarFactory(object):
    def __new__(cls):
        cls.builder = CarBuilder()
        print("Hello from __new__")
        return super().__new__(cls)
    
    def __init__(self):
        print("Hello from __init__")
        self.builder.add_engine()
        
        
car_factory = CarFactory()

----------------------------------------------------------------------
Output:
Hello from __new__
Hello from __init__
Engine added

CarFactory sınıfımızı çağırdığımızda öncelikle new methodu çağırılarak sınıf oluşturuldu sonrasında ise __init__ metodu çağırıldı, örneğimizde sınıf yaratıldığı esnada CarBuilder sınıfının instance’ını sınıf içinde oluşturduk ve bu sayede init metodu içinde kullanabildik, kısacası yaptığımız şey aslında sınıf başlatılmadan önce araya girerek ek bir özellik eklemek oldu.

Diğer değineceğimiz gereken metodlar ise __init__ ve __call__ metodları çoğu zaman pythonda geliştirme yaparken örnekte olmasını istediğimiz parametreleri init metodunda çağırır ve ve bu parametrelerde gerekli business logicleri yürütürüz, __call__ methodu temelde objelere çağırabilme özelliği ekler ve sınıfın örneğinin bir method gibi çağırılabilmesini sağlar, sınıf örneğinin çağırılması ise farklı koşullar altında sınıfın farklı parametrelerle çağırılabilmesini sağlar, evet biraz karışık gibi geliyor ama örnek üzerinden incelersek anlaşılabilir.

class MyClass(object):
    value:str = None
    
    def __init__(self):
        print("Hello from __init__")
        
    def __new__(cls):
        print("Hello from __new__")
        return super().__new__(cls)
        
    def __call__(self, *args, **kwargs):
        self.value = kwargs.get("val")
        print("Hello from __call__ : kwargs -> ", kwargs)
        
    def hello(self):
        print("Hello from class")
        
c = MyClass()
c(val="hello")
print(c.value)
c.hello()
-----------------------------------------------------------------------
Output:
Hello from __new__
Hello from __init__
Hello from __call__ : kwargs ->  {'val': 'hello'}
hello
Hello from class

Örnekte görüldüğü üzere programın akışında, bir classı yeni bir örnek/instance oluşturmadan farklı bir değerle çağırarak, farklı bir davranış gösterebilmesini sağladık, başka bir senaryoda init ve call metodlarına aynı argümanları geçtiğimizi düşünürsek bu sayede örneğin init metoduyla aldığı değerleri sınıfı yeniden örneklemeden güncelleyebiliyor olacaktık, ayrıca çıktıda gördüğümüz metodların çalışma sırasınada dikkat etmekte fayda var.

Singleton türkçe karşılığı tek çocuk, yalnız kimse, tek şey anlamlarına geliyor ve aslında bu anlamlar patternin yaptığı işi çok güzel açıklıyor, runtimeda bazı nesnelerin örneklerinin tek sefer oluşturulmasına ve programın hayatının sonuna kadar bu örnek üzerinden işlemlerin yapılmasını isteriz bunu yaparkende pratikte oluşturduğumuz örneği bellekte tutarak bu nesne her örneklendiğinde yeni bir örnek oluşturmadan aynı örneğin dönmesini sağlarız, örneğin veritabanı bağlantısını gerçekleştiren sınıfı tek sefer oluşturarak bu örnek üzerinden bağlantıyı gerçekleştirmek için singleton design patternden yararlanabiliriz ve ayrıca bu sayede database bağlantısını global bir erişim noktası üzerinden gerçekleştirebiliriz, diğer taraftan ise global değişkenlerden farklı olarak programın başlangıcında değil istediğimiz zamanda yaratabildiğimiz global bir erişim noktası elde ederiz. Bu kadar anlatımdan sonra biraz kodlamazsak olmaz;

class SingletonMeta(type):
    _instances = {}
    
    def __call__(cls, *args, **kwargs):
        if cls not in cls._instances:
            instance = super().__call__(*args, **kwargs)
            cls._instances[cls] = instance
            print("instance added : ", instance, " id : ", id(instance))
        print("instance called : ", cls._instances[cls], " id : ", id(cls._instances[cls]))
        return cls._instances[cls]
    
    
class Singleton(metaclass=SingletonMeta):
    def greeting(self):
        print("Hello from Single instance")
        

if __name__ == '__main__':
    s1 = Singleton()
    s2 = Singleton()
    s1.greeting()
    s2.greeting()
    print(id(s1), id(s2))
    print(s1 is s2)
    print(f"s1 and s2 instances name values : {s1.name} - {s2.name}")

-----------------------------------------------------------------------
Output:
instance added :  <__main__.Singleton object at 0x7fe11a103a00>  id :  140604781640192
instance called :  <__main__.Singleton object at 0x7fe11a103a00>  id :  140604781640192
instance called :  <__main__.Singleton object at 0x7fe11a103a00>  id :  140604781640192
John says: Hello from Single instance
John says: Hello from Single instance
140604781640192 140604781640192
True
s1 and s2 instances name values : John - John

veya deseni __new__ metoduylada biraz daha basitlestirerek uygulayabiliriz.

class Singleton:
    def __new__(cls, *args, **kwargs) :
        if not hasattr(cls, 'instance'):
            cls.instance = super(Singleton, cls).__new__(cls)
        return cls.instance
    
    
class A(Singleton):
    def __init__(self, x):
        self.x = x
        
        
a = A(3)
b = A(2)

print(id(a), id(b))
print(a is b)
print(f"a and b instances x values : {a.x} - {b.x}")

-----------------------------------------------------------------------
Output:
140604781640672 140604781640672
True
a and b instances x values : 2 - 2

Birden fazla thread üzerinden işlemlerin gerçekleştiği durumlarda ise her thread için ayrı bir singleton nesnesi oluşacaktır, bu durumun önüne geçmek için ise örneğimizi şöyle thread safe hale getirebiliriz;

from threading import Lock, Thread


class SingletonMeta(type):
    _instances = {}
    _lock: Lock = Lock()
    
    def __call__(cls, *args, **kwargs):
        with cls._lock:
            if cls not in cls._instances:
                instance = super().__call__(*args, **kwargs)
                cls._instances[cls] = instance
        return cls._instances[cls]
        
        
class Singleton(metaclass=SingletonMeta):
    value:str=None
    
    def __init__(self, value):
        self.value = value
        
    def get_value(self):
        return self.value
        
        
def test_singleton(value):
    s = Singleton(value)
    print(id(s), " -> ", s.get_value()) 
    
    
if __name__ == "__main__":
    p1 = Thread(target=test_singleton, args=("s1",))
    p2 = Thread(target=test_singleton, args=("s2",))
    p1.start()
    p2.start()
-----------------------------------------------------------------------
Output:
4308039088  ->  s1
4308039088  ->  s1

bu sayede farklı threadlardaki objeler çağırıldığında bellekteki ortak bir objeyi kullanıyor olacaklar.

Son olara singleton patternden bahsetmişken Borg patternden de söz ederek yazıyı sonlandıralım, borg pattern singletona benzer bir yapıdadır, Singleton Pattern, bir sınıfın yalnızca bir instance’ını oluşturmamızı sağlar ve bu örneğe, programın herhangi bir yerinde erişilebilir. Borg Pattern ise, bir sınıfın farklı örneklerine ait tüm özelliklerin ortak olmasını sağlar. Yani, farklı örnekler oluşturulsa bile, bu örnekler aynı özelliklere sahip olacaktır yani oluşturulan tüm sınıflar aynı state‘i paylaşıyor olacaklardır. Singleton Pattern’da olduğu gibi, Borg Pattern da paylaşılan bir örneğe sahip olmamızı sağlar, ancak farklı özelliklere sahip olabilen birden fazla örneğe izin verir. Bu nedenle, Borg Pattern, Singleton Pattern’dan daha esnek bir yapıdır ve uygulamanın gereksinimlerine daha iyi uyarlanabilir. Örneğin web uygulamamızda kullanıcıların sayfa istatistiklerini tutan ve kullanıcılara anlık olarak gösteren bir yapı kurmak istediğimizi düşünelim bu durumda, bir kullanıcı sayfaya tıkladığında görüntülenme sayısı artmalı ve başka bir kullanıcı görüntülenme sayısının arttığını görmeli bu durumda borg patterni kullanabiliriz. Örnek kod senaryomuzda ise cache datasını tutan bir yapı üzerinden ilerleyelim, Cache sınıfından programın farklı kısımlarında örnekleyerek cache’te tuttuğumuz dataya ulaşabilmeliyiz ve bu instancelerın stateleri arasında veri tutarlılığını sağlayabilmek adına herhangi bir fark olmamalı.

class Borg:
    _shared_state = {}

    def __init__(self):
        self.__dict__ = self._shared_state
    
    
class CacheManager(Borg):
    def __init__(self, c1_location):
        super().__init__()
        self.c1_location = c1_location


class ImportantCacheManager(Borg):
    def __init__(self, c2_location):
        super().__init__()
        self.c2_location = c2_location



c1=CacheManager(c1_location="r1")
print(c1._shared_state)

c2=ImportantCacheManager(c2_location="r2")
print(c2._shared_state)

print(f"c2_location from CacheManager instance: {c1.c2_location}")
print(f"c1_location from ImportantCacheManager instance: {c2.c1_location}")


-----------------------------------------------------------------------
Output:
{'c1_location': 'r1'}
{'c1_location': 'r1', 'c2_location': 'r2'}
c2_location from CacheManager instance: r2
c1_location from ImportantCacheManager instance: r1

ImportantCacheManager ve CacheManager sınıflarının tüm örnekleri, _shared_state adlı bir sınıf değişkenini paylaşırlar. Yeni bir örnek oluşturulduğunda, __dict__ özelliği bu paylaşılan durumu alır. Bu şekilde, tüm örneklerin aynı değişkenleri paylaşması sağlanır.


Sources:

Published inCreational Design PatternsDesign Patterns

Be First to Comment

Leave a Reply

Your email address will not be published. Required fields are marked *