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
FlagEmptyType.__module__          # 'typing'
FlagEmpty.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. (edit: Only in mypy.)
  2. Shouldn’t Singleton have or be a meta class? One shouldn’t be able to inherit from a singleton type. (edit: B is A is the error, not the inheritance.)
    #(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 initialization 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 different from build-in singleton-s (like None, or True (edit: Not a singleton, type(True)() is False.)), 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?
    (Edit: The simplest solution is for all singletons to have a instance getter. And maybe, only if needed, as a separate (but unlikely) proposal, @anonymous_class decorator.)
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.

2 Likes

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:

Sorry, I auto run mypy. My bad.

Sorry, I over complicated it by running mypy by accident. Those types already have a instance getter, so there is no problem to solve

I didn’t know this problem still has traction.

About inheritance from Singleton, my position has changed. There are 3 states a subtype can be in:

  1. Initial.
  2. Called - Inheriting from it would raise.
  3. Inherited from - Calling it would raise.

I don’t think this ever had any significant amount of it. I hope it will get some over time though as I think this might be a good idea for the long run.

That is one path to it. Any reasons why this is the best one?

E.g.:

class NewSingletonType(Singleton):
    def __bool__(self): return True
    def __eq__(self, other): return False

NewSingleton = NewSingletonType()

class InheritedSingletonType(NewSingletonType):
    # Has all the methods of NewSingletonType
    def __le__(self, other): return False

InheritedSingleton = InheritedSingletonType()

Without restriction above, some repetition can be avoided for certain cases.

Real-life use case:

class MyNoneType(type(None)):
    def __bool__(self):
        return True

MyNone = MyNoneType()

So I can have new singleton identical to None, but different __bool__.

If there are no specific reasons to restrict things for all cases, such restriction could be done manually if necessary. This would also make things more explicit and tailored to specific case. E.g.:

class NonInstantiableSingleton(Singleton):
    def __new__(cls, *args, **kwds):
        if type(cls) is NonInstantiableSingleton:
            raise
        return super().__new__(*args, **kwds)

class NonInheritableSingleton(Singleton):
    def __init_subclass__(cls, *args, **kwds):
        raise

Side note: this may trigger the bug 82266.

Only constancy reason. Those can be by convention, but for buildin types it’s at runtime.

  • object() as a singleton is so widely used, it’s not going anywhere. It’s also light weight.
  • Typing spec requires allows support for object() as a singleton.
  • Current PEP 661 draft aims to replace object() singleton, but that’s just not going to happen. Its failure is a matter of time.
  • Solution requiring an import, needs something to be worth it. It needs to be different than a simple object().
  • If the aim is to be a singleton, why not just actually be a singleton? That is, have a pair of final class and the only allowed instance of that type.
  • There is nothing wrong in making a super class for some set of singletons.

Noted.

This is slightly different to object(). object() is not really a singleton. It is an instance that is used as a sentinel. It is good for low level local programmatic use. PEP661 only aims to make this slightly nicer.

Exactly, this suggests introducing true singleton.

PEP661 is potentially very lightweight implementation. As long as it does not introduce new module I am quite fine with that. Having slightly more elegant object() in builtins doesn’t seem such a bad idea to me. Reading such __repr__s is not very pleasant to me:

[<object at 0x129668030>,
 1,
 <object at 0x129668030>,
 1,
 <object at 0x129668030>,
 1,
 <object at 0x129668030>,
 1,
 <object at 0x129668030>,
 1]

Personally, I have both in my library:

  1. PEP661-like lightweight sentinels
  2. Persistent/user-facing/flexible singletons.

I use (2) not only for placeholder-sentinels, but also for things like Inf, Superset and similar stuff.

All in all, it is very early days for this - there is no great need for this for now. Things might change:

  • There are up to 10 true singleton sentinels in standard library and the number is growing very slowly. This would eliminate a lot of boilerplate, but if nothing else changes I would probably become more active on this if the number starts approaching 20.
  • Another possibility is that after PEP661 is implemented it becomes evident that this is also needed as it did not cover as much as was expected (I have reasonable expectations what it is going to cover and I am quite happy with it).
  • Or maybe some other new singleton pattern becomes very popular which would bring this to the forefront.

But for now, unless I accidentally come across natural implementation, I am taking this slow.

1 Like

As this is for generic singletons, this ideally would be more loose than sentinel standards as sentinels is theoretically just one use case for it.

It is always possible to add it later. If added from the beginning, there is a risk of needing to remove it while the reverse is less costly.