Instructions Singleton
qiskit.circuit.singleton
Les mécanismes de ce module permettent de définir des sous-classes de Instruction et Gate qui, lors de leur instanciation, renvoient de préférence une instance singleton partagée et immuable. Si l'on prend l'exemple de XGate, le résultat final pour l'utilisateur est le suivant :
- Il existe une classe standard appelée
XGate, qui dérive deGate. - Une opération de ce type
XGate(label="my_gate")génère un objet dont le type est exactementXGate, et la mutabilité fonctionne parfaitement comme prévu; toutes les méthodes renvoient exactement celles définies parXGate,Gate, ou les classes parentes. - Cette opération
XGate()crée un objet singleton dont le type est une classe synthétique_SingletonXGate, qui hérite deXGatemais redéfinit__setattr__()pour se rendre immuable. L'objet lui-même possède exactement les mêmes attributs d'instance queXGate()ceux qu'il aurait s'il n'y avait pas de gestion de singleton. Cet objet se renverra lui-même souscopy(),deepcopy()et effectuera un aller-retour viapickle.
MeasureIl en va de même, par exemple, pour, sauf qu'il s'agit d'une sous-classe de Instruction uniquement, et non de Gate.
Les classes de ce module sont destinées à un usage avancé, car elles sont étroitement liées au cœur du modèle de données de Qiskit pour les circuits.
Du point de vue d'un auteur de bibliothèque, le minimum requis pour doter une classe Gate ou Instruction de ce comportement consiste à hériter de SingletonGate (SingletonInstruction) plutôt que de Gate (Instruction), et à faire en sorte que la __init__ méthode dispose de valeurs par défaut pour tous ses arguments (celles-ci correspondront à l'état de l'instance singleton). Par exemple :
class XGate(SingletonGate):
def __init__(self, label=None):
super().__init__("x", 1, [], label=label)
assert XGate() is XGate()Interface
Les classes publiques correspondent respectivement aux classes Instruction standard et Gate, dont elles sont des sous-classes.
SingletonInstruction
class qiskit.circuit.singleton.SingletonInstruction(*args, _force_mutable=False, **kwargs)
Bases : Instruction, _SingletonBase
Une classe de base destinée aux Instruction objets qui, par défaut, sont des instances singleton.
Cette classe doit être utilisée pour les classes d'instruction dont les définitions sont fixes et qui ne contiennent pas d'état unique. L'exemple canonique de quelque chose comme cela est Measure qui a une définition immuable et toute instance de Measure est identique. L'utilisation d'instructions singleton comme classe de base pour ces types de classes de portes permet de réduire considérablement l'empreinte mémoire des instructions multiples.
Il convient toutefois de noter qu'il existe une exception à cette règle : certains Instruction attributs label peuvent être définis différemment pour certaines instances spécifiques de portails. Pour garantir SingletonInstruction une utilisation correcte, ces attributs ne peuvent pas être modifiés; ils ne peuvent être définis qu’au moment de la création, ou sur un objet qui a été expressément rendu modifiable à l’aide de to_mutable(). Si l'un de ces attributs est utilisé lors de la création, une nouvelle instance distincte sera créée, au lieu d'utiliser une seule instance globale partagée de la même porte.
SingletonGate
class qiskit.circuit.singleton.SingletonGate(*args, _force_mutable=False, **kwargs)
Bases : Gate, _SingletonBase
Une classe de base destinée aux Gate objets qui, par défaut, sont des instances singleton.
Cette classe est très similaire à SingletonInstruction, sauf qu'elle implique également une sémantique unitaire Gate . Les mêmes mises en garde concernant la définition des attributs dans cette classe s'appliquent également ici.
SingletonControlledGate
class qiskit.circuit.singleton.SingletonControlledGate(*args, _force_mutable=False, **kwargs)
Bases : ControlledGate, _SingletonBase
Une classe de base destinée aux ControlledGate objets qui, par défaut, sont des instances singleton
Cette classe est très similaire à SingletonInstruction, sauf qu'elle implique également une sémantique unitaire ControlledGate . Les mêmes mises en garde concernant la définition des attributs dans cette classe s'appliquent également ici.
Lorsqu'elle hérite de l'une de ces classes, la classe produite aura une instance singleton créée avec empressement qui sera renvoyée chaque fois que la classe sera construite avec des arguments qui ont été définis comme étant des singletons. En général, il s'agit des valeurs par défaut. Ces instances sont immuables; toute tentative de modification de leurs propriétés entraînera l'apparition de TypeError.
Toutes les sous-classes de Instruction possèdent une mutable propriété. Pour la plupart des instructions, c'est True, tandis que pour les instances singleton, c'est False. On peut utiliser cette to_mutable() méthode pour obtenir une version de l'instruction dont on est propriétaire et qu'on peut modifier en toute sécurité.
Les instances singleton ne sont pas des instances exactes de leur classe de base; ce sont des sous-classes spéciales qui ne peuvent pas construire de nouveaux objets. Ce qui signifie que :
type(XGate()) is not XGateVous ne devez pas vous fier à type une valeur exacte; utilisez isinstance() plutôt cette méthode pour la vérification de type. Si vous avez besoin de récupérer de manière fiable la classe de base d'un Instruction, consultez l'attribut Instruction.base_class ; les instances singleton le définissent correctement. Dans la plupart des cas où l’on utilise Qiskit, Instruction.name permet de mieux déterminer ce que « signifie » une instruction dans un circuit.
Dérivation de nouveaux singletons
L'exemple le plus simple de dérivation d'une nouvelle instruction singleton consiste simplement à hériter de la bonne base et à fournir une méthode __init__() dont les valeurs par défaut des arguments sont immuables. Par exemple :
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").mutableL'instance singleton utilisera toutes les valeurs par défaut du constructeur.
Vous pouvez également dériver d'une instruction qui est elle-même un singleton. La nature singleton de la classe sera héritée, bien que les instances singleton des deux classes soient différentes :
class MyOtherInstruction(MyInstruction):
pass
assert MyOtherInstruction() is MyOtherInstruction()
assert MyOtherInstruction() is not MyInstruction()Si pour une raison quelconque vous souhaitez dériver de SingletonInstruction, ou l'une des classes liées ou sous-classes, mais vous ne souhaitez pas que l'instance singleton par défaut soit créée, par exemple si vous définissez une nouvelle classe de base abstraite, vous pouvez définir l'argument mot-clé create_default_singleton=False dans la définition de classe :
class NotASingleton(SingletonInstruction, create_default_singleton=False):
def __init__(self):
return super().__init__("my_mutable", 1, 0, [])
assert NotASingleton() is not NotASingleton()Si votre constructeur n'a pas de valeurs par défaut pour tous ses arguments, vous devez définir create_default_singleton=False.
Les sous-classes de SingletonInstruction et les autres classes associées peuvent contrôler la manière dont les arguments de leur constructeur sont interprétés, afin d'aider le mécanisme de singleton à renvoyer le singleton même lorsqu'un argument facultatif est explicitement défini sur sa valeur par défaut.
_singleton_lookup_key
static SingletonInstruction._singleton_lookup_key(*_args, **_kwargs)
Compte tenu des arguments du constructeur, renvoie un tuple de clés qui identifie l'instance singleton à récupérer, ou None si les arguments impliquent qu'un objet mutable doit être créé.
Pour des raisons de performance, cette méthode ne sera pas appelée si le constructeur de la classe a reçu zéro argument (par exemple, la construction XGate() n'appellera pas cette méthode, mais XGate(label=None) le fera), et le singleton par défaut sera immédiatement renvoyé.
Cette méthode statique peut (et devrait probablement) être surchargée par les sous-classes. La signature dérivée doit correspondre à celle de la classe __init__; cette méthode doit alors examiner les arguments pour déterminer si elle nécessite une mutabilité ou quelle doit être la clé de cache (le cas échéant).
La fonction doit renvoyer None ou une clé dict valide (c'est-à-dire hachable et implémentant l'égalité). Le retour de None signifie que l'instance créée doit être mutable. Aucun autre traitement basé sur les singlettes ne sera effectué, et la création de la classe se déroulera comme s'il n'y avait pas de gestion des singlettes. Sinon, la clé renvoyée peut être n'importe quelle clé hachable et aucune signification particulière ne lui est attribuée. Chaque fois que cette méthode renvoie la même clé, la même instance singleton sera renvoyée. Nous vous suggérons d'utiliser un tuple des valeurs de tous les arguments qui peuvent être définis tout en conservant la nature singleton.
Seules les clés qui correspondent aux arguments par défaut ou aux arguments donnés à additional_singletons au moment de la création de la classe renverront des singletons; les autres valeurs renverront une instance mutable standard.
La machine singleton gérera un retour non caché de cette fonction de manière élégante en renvoyant une instance mutable. Les sous-classes doivent s'assurer que leur clé est hachable dans le chemin heureux, mais elles n'ont pas besoin de vérifier manuellement que les arguments fournis par l'utilisateur sont hachables. Par exemple, il est possible de l'implémenter en toute sécurité sous la forme suivante :
@staticmethod
def _singleton_lookup_key(*args, **kwargs):
return None if kwargs else argsmême si un utilisateur peut donner un type non cachable comme l'un des args.
Il est défini par toutes les portes de la bibliothèque standard de Qiskit, de sorte que les arguments label et autres mots-clés similaires sont ignorés dans le calcul de la clé s'ils sont par défaut, ou qu'une instance mutable est renvoyée s'ils ne le sont pas.
Vous pouvez également spécifier d'autres combinaisons d'arguments de constructeur pour produire des instances singleton, en utilisant l'argument additional_singletons dans la définition de la classe. Elle prend un itérable de tuples (args, kwargs) et construit des singletons équivalents à cls(*args, **kwargs). Il n'est pas nécessaire de gérer le cas des arguments par défaut. Par exemple, étant donné la définition d'une classe :
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)il y aura deux instances singleton instanciées. L'une correspond à n=1 et label=None, et l'autre à n=2 et label="two". Chaque fois que MySingleton est construit avec des arguments compatibles avec l'un de ces deux cas, le singleton correspondant sera renvoyé. Par exemple :
assert MySingleton() is MySingleton(1, label=None)
assert MySingleton(2, "two") is MySingleton(n=2, label="two")Le cas où la classe est instanciée avec zéro argument est traité spécialement pour permettre une voie rapide absolue pour la performance de la boucle interne (bien que la machinerie générale ne soit pas désespérément lente de toute façon).
Implémentation
Cette section est principalement une documentation pour les développeurs; aucun des mécanismes décrits ici n'est public, et il n'est pas sûr d'en hériter directement.
Il y a plusieurs aspects à aborder ici. Le fait de renvoyer XGate() un objet singleton qui est une instance (imparfaite) de XGate sans pour autant appeler __init__ nous oblige à redéfinir type.__call__. Cela signifie que XGate doit disposer d'une métaclasse qui définit __call__ de manière à renvoyer l'instance singleton.
Ensuite, nous devons nous assurer qu'il existe une instance singleton à renvoyer à XGate() . Cela peut être fait dynamiquement à chaque appel (c'est-à-dire vérifier si l'instance existe et la créer si ce n'est pas le cas), mais comme nous voulons aussi que cette instance soit très spéciale, il est plus facile de la créer lors de la définition de l'objet de type XGate . Cela présente également l'avantage qu'il n'est pas nécessaire de rendre l'objet singleton prélevable; il suffit de spécifier où le récupérer lors du dépiquage, car la création de l'objet de type base recréera l'objet singleton.
Nous voulons que l'instance singleton :
- être immuable; il doit rejeter toutes les tentatives de mutation.
- ont exactement le même état qu'un
XGate()aurait eu s'il n'y avait pas eu de gestion de singleton.
Nous procédons en trois étapes :
- Avant de créer des singletons, nous définissons séparément les redéfinitions nécessaires pour rendre un
Instructionet unGateimmuables. Il s'agit_SingletonInstructionOverridesde cette classe et des autres_*Overridesclasses. - Pendant que nous créons l'objet de type
XGate, nous créons aussi dynamiquement une sous-classe de celui-ci qui possède les surcharges immuables dans l'ordre de résolution des méthodes au bon endroit. Celles-ci remplacent les méthodes/propriétés standard définies sur le portail mutable (nous ne tentons pas de remplacer les cas où l'objet de type que nous créons possède des méthodes supplémentaires sur place). - Nous ne pouvons pas instancier cette nouvelle sous-classe, car lorsqu'elle appellera
XGate.__init__, elle tentera de définir certains attributs, qui seront rejetés par l'immutabilité. Au lieu de cela, nous créons d'abord une instanceXGatetout à fait normale, puis nous changeons dynamiquement son type en classe singleton, ce qui la fige.
Nous pourrions le faire entièrement dans le cadre de la machinerie des métaclasses, mais cela nécessiterait que XGate soit défini comme quelque chose du genre :
class XGate(Gate, metaclass=_SingletonMeta, overrides=_SingletonGateOverrides): ...ce qui est vraiment pénible (sinon, on serait obligés de se _SingletonMeta livrer à toute une série d'introspections délicates). abc.ABCAu lieu de cela, nous utilisons le modèle consistantabc.ABCMeta à définir une classe intermédiaire concrète (SingletonGate dans ce XGate cas précis) qui définit la métaclasse, sélectionne les redéfinitions à appliquer et dispose d’un __init_subclass__() mécanisme qui met en œuvre les étapes de création de sous-classes singleton décrites ci-dessus. Les redéfinitions sont placées dans des classes distinctes afin que les instances *modifiables *XGate ne les intègrent pas dans leur propre ordre de résolution des méthodes; cette approche est plus simple à mettre en œuvre, mais oblige tous les accesseurs et vérificateurs à « jongler » lors de l'exécution pour tenter de valider si la modification de l'instance est autorisée.
Enfin, pour mettre en place tout cet ensemble, la base est _SingletonMeta, qui est une métaclasse compatible avec n’importe quelle métaclasse de Instruction. Cela définit le __call__() mécanisme qui remplace type.__call__ la méthode par défaut afin de renvoyer des instances singleton. L'autre composante de ce mécanisme est sa __new__(), qui est appelée (de manière non triviale) lors de la création de SingletonGate et SingletonInstruction avec son overrides argument-clé défini pour déterminer le __init_subclass__ de ces classes présentant les propriétés mentionnées ci-dessus. Nous utilisons la métaclasse pour ajouter cette méthode de manière dynamique, car le __init_subclass__() mécanisme doit rester abstrait, en capturant la classe overrides et la classe de base, tout en conservant la possibilité d'appeler super. Il est plus pratique de procéder de manière dynamique, en capturant la variable de classe souhaitée et en utilisant la forme à deux arguments de super, car la forme sans argument effectue une introspection « magique » en fonction de l'endroit où la fonction qui la contient a été définie.
La gestion de plusieurs singletons nécessite de stocker les arguments d'initialisation sous une forme ou une autre, afin de permettre la définition de to_mutable() la méthode et la sérialisation. Nous procédons ainsi en utilisant un dictionnaire de recherche sur l 'objet de type singleton. Il s'agit logiquement d'un attribut d'instance, mais comme nous devons appliquer dynamiquement le type dynamique _Singleton à une instance du type de base, cela devient assez complexe; soit nous devons exiger que le type de base dispose déjà d'un dictionnaire d'instances, soit nous risquons de perturber la __slots__ structure lors de la conversion. Étant donné que les singletons ont une durée de vie qui s'étend jusqu'au ramassage des ordures de l'objet-type de leur classe de base, nous pouvons simuler ce dictionnaire d'instances à l'aide d'un dictionnaire d'objets-types qui associe les pointeurs d'instances aux données que nous souhaitons stocker. Une autre solution consisterait à créer un nouvel objet de type pour chaque singleton, qui encapsulerait (ou stockerait) les arguments de l'initialiseur, mais les objets de type sont assez lourds et, de toute façon, le principe reste globalement le même.