Instruções Singleton
qiskit.circuit.singleton
O mecanismo desse módulo serve para definir subclasses de Instruction e Gate que retornam preferencialmente uma instância singleton imutável compartilhada quando instanciadas. Tomando o exemplo de XGateo resultado final para o usuário é que:
- Há uma classe regular chamada
XGate, que deriva deGate. - Fazer algo como
XGate(label="my_gate")produz um objeto cujo tipo é exatamenteXGate, e toda a mutabilidade funciona completamente como esperado; todos os métodos resolvem exatamente aqueles definidos porXGate,Gate, ou pais. - Fazer
XGate()produz um objeto singleton cujo tipo é uma classe_SingletonXGatesintética, que derivaXGatemas substitui__setattr__()para se tornar imutável. O objeto em si tem exatamente os mesmos atributos de instância que oXGate()teria se não houvesse tratamento de singleton. Esse objeto retornará a si mesmo sobcopy(),deepcopy()e fará uma viagem de ida e volta através depickle.
O mesmo pode ser verdadeiro para, por exemplo, Measureexceto pelo fato de ser uma subclasse de Instruction apenas, e não Gate.
As classes deste módulo são para uso avançado, pois estão intimamente ligadas ao coração do modelo de dados do Qiskit para circuitos.
Do ponto de vista de um autor de biblioteca, o mínimo necessário para aprimorar um Gate ou Instruction com esse comportamento é herdar de SingletonGate (SingletonInstruction) em vez de Gate (Instruction), e que o método __init__ tenha padrões para todos os seus argumentos (que serão o estado da instância singleton). Por exemplo:
class XGate(SingletonGate):
def __init__(self, label=None):
super().__init__("x", 1, [], label=label)
assert XGate() is XGate()Interface
As classes públicas correspondem às classes padrão Instruction e Gaterespectivamente, e são subclasses dessas classes.
SingletonInstruction
class qiskit.circuit.singleton.SingletonInstruction(*args, _force_mutable=False, **kwargs)
Bases: Instruction, _SingletonBase
Uma classe base a ser usada para Instruction objetos que, por padrão, são instâncias de singleton.
Essa classe deve ser usada para classes de instrução que tenham definições fixas e não contenham nenhum estado exclusivo. O exemplo canônico de algo assim é Measure que tem uma definição imutável e qualquer instância de Measure é a mesma. O uso de instruções singleton como classe base para esses tipos de classes de portas oferece uma grande vantagem no espaço ocupado pela memória de várias instruções.
No entanto, a exceção a ser observada nessa classe é o Instruction atributo label que podem ser definidos de forma diferente para instâncias específicas de gates. Para que o uso do SingletonInstruction a configuração desses atributos não está disponível e eles só podem ser definidos no momento da criação ou em um objeto que tenha sido especificamente tornado mutável usando to_mutable(). Se algum desses atributos for usado durante a criação, em vez de usar uma única instância global compartilhada do mesmo portão, será criada uma nova instância separada.
SingletonGate
class qiskit.circuit.singleton.SingletonGate(*args, _force_mutable=False, **kwargs)
Bases: Gate, _SingletonBase
Uma classe base a ser usada para Gate objetos que, por padrão, são instâncias de singleton.
Essa classe é muito semelhante a SingletonInstructionexceto pelo fato de implicar a semântica unitária Gate unitária também. As mesmas advertências sobre a definição de atributos nessa classe também se aplicam aqui.
SingletonControlledGate
class qiskit.circuit.singleton.SingletonControlledGate(*args, _force_mutable=False, **kwargs)
Bases: ControlledGate, _SingletonBase
Uma classe base a ser usada para ControlledGate objetos que, por padrão, são instâncias singleton
Essa classe é muito semelhante a SingletonInstructionexceto pelo fato de implicar a semântica unitária ControlledGate unitária também. As mesmas advertências sobre a definição de atributos nessa classe também se aplicam aqui.
Ao herdar de uma dessas classes, a classe produzida terá uma instância de singleton criada com antecedência que será retornada sempre que a classe for construída com argumentos que foram definidos como singletons. Normalmente, esse será o padrão. Essas instâncias são imutáveis; tentativas de modificar suas propriedades gerarão TypeError.
Todas as subclasses de Instruction têm uma propriedade mutable propriedade. Para a maioria das instruções, esse endereço é True, enquanto para as instâncias individuais é False. É possível usar o método to_mutable() para obter uma versão da instrução que seja própria e segura para sofrer mutação.
As instâncias singleton não são instâncias exatas de sua classe base; elas são subclasses especiais que não podem construir novos objetos. Isso significa que:
type(XGate()) is not XGateVocê não deve confiar em type tenha um valor exato; use isinstance() para verificar o tipo. Se você precisar recuperar de forma confiável a classe base de um Instructionconsulte o atributo Instruction.base_class as instâncias singleton definem esse atributo corretamente. Para a maioria dos casos em que se usa o Qiskit, Instruction.name é um determinante mais adequado do que uma instrução "significa" em um circuito.
Derivando novos singletons
O exemplo mais simples de derivação de uma nova instrução singleton é simplesmente herdar da base correta e fornecer um método __init__() que tenha padrões imutáveis para quaisquer argumentos. Por exemplo:
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").mutableA instância singleton usará todos os padrões do construtor.
Você também pode derivar de uma instrução que é, por si só, um singleton. A natureza de singleton da classe será herdada, embora as instâncias de singleton das duas classes sejam diferentes:
class MyOtherInstruction(MyInstruction):
pass
assert MyOtherInstruction() is MyOtherInstruction()
assert MyOtherInstruction() is not MyInstruction()Se, por algum motivo, você quiser derivar de SingletonInstructionou de uma das subclasses ou classes relacionadas, mas não quiser que a instância singleton padrão seja criada, por exemplo, se estiver definindo uma nova classe base abstrata, você poderá definir o argumento da palavra-chave create_default_singleton=False na definição da classe:
class NotASingleton(SingletonInstruction, create_default_singleton=False):
def __init__(self):
return super().__init__("my_mutable", 1, 0, [])
assert NotASingleton() is not NotASingleton()Se o seu construtor não tiver padrões para todos os seus argumentos, você deverá definir create_default_singleton=False.
As subclasses de SingletonInstruction e as outras classes associadas podem controlar como os argumentos de seus construtores são interpretados, a fim de ajudar o mecanismo de singleton a retornar o singleton mesmo no caso de um argumento opcional ser explicitamente definido com seu valor padrão.
_singleton_lookup_key
static SingletonInstruction._singleton_lookup_key(*_args, **_kwargs)
Considerando os argumentos do construtor, retorne uma tupla de chaves que identifique a instância singleton a ser recuperada ou None se os argumentos indicarem que um objeto mutável deve ser criado.
Para fins de desempenho, como um caso especial, esse método não será chamado se o construtor da classe tiver recebido zero argumentos (por exemplo, a construção XGate() não chamará esse método, mas XGate(label=None) chamará), e o singleton padrão será imediatamente retornado.
Esse método estático pode (e provavelmente deve) ser substituído por subclasses. A assinatura derivada deve corresponder à da classe __init__; esse método deve, então, examinar os argumentos para determinar se requer mutabilidade ou qual deve ser a chave de cache (se houver).
A função deve retornar None ou uma chave dict válida (ou seja, com hash e que implemente a igualdade). Retornar None significa que a instância criada deve ser mutável. Nenhum outro processamento baseado em singleton será feito, e a criação da classe prosseguirá como se não houvesse manipulação de singleton. Caso contrário, a chave retornada pode ser qualquer coisa passível de hash e nenhum significado especial é atribuído a ela. Sempre que esse método retornar a mesma chave, a mesma instância singleton será retornada. Sugerimos que você use uma tupla dos valores de todos os argumentos que podem ser definidos, mantendo a natureza de singleton.
Somente as chaves que correspondem aos argumentos padrão ou aos argumentos fornecidos a additional_singletons no momento da criação da classe realmente retornarão singletons; outros valores retornarão uma instância mutável padrão.
O mecanismo de singleton tratará um retorno sem hash dessa função de forma graciosa, retornando uma instância mutável. As subclasses devem garantir que sua chave seja passível de hash no caminho feliz, mas não precisam verificar manualmente se os argumentos fornecidos pelo usuário são passíveis de hash. Por exemplo, é seguro implementar isso como:
@staticmethod
def _singleton_lookup_key(*args, **kwargs):
return None if kwargs else argsmesmo que um usuário possa fornecer algum tipo não-habitável como um dos args.
Isso é definido por todas as portas da biblioteca padrão do Qiskit, de modo que os argumentos de label e de palavras-chave semelhantes sejam ignorados no cálculo da chave se forem seus padrões, ou uma instância mutável seja retornada se não forem.
Você também pode especificar outras combinações de argumentos do construtor para produzir instâncias de singleton, usando o argumento additional_singletons na definição da classe. Isso usa um iterável de tuplas (args, kwargs) e criará singletons equivalentes a cls(*args, **kwargs). Você não precisa tratar o caso dos argumentos padrão com isso. Por exemplo, dada uma definição de 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)haverá duas instâncias singleton instanciadas. Um corresponde a n=1 e label=None, e o outro a n=2 e label="two". Sempre que o MySingleton for construído com argumentos consistentes com um desses dois casos, o singleton relevante será retornado. Por exemplo:
assert MySingleton() is MySingleton(1, label=None)
assert MySingleton(2, "two") is MySingleton(n=2, label="two")O caso da classe que está sendo instanciada com zero argumentos é tratado especialmente para permitir um caminho absolutamente rápido para o desempenho do loop interno (embora o mecanismo geral não seja desesperadamente lento de qualquer forma).
implementação
Esta seção é principalmente a documentação do desenvolvedor para o código; nenhum dos mecanismos descritos aqui é público, e não é seguro herdar diretamente de nenhum deles.
Há várias partes móveis a serem abordadas aqui. O comportamento de fazer com que XGate() retorne algum objeto singleton que seja uma instância (inexata) de XGate mas sem chamar __init__ exige que substituamos type.__call__. Isso significa que XGate deve ter uma metaclasse que defina __call__ para retornar a instância singleton.
Em seguida, precisamos garantir que haja uma instância singleton para que o XGate() retorne. Isso pode ser feito dinamicamente em cada chamada (ou seja, verificar se a instância existe e criá-la se não existir), mas como também queremos que essa instância seja muito especial, é mais fácil conectá-la e criá-la durante a definição do objeto do tipo XGate . Isso também tem a vantagem de não precisarmos tornar o objeto singleton selecionável; só precisamos especificar de onde recuperá-lo durante o unpickle, porque a criação do objeto do tipo base recriará o singleton.
Queremos que a instância singleton:
- ser imutável; ele deve rejeitar todas as tentativas de sofrer mutação.
- têm exatamente o mesmo estado que um
XGate()teria se não houvesse manipulação de singleton.
Fazemos isso em um procedimento de três etapas:
- Antes de criar quaisquer singletons, definimos separadamente as substituições necessárias para criar um
Instructione umGateimutáveis. Este é o_SingletonInstructionOverridese as outras classes do_*Overrides. - Enquanto criamos o objeto do tipo
XGate, também criamos dinamicamente uma subclasse dele que tem as substituições imutáveis em sua ordem de resolução de método no local correto. Eles substituem os métodos/propriedades padrão que são definidos na porta mutável (não tentamos substituir nenhum caso em que o objeto de tipo que estamos criando tenha métodos adicionais no local). - Não podemos instanciar essa nova subclasse, pois quando ela chamar
XGate.__init__, tentará definir alguns atributos, que serão rejeitados pela imutabilidade. Em vez disso, primeiro criamos uma instância completamente regular doXGatee, em seguida, alteramos dinamicamente seu tipo para a classe singleton, congelando-a.
Poderíamos fazer isso inteiramente dentro do mecanismo de metaclasse, mas isso exigiria que o site XGate fosse definido como algo do tipo:
class XGate(Gate, metaclass=_SingletonMeta, overrides=_SingletonGateOverrides): ...o que é muito inconveniente (ou teríamos que fazer com que o _SingletonMeta fizesse um monte de introspecção frágil). Em vez disso, usamos o abc.ABC/abc.ABCMeta para definir uma classe média concreta (SingletonGate no caso XGate ) que define a metaclasse, seleciona as substituições a serem aplicadas e tem um __init_subclass__() que aplica as etapas de criação de subclasse singleton acima. As substituições estão em classes separadas para que *sejam mutáveis *XGate as instâncias não os têm em suas próprias ordens de resolução de método; fazer isso é mais fácil de implementar, mas requer que todos os setters e verificadores dancem em tempo de execução tentando validar se a mutação da instância é permitida.
Por fim, para realmente construir todo esse maquinário, a base é _SingletonMeta, que é uma metaclasse compatível com qualquer metaclasse de Instruction. Isso define o __call__() que substitui o type.__call__ para retornar as instâncias de singleton. O outro componente é o its __new__()que é chamado (não trivialmente) durante a criação de SingletonGate e SingletonInstruction com seu argumento de palavra-chave overrides definido para definir o __init_subclass__ dessas classes com as propriedades acima. Usamos a metaclasse para adicionar esse método dinamicamente, porque a __init_subclass__() quer ser abstrato, fechando o overrides e a classe base, mas ainda capaz de chamar super. É mais conveniente fazer isso dinamicamente, fechando a variável de classe desejada e usando a forma de dois argumentos de superjá que a forma de argumento zero faz introspecção mágica com base em onde a função que a contém foi definida.
O tratamento de vários singletons requer o armazenamento dos argumentos de inicialização de alguma forma, para permitir que o método to_mutable() e o pickling sejam definidos. Fazemos isso como um dicionário de pesquisa no objeto do tipo singleton. Logicamente, esse é um atributo de instância, mas como precisamos alternar dinamicamente o tipo dinâmico _Singleton para uma instância do tipo base, isso se torna bastante complexo; ou precisamos exigir que a base já tenha um dicionário de instância ou corremos o risco de quebrar o layout __slots__ durante a alternância. Como os singletons têm vida útil que dura até a coleta de lixo do objeto de tipo de sua classe base, podemos falsificar esse dicionário de instância usando um dicionário de objeto de tipo que mapeia os ponteiros de instância para os dados que queremos armazenar. Uma alternativa seria criar um novo objeto de tipo para cada singleton individual que fecha (ou armazena) os argumentos do inicializador, mas os objetos de tipo são bastante pesados e o princípio é basicamente o mesmo.