Skip to main content
IBM Quantum Platform

Instrucciones Singleton

qiskit.circuit.singleton

La maquinaria de este módulo sirve para definir subclases de Instruction y Gate que devuelven preferentemente una instancia única inmutable compartida cuando se instancian. Tomando el ejemplo de XGateel resultado final de cara al usuario es el siguiente:

  • Existe una clase regular llamada XGate, que deriva de Gate.
  • Haciendo algo como XGate(label="my_gate") se obtiene un objeto cuyo tipo es exactamente XGate, y toda la mutabilidad funciona completamente como se esperaba; todos los métodos se resuelven exactamente a los definidos por XGate, Gateo padres.
  • Haciendo XGate() se produce un objeto singleton cuyo tipo es una clase sintética _SingletonXGate , que deriva XGate pero anula __setattr__() para hacerse inmutable. El objeto en sí tiene precisamente los mismos atributos de instancia que tendría XGate() si no hubiera manejo de singletons. Este objeto se devolverá bajo copy(), deepcopy() y de ida y vuelta a través de pickle.

Lo mismo puede ocurrir, por ejemplo, con Measureexcepto que es una subclase de Instruction y no de Gate.

Nota

Las clases de este módulo son de uso avanzado, porque están estrechamente entrelazadas con el corazón del modelo de datos de Qiskit para circuitos.

Desde la perspectiva de una biblioteca-autor, lo mínimo que se necesita para mejorar una Gate o Instruction con este comportamiento es heredar de SingletonGate (SingletonInstruction) en lugar de Gate (Instruction), y que el método __init__ tenga valores por defecto para todos sus argumentos (éstos serán el estado de la instancia singleton). Por ejemplo:

class XGate(SingletonGate):
    def __init__(self, label=None):
        super().__init__("x", 1, [], label=label)

assert XGate() is XGate()

Interfaz

Las clases públicas corresponden a las clases estándar Instruction y Gaterespectivamente, y son subclases de éstas.

SingletonInstruction

class qiskit.circuit.singleton.SingletonInstruction(*args, _force_mutable=False, **kwargs)

GitHub

Bases: Instruction, _SingletonBase

Una clase base para Instruction objetos que por defecto son instancias singleton.

Esta clase debe utilizarse para clases de instrucciones que tengan definiciones fijas y no contengan ningún estado único. El ejemplo canónico de algo así es Measure que tiene una definición inmutable y cualquier instancia de Measure es la misma. El uso de instrucciones singleton como clase base para este tipo de clases de puertas proporciona una gran ventaja en la huella de memoria de las instrucciones múltiples.

La excepción a tener en cuenta con esta clase son los atributos Instruction atributo label que pueden establecerse de forma diferente para instancias específicas de puertas. Para que SingletonInstruction el ajuste de estos atributos no está disponible y sólo se puede establecer en el momento de la creación, o en un objeto que se ha hecho específicamente mutable utilizando to_mutable(). Si se utiliza alguno de estos atributos durante la creación, en lugar de utilizar una única instancia global compartida de la misma puerta se creará una nueva instancia independiente.

SingletonGate

class qiskit.circuit.singleton.SingletonGate(*args, _force_mutable=False, **kwargs)

GitHub

Bases: Gate, _SingletonBase

Una clase base para Gate objetos que por defecto son instancias singleton.

Esta clase es muy similar a SingletonInstructionexcepto que implica una semántica Gate unitaria. Las mismas advertencias sobre el establecimiento de atributos en esa clase se aplican aquí también.

SingletonControlledGate

class qiskit.circuit.singleton.SingletonControlledGate(*args, _force_mutable=False, **kwargs)

GitHub

Bases: ControlledGate, _SingletonBase

Una clase base para ControlledGate objetos que por defecto son instancias singleton

Esta clase es muy similar a SingletonInstructionexcepto que implica una semántica ControlledGate unitaria. Las mismas advertencias sobre el establecimiento de atributos en esa clase se aplican aquí también.

Cuando se hereda de una de estas clases, la clase producida tendrá una instancia singleton creada ansiosamente que será devuelta siempre que la clase se construya con argumentos que hayan sido definidos como singletons. Normalmente serán los valores por defecto. Estas instancias son inmutables; si se intenta modificar sus propiedades, se producirá el error TypeError.

Todas las subclases de Instruction tienen una propiedad mutable propiedad. Para la mayoría de las instrucciones es True, mientras que para las instancias únicas es False. Se puede utilizar el método to_mutable() para obtener una versión de la instrucción que sea propia y segura para mutar.

Las instancias singleton no son instancias exactas de su clase base; son subclases especiales que no pueden construir nuevos objetos. Esto significa que:

type(XGate()) is not XGate

No debe confiar en type tenga un valor exacto; utilice isinstance() para la comprobación de tipos. Si necesita recuperar de forma fiable la clase base de un archivo Instructionconsulte el atributo Instruction.base_class las instancias singleton lo establecen correctamente. Para la mayoría de los casos en el uso de Qiskit Instruction.name es un determinante más adecuado de lo que una instrucción "significa" en un circuito.

