Reputation: 11
I have a Singleton class and I want to share an attribute of that Singleton class across all processes without passing it through a function argument as a shared variable.
Sample code given below:
class Singleton():
abc = {}
def __call__():
abc['key'] = ".com"
class myClass(metaclass=Singleton):
def capslock(name):
print name.upper()
if __name__==__main__:
import multiprocessing as mp
process1 = mp.Process(target=myClass.capslock, args=("stackoverflow"))
process1.start()
process1.join()
For the print statement, I need name.upper() + abc['key] but all the child processes will have the Singleton attribute empty.
Upvotes: 0
Views: 417
Reputation: 3738
You can implement __reduce__
as a class property, with the help of cached_classproperty, to prevent pickle
from calling __new__
and __setstate__
.
class myClass(metaclass=Singleton):
@cached_classproperty
def singleton_instance(cls) -> Self:
return cls()
@override
def __reduce__(self) -> str | Tuple[Any, ...]:
return (
f"{type(self).__qualname__}.singleton_instance"
if self is self.singleton_instance
else super().__reduce__()
)
Note that metaclass=Singleton
is not necessary if the user always visit the instance via singleton_instance
. Simply move the initialization to myClass.__init__
.
class myClass:
@cached_classproperty
def singleton_instance(cls) -> Self:
return cls()
@override
def __reduce__(self) -> str | Tuple[Any, ...]:
return (
f"{type(self).__qualname__}.singleton_instance"
if self is self.singleton_instance
else super().__reduce__()
)
abc = {}
def __init__(self):
abc['key'] = ".com"
This is more Pythonic than hacking the metaclass.
Upvotes: 0
Reputation: 44313
I am tempted to issue a close vote on your question since it is not entirely clear what you are trying to accomplish (see my posted comment to your question).
But if you are trying to automatically add a class attribute abc
whenever a singleton is created, it will not work the way you propose with multiprocessing because when the singleton instance is serialized/deserialized from the main process to the child process using pickle
, this bypasses normal instance creation. Any class attributes that have been defined in the class definition will be pickled as originally defined, but any class attributes that are subsequently added or modified will not be reflected in the child process.
The following code demonstrates how I would create singleton instances with adding a class attribute. But this also demonstrates that when such an instance is pickled to the new child process, the changed class attribute x
will have the value as defined when the class was created and class attribute abc
, which is dynamically added subsequent to class creation, will not exist at all:
class Singleton(type):
_instances = {}
def __call__(self, *args, **kwargs):
if not self in self._instances:
print('Creating singleton:')
instance = super().__call__(*args, **kwargs)
self._instances[self] = instance
# Add class atribute
instance.__class__.abc = {"key": "com"}
return self._instances[self]
class myClass(metaclass=Singleton):
x = 1 # class attribute
@staticmethod
def capslock(name):
print(f'{name.upper()}, attribute x is {myClass.x}, attribute abc is {getattr(myClass, "abc", "missing")}')
if __name__== "__main__":
import multiprocessing as mp
# You need to crate an instance of myClass to get
# class attribute abc. Here myClass.abc will be missing:
print(f'attribute x is {myClass.x}, attribute abc is {getattr(myClass, "abc", "missing")}')
myClass_singleton = myClass()
# Now myClass.abc will exist:
myClass.x = 2 # change dynamically
print(f'attribute x is {myClass.x}, attribute abc is {getattr(myClass, "abc", "missing")}')
# Verify we are getting singletons:
myClass_singleton_another = myClass()
print(myClass_singleton is myClass_singleton_another)
# The class of the singleton and class attributes x and abc will be printed by
# method capslock. When called by the main process, x will have the modified
# value of 2 and abc will be a dictionary.
myClass_singleton.capslock('main process')
# When capslock is called by the child process, x will again be 1 and
# abc will be missing entirely:
process1 = mp.Process(target=myClass_singleton.capslock, args=("child process",))
process1.start()
process1.join()
Prints:
attribute x is 1, attribute abc is missing
Creating singleton:
attribute x is 2, attribute abc is {'key': 'com'}
True
MAIN PROCESS, attribute x is 2, attribute abc is {'key': 'com'}
CHILD PROCESS, attribute x is 1, attribute abc is missing
A Solution
Here we customize the pickle
serialization process to ensure that we also serialize/de-serialize class attribute abc
. For this we define a mixin class SingletonMixin
from which our myClss
class inherits:
class Singleton(type):
_instances = {}
def __call__(self, *args, **kwargs):
if not self in self._instances:
print('Creating singleton:')
instance = super().__call__(*args, **kwargs)
self._instances[self] = instance
# Add class atribute
cls = self.__class__
cls.abc = {"key": "com"}
return self._instances[self]
class SingletonMixin:
def __getstate__(self):
return getattr(myClass, "abc", {}), self.__dict__
def __setstate__(self, state):
abc, __dict__ = state
self.__dict__.update(__dict__)
self.__class__.abc = abc
class myClass(SingletonMixin, metaclass=Singleton):
def capslock(self, name):
print(f'{name.upper()}, attribute abc is {getattr(myClass, "abc", "missing")}')
if __name__== "__main__":
import multiprocessing as mp
# You need to crate an instance of myClass to get
# class attribute abc. Here myClass.abc will be missing:
print(f'attribute abc is {getattr(myClass, "abc", "missing")}')
myClass_singleton = myClass()
# Now myClass.abc will exist:
myClass.x = 2 # change dynamically
print(f'attribute abc is {getattr(myClass, "abc", "missing")}')
# Verify we are getting singletons:
myClass_singleton_another = myClass()
print(myClass_singleton is myClass_singleton_another)
# The class of the singleton and class attributes x and abc will be printed by
# method capslock. When called by the main process, x will have the modified
# value of 2 and abc will be a dictionary.
myClass_singleton.capslock('main process')
# When capslock is called by the child process, x will again be 1 and
# abc will be missing entirely:
process1 = mp.Process(target=myClass_singleton.capslock, args=("child process",))
process1.start()
process1.join()
Prints:
attribute abc is missing
Creating singleton:
attribute abc is {'key': 'com'}
True
MAIN PROCESS, attribute abc is {'key': 'com'}
CHILD PROCESS, attribute abc is {'key': 'com'}
Upvotes: 1