5

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)
Community
  • 1
  • 1
Erotemic
  • 4,806
  • 4
  • 39
  • 80

2 Answers2

4

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.

user2357112
  • 260,549
  • 28
  • 431
  • 505
  • This is perfect. There has to be some degree of trust that this object will be used as intended. Adding the ability to pickle and copy the value while preserving the `is` operation is enough robustness that I'm comfortable with using it. I understand the `__new__` method, but do you mind explaining what the purpose of the `__reduce__` method is? – Erotemic Dec 08 '16 at 23:31
  • 1
    @Erotemic: It's [one of the ways](https://docs.python.org/2/library/pickle.html#object.__reduce__) to customize pickling, necessary because without it, pickle protocol 0 actually would end up trying to do `object.__new__(_NoParamType)` when unpickling `NoParam`. – user2357112 Dec 08 '16 at 23:36
  • 2
    Overriding `__new__` is overkill. You can just return `'NoParam'` from `__reduce__` and [then the Pickler will look that name up in the module's global scope](https://docs.python.org/3/library/pickle.html#object.__reduce__). At that point, you can just delete the class after you've used it to construct `NoParam`, and then you can be reasonably sure nobody will use it to construct a new one without doing some magical `type(NoParam)()` nonsense, which you can also catch and block if you really want to. – Kevin Dec 08 '16 at 23:37
1

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.

Community
  • 1
  • 1
jwodder
  • 54,758
  • 12
  • 108
  • 124
  • In my view using `**kwargs` for the purpose other than ignoring or passing through, is a very bad practice, as it violates one of the most important principles of Python -- be explicit. Anything that is used in a function, should be defined explicitly. – zr67800 Mar 17 '22 at 02:57