ceremcem
ceremcem

Reputation: 4350

Python Singleton base class unexpected behaviour

This singleton pattern implementation was working as expected, but there is a situation unexpected (see Test). What would have been wrong?

Code:

class Singleton(object):
    _instance = None
    def __new__(cls, *args, **kwargs):
        if not cls._instance:
            cls._instance = super(Singleton, cls).__new__(
                                cls, *args, **kwargs)
            print("new class created")
        return cls._instance

class CounterClassBase(object):
    def __init__(self, *args, **kwargs):
        super(CounterClassBase, self).__init__(*args, **kwargs)
        print("Counter class init in base")

class Counter(CounterClassBase, Singleton):
    counter = None

    def __init__(self, *args, **kwargs):
        super(Counter, self).__init__(*args, **kwargs)
        print("Counter class init")
        self.counter = 0

import pdb
pdb.set_trace()

Test:

(Pdb) var1 = Counter()
new class created
Counter class init in base
Counter class init
(Pdb) var2 = Counter()
Counter class init in base
Counter class init
(Pdb) var3 = Counter()
Counter class init in base
Counter class init
(Pdb) var1.counter = 3
(Pdb) var1.counter
3
(Pdb) var2.counter
3
(Pdb) var3.counter
3
(Pdb) var4 = Counter()
Counter class init in base
Counter class init
(Pdb) var4.counter
0
(Pdb)

Expected value for var4.counter was 3, not 0.

Edit:

As mentioned in the answers, my test was wrong. There is seen obviously that the "Counter class init" statement is printed, so init() is called, so variables are initialized everytime a new class instance is created.

So I changed my code as follows (source: https://stackoverflow.com/a/8665179/1952991) :

class Singleton(type):
    def __init__(self, *args, **kwargs):
        super(Singleton, self).__init__(*args, **kwargs)
        self.__instance = None
    def __call__(self, *args, **kwargs):
        if self.__instance is None:
            self.__instance = super(Singleton, self).__call__(*args, **kwargs)
        return self.__instance


class Counter(object):
    __metaclass__ = Singleton 
    counter = None

    def __init__(self, *args, **kwargs):
        print("Counter class init")
        self.counter = 0

Upvotes: 0

Views: 141

Answers (3)

Isaac Supeene
Isaac Supeene

Reputation: 90

The reason is that __init__ is called every time you call the constructor. You've overridden __new__ to always return the same instance, but the initialization code that sets the counter to 0 is still called every time, right after __new__. If you check var1.counter after you've assigned var4, you'll find that it also returns 0.

Instead of doing it this way, consider the following:

class Counter():
    counter = 0 # Initialize the field here, rather than in __init__

    def __init__(self, *args, **kwargs):
        super(Counter, self).__init__(*args, **kwargs)
        print("Counter class init")

This will initialize the counter field only once, and not overwrite it when a new variable is assigned using the Counter() constructor.

Upvotes: 2

Peter Westlake
Peter Westlake

Reputation: 5036

It's zero because Counter.__init__ assigns it to zero. There's only one singleton object, as intended, but the code in both Counter.__init__ and CounterBase.__init__ is executed each time it's called. To make it work, you would have to initialise the counter attribute in the Singleton class itself. I can see that you wanted to keep the counting and the singleton aspects separate, but for this they are a bit too separate.

Upvotes: 2

mgrouchy
mgrouchy

Reputation: 1961

So your test is wrong. Var 4 is correct. Even thought it is using the originally created class it sets self.counter == 0 in your Counter().__init__().

I would expect if you run your test again and instantiate all your counters at the same time then set the variables to 3, they will all be 3. I'm not sure is this is your desired behaviour.

Upvotes: 2

Related Questions