Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

How to create a second None in Python? Making a singleton object where the id is always the same

Tags:

python

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)
like image 997
Erotemic Avatar asked Dec 08 '16 20:12

Erotemic


2 Answers

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.

like image 97
user2357112 supports Monica Avatar answered Nov 01 '22 09:11

user2357112 supports Monica


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.

like image 32
jwodder Avatar answered Nov 01 '22 07:11

jwodder