WARNING: The following question is asking for information concerning poor practices and dirty code. Developer discretion is advised.
Note: This is different than the Creating a singleton in Python question because we want to address pickling and copying as well as normal object creation.
Goal: I want to create a value (called NoParam
) that simulates the behavior of None
. Specifically I want any instance of NoParamType
to be the same value --- i.e. have the same id
--- so that the is
operator always returns True
between two of these values.
Why: I have configuration classes that holds onto values of parameters. Some of these parameters can take None
as a valid type. However, I want these parameters to take particular default values if no type is specified. Therefore I need some sentinel that is not None
to specify that no parameter was specified. This sentinel needs to be something that
could never be used as a valid parameter value. I would prefer to have a special type for this sentinel instead of using some unlikely to be used string.
For instance:
def add_param(name, value=NoParam):
if value is NoParam:
# do some something
else:
# do something else
But lets not worry so much about the why. Lets focus on the how.
What I have so far:
I can achieve most of this behavior pretty easily. I have created a special module called util_const.py
. This contains a class that creates a NoParamType and then a singleton instance of the class.
class _NoParamType(object):
def __init__(self):
pass
NoParam = _NoParamType()
I'm simply assuming that a second instance of this class will never be created. Whenever I want to use the value I import util_const
and use util_const.NoParam
.
This works well for most cases. However, I just encountered a case where
a NoParam
value was set as an object value. The object was deep copied using copy.deepcopy
and thus a second NoParam instance was created.
I found a very simple workaround for this by defining the __copy__
and __deepcopy__
methods
class _NoParamType(object):
def __init__(self):
pass
def __copy__(self):
return NoParam
def __deepcopy__(self, memo):
return NoParam
NoParam = _NoParamType()
Now, if deepcopy
is ever called no NoParam
it simply returns the existing NoParam
instance.
Now for the question:
Is there anything I can do to achieve this same behavior with pickling? Initially I thought I could define __getstate__
but the second instance has already been created at that point. Essentially I want pickle.loads(pickle.dumps(NoParam)) is NoParam
to return True. Is there a way to do this (perhaps with metaclasses)?
To take it even further: is there anything I can do to ensure that only one instance of NoParam is ever created?
Solution
Big thanks to @user2357112 for answering the question about pickling. I've also figured out how to make this class robust to module reloading as well. Here is what I've learned all put together
# -*- coding: utf-8 -*-
# util_const.py
class _NoParamType(object):
"""
Class used to define `NoParam`, a setinal that acts like None when None
might be a valid value. The value of `NoParam` is robust to reloading,
pickling, and copying.
Example:
>>> import util_const
>>> from util_const import _NoParamType, NoParam
>>> from six.moves import cPickle as pickle
>>> import copy
>>> versions = {
... 'util_const.NoParam': util_const.NoParam,
... 'NoParam': NoParam,
... '_NoParamType()': _NoParamType(),
... 'copy': copy.copy(NoParam),
... 'deepcopy': copy.deepcopy(NoParam),
... 'pickle': pickle.loads(pickle.dumps(NoParam))
... }
>>> print(versions)
>>> assert all(id(v) == id_ for v in versions.values())
>>> import imp
>>> imp.reload(util_const)
>>> assert id(NoParam) == id(util_const.NoParam)
"""
def __new__(cls):
return NoParam
def __reduce__(self):
return (_NoParamType, ())
def __copy__(self):
return NoParam
def __deepcopy__(self, memo):
return NoParam
def __call__(self, default):
pass
# Create the only instance of _NoParamType that should ever exist
# When the module is first loaded, globals() will not contain NoParam. A
# NameError will be thrown, causing the first instance of NoParam to be
# instanciated.
# If the module is reloaded (via imp.reload), globals() will contain
# NoParam. This skips the code that would instantiate a second object
# Note: it is possible to hack around this via
# >>> del util_const.NoParam
# >>> imp.reload(util_const)
try:
NoParam
except NameError:
NoParam = object.__new__(_NoParamType)
Is there anything I can do to achieve this same behavior with pickling?
Yes.
class _NoParamType(object):
def __new__(cls):
return NoParam
def __reduce__(self):
return (_NoParamType, ())
NoParam = object.__new__(_NoParamType)
To take it even further: is there anything I can do to ensure that only one instance of NoParam is ever created?
Not without writing NoParam
in C. Unless you write it in C and take advantage of C API-only capabilities, it'll always be possible to do object.__new__(type(NoParam))
to get another instance.
I'm going to answer part X, not part Y:
I have configuration classes that holds onto values of parameters. Some of these parameters can take
None
as a valid type. However, I want these parameters to take particular default values if no type is specified....
For instance:
def add_param(name, value=NoParam): if value is NoParam: # do some something else: # do something else
The proper way to test whether or not value
was defined by the caller is to eliminate the default value completely:
def add_param(name, **kwargs):
if 'value' not in kwargs:
# do some something
else:
# do something else
True, this breaks some introspection-based features, like linters that check whether you're passing the right arguments to functions, but that headache should be much, much less than trying to bend the identity system over backwards.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With