Singleton instructions
qiskit.circuit.singleton
The machinery in this module is for defining subclasses of Instruction
and Gate
that preferentially return a shared immutable singleton instance when instantiated. Taking the example of XGate
, the final user-facing result is that:
- There is a regular class called
XGate
, which derives fromGate
. - Doing something like
XGate(label="my_gate")
produces an object whose type is exactlyXGate
, and all the mutability works completely as expected; all the methods resolve to exactly those defined byXGate
,Gate
, or parents. - Doing
XGate()
produces a singleton object whose type is a synthetic_SingletonXGate
class, which derivesXGate
but overrides__setattr__()
to make itself immutable. The object itself has precisely the same instance attributes asXGate()
would have if there was no singleton handling. This object will return itself undercopy()
,deepcopy()
and roundtrip throughpickle
.
The same can be true for, for example, Measure
, except that it’s a subclass of Instruction
only, and not Gate
.
The classes in this module are for advanced use, because they are closely entwined with the heart of Qiskit’s data model for circuits.
From a library-author perspective, the minimum that is needed to enhance a Gate
or Instruction
with this behavior is to inherit from SingletonGate
(SingletonInstruction
) instead of Gate
(Instruction
), and for the __init__
method to have defaults for all of its arguments (these will be the state of the singleton instance). For example:
class XGate(SingletonGate):
def __init__(self, label=None):
super().__init__("x", 1, [], label=label)
assert XGate() is XGate()
Interface
The public classes correspond to the standard classes Instruction
and Gate
, respectively, and are subclasses of these.
SingletonInstruction
class qiskit.circuit.singleton.SingletonInstruction(*args, _force_mutable=False, **kwargs)
A base class to use for Instruction
objects that by default are singleton instances.
This class should be used for instruction classes that have fixed definitions and do not contain any unique state. The canonical example of something like this is Measure
which has an immutable definition and any instance of Measure
is the same. Using singleton instructions as a base class for these types of gate classes provides a large advantage in the memory footprint of multiple instructions.
The exception to be aware of with this class though are the Instruction
attributes label
, condition
, duration
, and unit
which can be set differently for specific instances of gates. For SingletonInstruction
usage to be sound setting these attributes is not available and they can only be set at creation time, or on an object that has been specifically made mutable using to_mutable()
. If any of these attributes are used during creation, then instead of using a single shared global instance of the same gate a new separate instance will be created.
SingletonGate
class qiskit.circuit.singleton.SingletonGate(*args, _force_mutable=False, **kwargs)
A base class to use for Gate
objects that by default are singleton instances.
This class is very similar to SingletonInstruction
, except implies unitary Gate
semantics as well. The same caveats around setting attributes in that class apply here as well.
SingletonControlledGate
class qiskit.circuit.singleton.SingletonControlledGate(*args, _force_mutable=False, **kwargs)
A base class to use for ControlledGate
objects that by default are singleton instances
This class is very similar to SingletonInstruction
, except implies unitary ControlledGate
semantics as well. The same caveats around setting attributes in that class apply here as well.
When inheriting from one of these classes, the produced class will have an eagerly created singleton instance that will be returned whenever the class is constructed with arguments that have been defined to be singletons. Typically this will be the defaults. These instances are immutable; attempts to modify their properties will raise TypeError
.
All subclasses of Instruction
have a mutable
property. For most instructions this is True
, while for the singleton instances it is False
. One can use the to_mutable()
method to get a version of the instruction that is owned and safe to mutate.
The singleton instances are not exact instances of their base class; they are special subclasses that cannot construct new objects. This means that:
type(XGate()) is not XGate
You should not rely on type
having an exact value; use isinstance()
instead for type checking. If you need to reliably retrieve the base class from an Instruction
, see the Instruction.base_class
attribute; singleton instances set this correctly. For most cases in using Qiskit, Instruction.name
is a more suitable determiner of what an instruction “means” in a circuit.
Deriving new singletons
The simplest example of deriving a new singleton instruction is simply to inherit from the correct base and supply an __init__()
method that has immutable defaults for any arguments. For example:
from qiskit.circuit.singleton import SingletonInstruction
class MyInstruction(SingletonInstruction):
def __init__(self, label=None):
super().__init__("my_instruction", 1, 0, label=label)
assert MyInstruction() is MyInstruction()
assert MyInstruction(label="some label") is not MyInstruction()
assert MyInstruction(label="some label").mutable
The singleton instance will use all the constructor’s defaults.
You can also derive from an instruction that is itself a singleton. The singleton nature of the class will be inherited, though the singleton instances of the two classes will be different:
class MyOtherInstruction(MyInstruction):
pass
assert MyOtherInstruction() is MyOtherInstruction()
assert MyOtherInstruction() is not MyInstruction()
If for some reason you want to derive from SingletonInstruction
, or one of the related or subclasses but do not want the default singleton instance to be created, such as if you are defining a new abstract base class, you can set the keyword argument create_default_singleton=False
in the class definition:
class NotASingleton(SingletonInstruction, create_default_singleton=False):
def __init__(self):
return super().__init__("my_mutable", 1, 0, [])
assert NotASingleton() is not NotASingleton()
If your constructor does not have defaults for all its arguments, you must set create_default_singleton=False
.
Subclasses of SingletonInstruction
and the other associated classes can control how their constructor’s arguments are interpreted, in order to help the singleton machinery return the singleton even in the case than an optional argument is explicitly set to its default.
_singleton_lookup_key
static SingletonInstruction._singleton_lookup_key(*_args, **_kwargs)
Given the arguments to the constructor, return a key tuple that identifies the singleton instance to retrieve, or None
if the arguments imply that a mutable object must be created.
For performance, as a special case, this method will not be called if the class constructor was given zero arguments (e.g. the construction XGate()
will not call this method, but XGate(label=None)
will), and the default singleton will immediately be returned.
This static method can (and probably should) be overridden by subclasses. The derived signature should match the class’s __init__
; this method should then examine the arguments to determine whether it requires mutability, or what the cache key (if any) should be.
The function should return either None
or valid dict
key (i.e. hashable and implements equality). Returning None
means that the created instance must be mutable. No further singleton-based processing will be done, and the class creation will proceed as if there was no singleton handling. Otherwise, the returned key can be anything hashable and no special meaning is ascribed to it. Whenever this method returns the same key, the same singleton instance will be returned. We suggest that you use a tuple of the values of all arguments that can be set while maintaining the singleton nature.
Only keys that match the default arguments or arguments given to additional_singletons
at class-creation time will actually return singletons; other values will return a standard mutable instance.
The singleton machinery will handle an unhashable return from this function gracefully by returning a mutable instance. Subclasses should ensure that their key is hashable in the happy path, but they do not need to manually verify that the user-supplied arguments are hashable. For example, it’s safe to implement this as:
@staticmethod
def _singleton_lookup_key(*args, **kwargs):
return None if kwargs else args
even though a user might give some unhashable type as one of the args
.
This is set by all Qiskit standard-library gates such that the label
and similar keyword arguments are ignored in the key calculation if they are their defaults, or a mutable instance is returned if they are not.
You can also specify other combinations of constructor arguments to produce singleton instances for, using the additional_singletons
argument in the class definition. This takes an iterable of (args, kwargs)
tuples, and will build singletons equivalent to cls(*args, **kwargs)
. You do not need to handle the case of the default arguments with this. For example, given a class definition:
class MySingleton(SingletonGate, additional_singletons=[((2,), {"label": "two"})]):
def __init__(self, n=1, label=None):
super().__init__("my", n, [], label=label)
@staticmethod
def _singleton_lookup_key(n=1, label=None):
return (n, label)
there will be two singleton instances instantiated. One corresponds to n=1
and label=None
, and the other to n=2
and label="two"
. Whenever MySingleton
is constructed with arguments consistent with one of those two cases, the relevant singleton will be returned. For example:
assert MySingleton() is MySingleton(1, label=None)
assert MySingleton(2, "two") is MySingleton(n=2, label="two")
The case of the class being instantiated with zero arguments is handled specially to allow an absolute fast-path for inner-loop performance (although the general machinery is not desperately slow anyway).
Implementation
This section is primarily developer documentation for the code; none of the machinery described here is public, and it is not safe to inherit from any of it directly.
There are several moving parts to tackle here. The behavior of having XGate()
return some singleton object that is an (inexact) instance of XGate
but without calling __init__
requires us to override type.__call__
. This means that XGate
must have a metaclass that defines __call__
to return the singleton instance.
Next, we need to ensure that there is a singleton instance for XGate()
to return. This can be done dynamically on each call (i.e. check if the instance exists and create it if not), but since we also want that instance to be very special, it’s easier to hook in and create it during the definition of the XGate
type object. This also has the advantage that we do not need to make the singleton object pickleable; we only need to specify where to retrieve it from during the unpickle, because the creation of the base type object will recreate the singleton.
We want the singleton instance to:
- be immutable; it should reject all attempts to mutate itself.
- have exactly the same state as an
XGate()
would have had if there was no singleton handling.
We do this in a three-step procedure:
- Before creating any singletons, we separately define the overrides needed to make an
Instruction
and aGate
immutable. This is_SingletonInstructionOverrides
and the other_*Overrides
classes. - While we are creating the
XGate
type object, we dynamically also create a subclass of it that has the immutable overrides in its method-resolution order in the correct place. These override the standard methods / properties that are defined on the mutable gate (we do not attempt to override any cases where the type object we are creating has extra inplace methods). - We can’t instantiate this new subclass, because when it calls
XGate.__init__
, it will attempt to set some attributes, and these will be rejected by immutability. Instead, we first create a completely regularXGate
instance, and then we dynamically change its type to the singleton class, freezing it.
We could do this entirely within the metaclass machinery, but that would require XGate
to be defined as something like:
class XGate(Gate, metaclass=_SingletonMeta, overrides=_SingletonGateOverrides): ...
which is super inconvenient (or we’d have to have _SingletonMeta
do a bunch of fragile introspection). Instead, we use the abc.ABC
/abc.ABCMeta
pattern of defining a concrete middle class (SingletonGate
in the XGate
case) that sets the metaclass, selects the overrides to be applied, and has an __init_subclass__()
that applies the singleton-subclass-creation steps above. The overrides are in separate classes so that mutable XGate
instances do not have them in their own method-resolution orders; doing this is easier to implement, but requires all the setters and checkers to dance around at runtime trying to validate whether mutating the instance is allowed.
Finally, to actually build all this machinery up, the base is _SingletonMeta
, which is a metaclass compatible with any metaclass of Instruction
. This defines the __call__()
machinery that overrides type.__call__
to return the singleton instances. The other component of it is its __new__()
, which is called (non-trivially) during the creation of SingletonGate
and SingletonInstruction
with its overrides
keyword argument set to define the __init_subclass__
of those classes with the above properties. We use the metaclass to add this method dynamically, because the __init_subclass__()
machinery wants to be abstract, closing over the overrides
and the base class, but still able to call super
. It’s more convenient to do this dynamically, closing over the desired class variable and using the two-argument form of super
, since the zero-argument form does magic introspection based on where its containing function was defined.
Handling multiple singletons requires storing the initialization arguments in some form, to allow the to_mutable()
method and pickling to be defined. We do this as a lookup dictionary on the singleton type object. This is logically an instance attribute, but because we need to dynamically switch in the dynamic _Singleton type onto an instance of the base type, that gets rather complex; either we have to require that the base already has an instance dictionary, or we risk breaking the __slots__
layout during the switch. Since the singletons have lifetimes that last until garbage collection of their base class’s type object, we can fake out this instance dictionary using a type-object dictionary that maps instance pointers to the data we want to store. An alternative would be to build a new type object for each individual singleton that closes over (or stores) the initializer arguments, but type objects are quite heavy and the principle is largely same anyway.