Derivación de nuevos singletons

El ejemplo más simple de derivar una nueva instrucción singleton es simplemente heredar de la base correcta y suministrar un método __init__() que tiene valores predeterminados inmutables para cualquier argumento. Por ejemplo:

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

La instancia singleton utilizará todos los valores predeterminados del constructor.

También se puede derivar de una instrucción que sea a su vez un singleton. La naturaleza singleton de la clase se heredará, aunque las instancias singleton de las dos clases serán diferentes:

class MyOtherInstruction(MyInstruction):
    pass

assert MyOtherInstruction() is MyOtherInstruction()
assert MyOtherInstruction() is not MyInstruction()

Si por alguna razón desea derivar de SingletonInstructiono de una de las clases relacionadas o subclases pero no desea que se cree la instancia singleton por defecto, como por ejemplo si está definiendo una nueva clase base abstracta, puede establecer el argumento de palabra clave create_default_singleton=False en la definición de la clase:

class NotASingleton(SingletonInstruction, create_default_singleton=False):
    def __init__(self):
        return super().__init__("my_mutable", 1, 0, [])

assert NotASingleton() is not NotASingleton()

Si su constructor no tiene valores por defecto para todos sus argumentos, debe establecer create_default_singleton=False.

Las subclases de SingletonInstruction y las demás clases asociadas pueden controlar cómo se interpretan los argumentos de su constructor, con el fin de ayudar al mecanismo de singleton a devolver el singleton incluso en el caso de que un argumento opcional se establezca explícitamente en su valor por defecto.

_singleton_lookup_key

static SingletonInstruction._singleton_lookup_key(*_args, **_kwargs)

GitHub

Dados los argumentos del constructor, devuelve una tupla clave que identifica la instancia singleton a recuperar, o None si los argumentos implican que debe crearse un objeto mutable.

Por rendimiento, como caso especial, este método no será llamado si al constructor de la clase se le han dado cero argumentos (por ejemplo, la construcción XGate() no llamará a este método, pero XGate(label=None) sí), e inmediatamente se devolverá el singleton por defecto.

Este método estático puede (y probablemente debería) ser sobrescrito por subclases. La firma derivada debe coincidir con la de la clase __init__; este método debe entonces examinar los argumentos para determinar si requiere mutabilidad, o cuál debe ser la clave de caché (si la hay).

La función debe devolver None o una clave dict válida (es decir, hashable e implementa la igualdad). Devolver None significa que la instancia creada debe ser mutable. No se realizará ningún otro procesamiento basado en singletons, y la creación de la clase procederá como si no hubiera manejo de singletons. En caso contrario, la clave devuelta puede ser cualquier cosa hashable y no se le atribuye ningún significado especial. Siempre que este método devuelva la misma clave, se devolverá la misma instancia singleton. Le sugerimos que utilice una tupla de los valores de todos los argumentos que pueden establecerse manteniendo la naturaleza de singleton.

Sólo las claves que coincidan con los argumentos por defecto o los argumentos dados a additional_singletons en el momento de la creación de la clase devolverán realmente singletons; otros valores devolverán una instancia mutable estándar.

Nota

La maquinaria del singleton manejará un retorno no hash de esta función con gracia devolviendo una instancia mutable. Las subclases deben asegurarse de que su clave es hashable en la ruta feliz, pero no necesitan verificar manualmente que los argumentos proporcionados por el usuario son hashables. Por ejemplo, es seguro implementar esto como:

@staticmethod
def _singleton_lookup_key(*args, **kwargs):
    return None if kwargs else args

a pesar de que un usuario pueda dar algún tipo no hashable como uno de los args.

Esto es establecido por todas las puertas de la biblioteca estándar de Qiskit de tal forma que los argumentos label y palabras clave similares son ignorados en el cálculo de la clave si son sus valores por defecto, o se devuelve una instancia mutable si no lo son.

También puede especificar otras combinaciones de argumentos del constructor para producir instancias singleton, utilizando el argumento additional_singletons en la definición de la clase. Toma un iterable de tuplas (args, kwargs) , y construirá singletons equivalentes a cls(*args, **kwargs). No es necesario manejar el caso de los argumentos por defecto con esto. Por ejemplo, dada una definición de clase

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)

habrá dos instancias singleton. Uno corresponde a n=1 y label=None, y el otro a n=2 y label="two". Siempre que se construya MySingleton con argumentos consistentes con uno de esos dos casos, se devolverá el singleton correspondiente. Por ejemplo:

assert MySingleton() is MySingleton(1, label=None)
assert MySingleton(2, "two") is MySingleton(n=2, label="two")

El caso de que la clase se instancie con cero argumentos se trata de forma especial para permitir una ruta rápida absoluta para el rendimiento del bucle interno (aunque la maquinaria general no es desesperadamente lenta de todos modos).


Implementación

Nota

Esta sección es principalmente documentación de desarrollo para el código; ninguna de la maquinaria descrita aquí es pública, y no es seguro heredar de ella directamente.

Aquí hay varias partes móviles que abordar. El comportamiento de hacer que XGate() devuelva algún objeto singleton que sea una instancia (inexacta) de XGate pero sin llamar a __init__ nos obliga a sobreescribir type.__call__. Esto significa que XGate debe tener una metaclase que defina __call__ para devolver la instancia singleton.

A continuación, tenemos que asegurarnos de que existe una instancia singleton para que XGate() la devuelva. Esto se puede hacer dinámicamente en cada llamada (es decir, comprobar si la instancia existe y crearla si no), pero como también queremos que esa instancia sea muy especial, es más fácil engancharla y crearla durante la definición del objeto de tipo XGate . Esto también tiene la ventaja de que no necesitamos hacer el objeto singleton pickleable; sólo necesitamos especificar de dónde recuperarlo durante el unpickle, porque la creación del objeto de tipo base recreará el singleton.

Queremos que la instancia singleton:

  • ser inmutable; debe rechazar todo intento de mutarse.
  • tienen exactamente el mismo estado que tendría un XGate() si no hubiera manejo de singletons.

Lo hacemos en tres pasos:

  1. Antes de crear cualquier singleton, definimos por separado las anulaciones necesarias para hacer un Instruction y a Gate sean inmutables. Se trata de _SingletonInstructionOverrides y las demás clases de _*Overrides .
  2. Mientras creamos el objeto de tipo XGate , también creamos dinámicamente una subclase del mismo que tiene los overrides inmutables en su orden de resolución de métodos en el lugar correcto. Estos anulan los métodos / propiedades estándar que están definidos en la puerta mutable (no intentamos anular ningún caso en el que el objeto de tipo que estamos creando tenga métodos extra inplace).
  3. No podemos instanciar esta nueva subclase, porque cuando llame a XGate.__init__, intentará establecer algunos atributos, y estos serán rechazados por la inmutabilidad. En su lugar, primero creamos una instancia completamente normal de XGate , y luego cambiamos dinámicamente su tipo a la clase singleton, congelándola.

Podríamos hacerlo completamente dentro de la maquinaria de metaclases, pero eso requeriría que XGate se definiera como algo así:

class XGate(Gate, metaclass=_SingletonMeta, overrides=_SingletonGateOverrides): ...

lo cual es súper inconveniente (o tendríamos que hacer que _SingletonMeta hiciera un montón de frágiles introspecciones). En su lugar, utilizamos la directiva abc.ABC/abc.ABCMeta para definir una clase media concreta (SingletonGate en el XGate ) que establece la metaclase, selecciona las modificaciones que se aplicarán y tiene una función __init_subclass__() que aplica los pasos de creación de subclases singleton anteriores. Las anulaciones están en clases separadas para que sean *mutables. *XGate las instancias no los tienen en sus propios órdenes de resolución de métodos; hacer esto es más fácil de implementar, pero requiere que todos los configuradores y verificadores bailen en el tiempo de ejecución tratando de validar si se permite mutar la instancia.

Por último, para construir realmente toda esta maquinaria, la base es _SingletonMeta, que es una metaclase compatible con cualquier metaclase de Instruction. Esto define la __call__() que sustituye a type.__call__ para devolver las instancias singleton. El otro componente es su __new__()que se invoca (de forma no trivial) durante la creación de SingletonGate y SingletonInstruction con su argumento de palabra clave overrides establecido para definir el __init_subclass__ de aquellas clases con las propiedades anteriores. Usamos la metaclase para añadir este método dinámicamente, porque la __init_subclass__() quiere ser abstracta, cerrándose sobre overrides y la clase base, pero pudiendo llamar a super. Es más conveniente hacer esto dinámicamente, cerrando sobre la variable de clase deseada y usando la forma de dos argumentos de superya que la forma de cero argumentos hace una introspección mágica basada en dónde fue definida su función contenedora.

Manejar múltiples singletons requiere almacenar los argumentos de inicialización de alguna forma, para permitir que el to_mutable() y el decapado. Hacemos esto como un diccionario de búsqueda en el objeto de tipo singleton. Esto es lógicamente un atributo de instancia, pero debido a que necesitamos cambiar dinámicamente el tipo dinámico _Singleton a una instancia del tipo base, esto se vuelve bastante complejo; o tenemos que requerir que la base ya tenga un diccionario de instancia, o nos arriesgamos a romper el diseño __slots__ durante el cambio. Dado que los singletons tienen un tiempo de vida que dura hasta la recolección de basura del objeto tipo de su clase base, podemos falsear este diccionario de instancias utilizando un diccionario tipo-objeto que asigne punteros de instancia a los datos que queremos almacenar. Una alternativa sería construir un nuevo objeto de tipo para cada singleton individual que cierra sobre (o almacena) los argumentos del inicializador, pero los objetos de tipo son bastante pesados y el principio es en gran medida el mismo de todos modos.

¿Le ha resultado útil esta página?
Informe de un error, de una errata o solicite contenido en GitHub.