Singletonobject.c and unification of `singletons` and `singlenels`

1. Terminology

Note, I use “singlenel” to differentiate and to not confuse this to PEP 661: Sentinel Values. The difference between these:

  1. singlenel - has its own type, supports custom attributes and methods. In other words, it is a specialisation of a singleton.
  2. PEP661 - One type for instantiation of many different sentinels

2. Motivation

Currently, there are several singlenel (singleton sentinel) implementations:

  • builtins: None & friends
  • functools.Placeholder
  • typing.NoDefault
  • …?

Why unify?

  1. The repetition needed to implement such object is fairly large and can get quite complex
  2. Serialization issues. E.g. None & friends are special-cased in pickle as their type is not located anywhere. This is because it is often desirable to expose the instance, but not its type. This leads to: type(None).__module__ == 'builtins', but builtins.NoneType -> AttributeError
  3. I think singletons and singlenels would be used more if there was an easy way to implement these. I suspect that some extensions that could make use of these simply don’t happen because implementation of singleton would be more than 50% of the work required. My personal most recent case: Add operator.getitemtuple & operator.itemtuplegetter · Issue #128000 · python/cpython · GitHub.

Why in C?

  1. It could be a base class of None & friends.
  2. It would resolve inconveniences that arise in cases where there are both C and Python implementations. I had issues when testing copy, pickle & similar due to nuances of multi-phase module initialisation. If singlenels lived in independent lower level module, both C and Python versions could use the same.

Why singleton?
Because from implementation perspective None is 99% singleton.
Thus, implementing singleton would also give general singletons and singlenel would be just one specialisation.

3. How would it look like?

So I haven’t got the specifics yet, there is a bit of ground to cover.
But I thought it would be useful to put it out a bit earlier to see whether it is worth going further at all.

However, I had this in mind for a fair while now and have a rough idea.
Base class of builtin and other C-implemented singletons:

type(None).__mro__    # (NoneType, Singleton, object)


Implementing new singleton in Python:

from singleton import Singleton

class UniversalSetType(Singleton):
    def __contains__(self, other):
        return True

UniversalSet = UniversalSetType()
type(UniversalSet)() is UniversalSet# True
UniversalSet.__module__    # "this module"


Shorthand for implementing new singlenel in Python:

from singleton implement new_sentinel
# new_sentinel(name, namespace, /, **attrs)

FlagEmpty = new_sentinel('FlagEmpty', 'typing', a=1)

type(FlagEmpty)() is FlagEmpty    # True
FooType.__module__                # 'typing'
Foo.a                             # 1

# Only 1 unique name per namespace
FlagEmpty = new_sentinel('FlagEmpty', namespace='typing')
    # Error


Adding new singlenel in standard library or extension with standard defaults:

// mymodule.c
#include "singletonobject.h"

MACRO_NEW_SINGLETON("StandardSentinel", ...)
...
from mymodule import StandardSentinel

type(StandardSentinel)() is StandardSentinel   # True
StandardSentinel.__module__                    # 'mymodule`


Adding new Singleton in standard library or extension with custom attributes and methods:

This needs work, but something along the lines of:

// mymodule.c
#include "singletonobject.h"

typedef struct {
    PyObject_HEAD
} customsingletonobject;

// Some macros for common methods
MACRO_SINGLETON_REPR(customsingleton_repr, "CustomSentinel")

static PyType_Slot customsingleton_type_slots[] = {
    {Py_tp_repr, customsingleton_repr},
    ...
    {0, 0}
};

static PyType_Spec customsingleton_type_spec = {
    ...
    .slots = customsingleton_type_slots
};
...

4. Summary

I think this would:

  1. Simplify a fair amount of present and future code in CPython by addressing common issues in one localised place.
  2. Standardise things a bit by introducing a bare-bone protocol.
  3. Provide users with convenient singlenel (and more general singleton) creation

Thoughts?

It did take some time think about. User defined singleton-s are a code smell for me, but the idea as a whole seems good. I do have some questions, tho’.

  1. Why calling a singleton type gives the singleton? Shouldn’t that be an error? type(None)() is an error.
  2. Shouldn’t Singleton have or be a meta class? One shouldn’t be able to inherit from a singleton type.
    #(following OP convention)
    class AType(Singleton): ...
    A = AType()
    class BType(AType): ... #but A is a Singleton.
    B = BType()
    print(B is A) #False, possibly leading to error!
    #But also currently:
    class CType(type(None)): ... #is an error.
    
  3. Last question: lazy initiliazation singleton-s vs anonymous class singleton-s? I understand the most simple approach is to first make (and name) a class, and then get na instance from that class (possibly lazy). But that’s diffrent from build-in singleton-s (like None, or True), in them one gets the type from its instance, not the other way around. I will need to think more, is there an easy solution to it?
1 Like

It is not:

>>> print(type(None)())
None

Making a class final is possible without a metaclass at least in C. Singleton is just a special base class.

1 Like

This indeed would need to be addressed in the process.

However, allowing such inheritance is also an option. There is nothing intrinsically wrong with inheriting from Singleton.

If it is “final”, user can ensure that himself, but he also might want to derive a new base class which he then uses for a cluster of singleton applications.

class SingletonBoolError(Singleton):
    def __bool__(self):
        raise NotImplementedError

class NoValue(SingletonBoolError):

Solution to exactly what?

It seems that it is often desirable to limit public API.

This leads to various nuances / special case handling every time. Some of them are solved (but there is still code repetition) and some are not (such as serialization of type without exposing class at least as private).

This would aim to solve and abstract all of the above.

I don’t think there is right and wrong here.
I am more concerned about consistency.
And type(None)() is None is the behaviour of most existing cases.

And personally, I think this is more fun than raising an error. :slight_smile: