1.13. Programação Orientada a Objetos em Python: Definindo Classes

Nós afirmamos anteriormente que o Python é uma linguagem de programação orientada a objetos. Até agora, usamos várias classes nativas para mostrar exemplos de estruturas de dados e controle. Um dos recursos mais poderosos em uma linguagem de programação orientada a objeto é a capacidade de permitir que um programador (solucionador de problemas) crie novas classes que modelem dados necessários para resolver o problema.

Lembre-se que usamos tipos de dados abstratos como descrição lógica de como um objeto de dados se parece (seu estado) e o que ele pode fazer (seus métodos). Ao construir uma classe que implementa um tipo de dado abstrato, um programador pode tirar proveito do processo de abstração e ao mesmo tempo, fornecer os detalhes necessários para usar a abstração em um programa. Sempre que desejarmos implementar um tipo de dados abstrato, vamos fazer isso com uma nova classe.

1.13.1. Uma Classe Fraction

Um exemplo muito comum para mostrar os detalhes da implementação de uma classe definida pelo usuário é construir uma classe para implementar o tipo de dados abstrato Fraction (fração). Nós já vimos que o Python fornece um número de classes numéricas para nosso uso. Há momentos, no entanto, que seria mais apropriado poder criar objetos de dados que se “pareçam” com frações.

Uma fração como \(\frac {3}{5}\) consiste de duas partes. O valor em cima, conhecido como numerador, pode ser qualquer inteiro. O valor embaixo, chamado denominador, pode ser qualquer número inteiro maior que 0 (frações negativas têm um numerador negativo). Embora seja possível criar uma aproximação em ponto flutuante para qualquer fração, neste caso nós gostaríamos de representar uma fração como um valor exato.

As operações para o tipo Fraction permitirão que um objeto de dado Fraction se comporte como qualquer outro valor numérico. Precisamos ser capazes de adicionar, subtrair, multiplicar e dividir frações. Nós também queremos ser capazes de mostrar frações usando a forma padrão com “barra”, por exemplo, 3/5. Além disso, todos os métodos de Fraction devem retornar resultados em seus menores termos de modo que, não importa qual computação seja realizada, sempre acabamos com a forma mais comum.

Em Python, definimos uma nova classe fornecendo um nome e um conjunto de métodos que são sintaticamente semelhantes às definições de função. Para este exemplo,

class Fraction:

   # Coloque os métodos aqui

fornece a estrutura para definirmos os métodos. O primeiro método que todas as classes devem fornecer é o construtor. O construtor define a maneira como os objetos de dados são criados. Para criar um objeto Fraction, precisaremos fornecer dois dados, o numerador e denominador. Em Python, o método construtor é sempre chamado __init__ (com dois underscores antes e depois de init) e é mostrado na Listagem 2.

Listagem 2

class Fraction:

    def __init__(self, cima, baixo):

        self.num = cima
        self.den = baixo

Note que a lista formal de parâmetros contém três itens (self, cima, baixo). O self é um parâmetro especial que sempre deve ser usado como uma referência ao próprio objeto. Deve ser sempre o primeiro parâmetro formal; no entanto, esse parâmetro nunca receberá um valor na chamada. Como descrito anteriormente, as frações requerem dois objetos de dados de estado, o numerador e o denominador. A notação self.num no construtor define que um objeto Fraction tenha um objeto de dados interno chamado num como parte de seu estado. Da mesma forma, self.den cria o denominador. Os valores dos dois parâmetros formais são inicialmente atribuídos ao estado, permitindo que o novo objeto Fraction receba o seu valor inicial.

Para criar uma instância da classe Fraction, devemos invocar o construtor. Isso acontece quando usamos o nome da classe e passamos valores necessários para iniciar o estado (note que nunca invocamos __init__ diretamente). Por exemplo,

myfraction = Fraction(3,5)

cria um objeto chamado myfraction representando a fração \(\frac {3}{5}\) (três quintos). A Figura 5 mostra esse objeto como implementado até agora.

../_images/fraction1.png

Figura 5: Uma Instância da Classe Fraction Referenciada por myfraction

A próxima coisa que precisamos fazer é implementar o comportamento requerido pelo tipo de dado abstrato. Para começar, considere o que acontece quando tentamos imprimir um objeto Fraction.

>>> myfraction = Fraction(3,5)
>>> print(myfraction)
<__main__.Fraction instance at 0x409b1acc>

O objeto Fraction, myfraction, não sabe como responder a esse pedido para imprimir. A função print requer que o objeto se converta em uma string (cadeia de caracteres) para que a string possa ser escrita na saída. A única escolha do myfraction é mostrar a referência real que é armazenada na variável (o próprio endereço). Isto não é o que nós queremos.

Existem duas maneiras de resolver este problema. Uma é definindo um método chamado show (mostrar) que permitirá que o objeto Fraction seja impresso como uma string. Podemos implementar este método como mostrado na Listagem 3. Se criarmos um objeto Fraction como antes, nós podemos lhe pedir para se mostrar, ou em outras palavras, para imprimir seu valor no formato apropriado. Infelizmente, isso geralmente não funciona. Para que a impressão funcione corretamente, precisamos dizer à classe Fraction como se converter em uma string. Isto é o que a função print precisa para fazer o trabalho dela.

Listagem 3

def show(self):
     print(self.num,"/",self.den)
>>> myfraction = Fraction(3,5)
>>> myfraction.show()
3 / 5
>>> print(myfraction)
<__main__.Fraction instance at 0x40bce9ac>
>>>

No Python, todas as classes têm um conjunto de métodos padrão que são fornecidos mas podem não funcionar corretamente. Um desses, __str__, é o método para converter um objeto em uma string. A implementação default para este método é retornar a string correspondente ao endereço da instância, como já vimos. O que precisamos fazer é fornecer uma implementação “melhor” para esse método. Dizemos que esta implementação sobrescreve a anterior, ou que redefine o comportamento do método.

Para fazer isso, nós simplesmente definimos um método com o nome __str__ e fornecemos uma nova implementação como mostrado na Listagem 4. Esta definição não precisa de nenhuma outra informação exceto o parâmetro especial self. Por sua vez, o método irá construir uma string convertendo cada pedaço dos dados de estado interno em strings e depois colocando um caractere / entre as strings por concatenação. A string resultante será retornada sempre que um objeto Fraction for solicitado para se converter em string. Observe que há várias maneiras de se usar essa função.

Listagem 4

def __str__(self):
    return str(self.num)+"/"+str(self.den)
>>> myfraction = Fraction(3,5)
>>> print(myfraction)
3/5
>>> print("Eu comi", myfraction, "da pizza")
Eu comi 3/5 da pizza
>>> myfraction.__str__()
'3/5'
>>> str(myfraction)
'3/5'
>>>

Podemos sobrescrever muitos outros métodos para nossa nova classe Fraction. Algumas das mais importantes são as operações aritméticas básicas. Nós gostaríamos de poder criar dois objetos do tipo Fraction e depois adicioná-los usando a notação padrão “+”. Neste ponto, se tentarmos adicionar duas Frações, obtemos o seguinte:

>>> f1 = Fraction(1,4)
>>> f2 = Fraction(1,2)
>>> f1+f2

Traceback (most recent call last):
  File "<pyshell#173>", line 1, in -toplevel-
    f1+f2
TypeError: unsupported operand type(s) for +:
          'instance' and 'instance'
>>>

A mensagem do interpretador Python “unsupported operand type(s) for +” (o tipo de operando não é suportado por +) informa que o problema é que o operador “+” não entende operandos Fraction.

Podemos consertar isso fornecendo à classe Fraction um método que sobrescreve o método de adição. Em Python, esse método é chamado __add__ e requer dois parâmetros. O primeiro, self, é sempre necessário, e o segundo representa o outro operando na expressão. Por exemplo,

f1.__add__(f2)

pediria ao objeto Fraction f1 para adicionar o objeto Fraction f2 ao seu próprio valor. Isso pode ser escrito na notação padrão, f1 + f2.

Duas frações devem ter o mesmo denominador para serem adicionadas. A maneira mais fácil de se certificar de que eles têm o mesmo denominador é simplesmente usar o produto dos dois denominadores como um denominador comum para que \(\frac {a}{b} + \frac {c}{d} = \frac {ad}{bd} + \frac {cb}{bd} = \frac{ad+cb}{bd}\). A implementação é mostrada na Listagem 5. A função de adição retorna um novo objeto Fraction com o numerador e o denominador da soma. Podemos usar esse método escrevendo uma expressão aritmética padrão envolvendo frações, atribuindo o resultado da adição e depois imprimindo o nosso resultado.

Listagem 5

def __add__(self,other):

     novonum = self.num*other.den + self.den*other.num
     novoden = self.den * other.den

     return Fraction(novonum,novoden)
>>> f1=Fraction(1,4)
>>> f2=Fraction(1,2)
>>> f3=f1+f2
>>> print(f3)
6/8
>>>

O método de adição funciona como desejamos, mas uma coisa poderia ser melhor. Note que \(6/8\) é o resultado correto (\(\frac {1}{4} + \frac {1}{2}\)) mas que não está na forma irredutível (representação usando os “termos mais baixos”). A melhor representação seria \(3/4\). Para ter certeza de que nossos resultados estão sempre nos termos mais baixos, precisamos de uma função auxiliar que saiba como reduzir frações. Esta função precisará procurar o máximo divisor comum ou MDC. Podemos então dividir o numerador e o denominador pelo MDC e o resultado será reduzido para os termos mais baixos.

O algoritmo mais conhecido para encontrar o máximo divisor comum é o Algoritmo de Euclides, que será discutido em detalhes no Capítulo 8. O algoritmo de Euclides afirma que o máximo divisor comum de dois inteiros \(m\) e \(n\) é \(n\) se \(n\) é um divisor próprio de \(m\). No entanto, se \(n\) não for um divisor próprio de \(m\), então a resposta é o máximo divisor comum de \(n\) e o resto da divisão de \(m\) por \(n\). Nós vamos simplesmente fornecer uma implementação iterativa aqui (veja ActiveCode 1). Note que esta implementação do algoritmo de MDC funciona apenas quando o denominador é positivo. Isso é aceitável para nossa Classe Fraction porque dissemos que uma fração negativa será representada por um numerador negativo.

Agora podemos usar essa função para ajudar a reduzir qualquer fração. Para colocar uma fração nos termos mais baixos, dividiremos o numerador e o denominador pelo seu máximo divisor comum. Então, para a fração \(6/8\), o maior divisor comum é 2. Dividindo o numerador e o denominador por 2 criamos uma nova fração, \(3/4\) (veja a Listagem 6).

Listagem 6

def __add__(self, other):
    novonum = self.num*other.den + self.den*other.num
    novoden = self.den * other.den
    comum = mdc(novonum, novoden)
    return Fraction(novonum//comum, novoden//comun)
>>> f1=Fraction(1, 4)
>>> f2=Fraction(1, 2)
>>> f3=f1+f2
>>> print(f3)
3/4
>>>
../_images/fraction2.png

Figura 6: Uma Instância da Classe Fraction com Dois Métodos

Nosso objeto Fraction agora tem dois métodos muito úteis e se parece como mostrado na Figura 6. Um grupo adicional de métodos que precisamos incluir no nosso exemplo da classe Fraction permitirá que duas frações se comparem uma com a outra. Suponha que temos dois objetos Fraction f1 e f2. f1 == f2 só será True se eles forem referências ao mesmo objeto. Dois objetos diferentes com os mesmos numeradores e denominadores não seriam iguais sob esta implementação. Isso é chamado de igualdade rasa (shallow equality) como ilustrado na Figura 7.

../_images/fraction3.png

Figura 7: Igualdade Rasa Versus Igualdade Profunda

Podemos criar igualdade profunda (deep equality - veja a Figura 7) – igualdade pelo mesmo valor, não a mesma referência - sobrescrevendo o método __eq__. O método __eq__ é outro método padrão disponível em qualquer classe. O método __eq__ compara dois objetos e retorna True se seus valores forem os mesmos, False caso contrário.

Na classe Fraction, podemos implementar o método __eq__ colocando as duas frações novamente em termos comuns e depois comparando os numeradores (veja a Listagem 7). É importante notar que há outros operadores relacionais que podem ser sobrescritos. Por exemplo, o O método __le__ fornece a funcionalidade menor que ou igual.

Listagem 7

def __eq__(self, other):
    primeiro = self.num * other.den
    segundo  = other.num * self.den

    return primeiro == segundo

A classe Fraction completa, até este ponto, é mostrada no ActiveCode 2. Deixamos os demais métodos aritméticos e relacionais como exercícios.

Auto Avaliação

Para ter certeza que você entendeu como implementar operadores de classes em Python e como escrever métodos corretamente, escreva alguns métodos para implementar *, / e -. Também implemente os operadores relacionais > e <

1.13.2. Herança: Circuitos e Portas Lógicas

Nossa seção final apresentará outro aspecto importante da programação orientada a objetos. Herança é a capacidade de uma classe de ser relacionada a outra classe de maneira muito semelhante à forma que as pessoas podem ser relacionadas umas às outras. Assim como crianças herdam características de seus pais, classes filhas podem herdar dados e comportamentos característicos de uma classe pai. Essas classes são muitas vezes chamadas de subclasses e superclasses.

A Figura 8 mostra as coleções nativas do Python e suas relações entre si. Nós chamamos uma estrutura relacional como esta de hierarquia de herança (inheritance hierarchy). Por exemplo, o tipo lista (list) é filho da coleção sequencial (sequential collection). Neste caso, chamamos o tipo lista de filho e a sequência de pai (ou lista de subclasse e sequência de superclasse). Isso é frequentemente referido como uma relação É-UM (IS-A Relationship) (a lista É-UMA coleção sequencial). Isso implica que as listas herdam importantes características das sequências, como a ordenação do dados e operações como concatenação, repetição e indexação.

../_images/inheritance1.png

Figura 8: Uma Hierarquia de Herança para Coleções do Python

Listas, tuplas e strings são todos tipos de coleções sequenciais. Todos eles herdam a organização e as operações comuns de dados. No entanto, cada um deles é distinto dependendo se os dados são homogêneos ou se a coleção é imutável. Os filhos ganham tudo dos pais mas se diferenciam deles com características adicionais.

Organizando classes dessa maneira hierárquica, as linguagens de programação orientadas a objetos permitem que códigos escritos previamente sejam estendidos para satisfazer as necessidades de uma nova situação. Além disso, organizando dados desta forma hierárquica, podemos entender melhor as relações existentes. Podemos ser mais eficientes na construção de nossas representações abstratas.

Para explorar mais essa ideia, construiremos uma simulação, uma aplicação para simular circuitos digitais. O bloco de construção básico para esta simulação será a porta lógica (logic gate). Essas chaves eletrônicas representam relações de álgebra booleana entre sua entrada e sua saída. Em geral, as portas têm uma única linha de saída. O valor da saída depende dos valores dados nas linhas de entrada.

As portas AND têm duas linhas de entrada, cada uma das quais pode ser 0 ou 1 (representando False ou True, respectivamente). Se ambas as linhas de entrada têm valor 1, a saída resultante é 1. No entanto, se ambas as linhas de entrada forem 0, o resultado é 0. As portas OR também possuem duas linhas de entrada e a saída é 1 se um ou ambos os valores de entrada forem 1. No caso em que ambas as linhas de entrada são 0, o resultado é 0.

As portas NOT diferem das outras duas portas pois têm uma única linha de entrada. O valor de saída é simplesmente o oposto do valor de entrada. Se 0 aparecer na entrada, 1 será produzido na saída. Similarmente, 1 produz 0. A Figura 9 mostra como cada uma dessas portas é tipicamente representada. Cada porta também tem uma tabela de verdade (truth table) de valores que mostram o mapeamento da entrada para a saída executado pela porta.

../_images/truthtable.png

Figura 9: Três tipos de Portas Lógicas

Combinando essas portas em vários padrões e, em seguida, aplicando um conjunto de valores de entrada, podemos construir circuitos que possuem funções lógicas. A Figura 10 mostra um circuito que consiste em duas portas AND, uma porta OR e uma única porta NOT. As linhas de saída das duas portas AND alimentam diretamente a porta OR e a saída resultante da porta OR é dada à porta NOT. Se aplicarmos um conjunto de valores de entrada às quatro linhas de entrada (duas para cada porta AND), os valores são processados e o resultado aparece na saída da porta NOT. A Figura 10 também mostra um exemplo com valores.

../_images/circuit1.png

Figura 10: Circuito

Para implementar um circuito, vamos primeiro construir uma representação para portas lógicas. Portas lógicas são facilmente organizadas em uma hierarquia de herança de classes como mostrado na Figura 11. No topo da hierarquia, a classe LogicGate representa as características mais gerais das portas lógicas, ou seja, um rótulo para a porta e uma linha de saída. O próximo nível de subclasses divide as portas lógicas em duas famílias, aquelas que possuem uma linha de entrada (unary gate) e aquelas que possuem duas (binary gate). Abaixo disso aparecem as funções lógicas específicas de cada uma.

../_images/gates.png

Figura 11: Uma Hierarquia de Heranças para Portas Lógicas

Agora podemos começar a implementar as classes iniciando com a mais geral, LogicGate. Como observado anteriormente, cada porta tem um rótulo (label) para identificação e uma única linha de saída (output). Além disso, precisamos de métodos para permitir que um usuário de uma porta pergunte pelo seu rótulo (getLabel).

O outro comportamento que toda porta lógica precisa é a capacidade de saber seu valor de saída (getOutput). Isso exigirá que a porta realize a lógica apropriada baseada na entrada atual. Para produzir a saída, a porta precisa saber especificamente qual é essa lógica. Isso significa chamar um método para executar a computação lógica (performGateLogic). A definição completa da classe é mostrada na Listagem 8.

Listagem 8

class LogicGate:

    def __init__(self,n):
        self.label = n
        self.output = None

    def getLabel(self):
        return self.label

    def getOutput(self):
        self.output = self.performGateLogic()
        return self.output

Neste ponto, não iremos implementar a função performGateLogic. A razão disso é que não sabemos como cada porta irá executar sua própria operação lógica. Esses detalhes serão incluídos em cada porta que for adicionada à hierarquia. Esse é um conceito muito poderoso da programação orientada a objetos. Estamos escrevendo um método que irá usar um código que ainda não existe. O parâmetro self é uma referência para o objeto real do tipo porta que chama o método. Qualquer nova porta lógica que for adicionada à hierarquia simplesmente precisará implementar o método performGateLogic e será usado na hora certa. Uma vez feito isso, a porta pode fornecer seu valor de saída. Essa capacidade de estender uma hierarquia que existe atualmente e fornecer as funções específicas que a hierarquia precisa para usar a nova classe é extremamente importante para reutilizar códigos existentes.

Categorizamos as portas lógicas com base no número de linhas de entrada. A porta AND possui duas linhas de entrada. A porta OR também possui duas linhas de entrada. A porta NOT têm uma única linha de entrada. A classe BinaryGate (porta binária) será uma subclasse de LogicGate (porta lógica) e adicionará duas linhas de entrada. A classe UnaryGate (porta unária) também será uma subclasse de LogicGate mas terá apenas uma única linha de entrada. No design de circuitos de computadores, essas linhas são às vezes chamadas de “pinos” (do inglês pin) de forma que vamos usar essa terminologia em nossa implementação.

Listagem 9

class BinaryGate(LogicGate):

    def __init__(self,n):
        LogicGate.__init__(self,n)

        self.pinA = None
        self.pinB = None

    def getPinA(self):
        return int(input("Digite a entrada do Pino A para a porta "+ self.getLabel()+"-->"))

    def getPinB(self):
        return int(input("Digite a entrada do Pino B para a porta "+ self.getLabel()+"-->"))

Listagem 10

class UnaryGate(LogicGate):

    def __init__(self,n):
        LogicGate.__init__(self,n)

        self.pin = None

    def getPin(self):
        return int(input("Digite a entrada do Pino para a porta "+ self.getLabel()+"-->"))

A Listagem 9 e a Listagem 10 implementam essas duas classes. Os construtores em ambas as classes começam com uma chamada explícita ao construtor da classe pai usando o método __init__ do pai. Ao criar uma instância da classe BinaryGate, nós primeiro desejamos inicializar todos os itens de dados que são herdados de LogicGate. Nesse caso, isso corresponde ao rótulo da porta. O construtor, em seguida, adiciona as duas linhas de entrada (pinA e pinB). Este é um padrão muito comum que você deve sempre usar quando for construir hierarquias de classes. Um construtor de uma classe filho precisa chamar o construtor da classe pai para, em seguida, tratar dos seus próprios dados distintos.

O Python também tem uma função chamada super que pode ser usada ao invés de nomear explicitamente a classe pai. Este é um mecanismo mais geral e é amplamente usado, especialmente quando uma classe tem mais de um pai. Mas isso não é algo que iremos discutir nesta introdução. Por exemplo, no nosso exemplo acima LogicGate.__init__(self, n) poderia ser substituído por super(UnaryGate,self).__init__(n).

O único comportamento que a classe BinaryGate adiciona é a capacidade de obter os valores das duas linhas de entrada. Como esses valores vêm de algum lugar externo, nós simplesmente perguntaremos ao usuário por meio de um comando input. A mesma implementação é usada para a classe UnaryGate exceto que há apenas uma linha de entrada.

Agora que temos uma classe geral para portas, dependendo do número de linhas de entrada, podemos construir portais específicas que possuem um comportamento exclusivo. Por exemplo, a classe AndGate (porta AND) será uma subclasse de BinaryGate desde que as portas AND possuam duas linhas de entrada. Como anteriormente, a primeira linha do construtor chama o construtor da classe pai (BinaryGate), que por sua vez chama o construtor de sua classe pai (LogicGate). Note que a classe AndGate não fornece novos dados, uma vez que herda duas linhas de entrada, uma linha de saída e um rótulo.

Listagem 11

class AndGate(BinaryGate):

    def __init__(self,n):
        BinaryGate.__init__(self,n)

    def performGateLogic(self):

        a = self.getPinA()
        b = self.getPinB()
        if a==1 and b==1:
            return 1
        else:
            return 0

A única coisa que a classe AndGate precisa adicionar é o comportamento específico que executa a operação booleana descrita anteriormente. Este é o lugar onde podemos fornecer o método performGateLogic. Para uma porta AND, este método primeiro deve obter os dois valores de entrada e, em seguida, retornará 1 apenas se os dois valores de entrada forem 1. A classe completa é mostrada na Listagem 11.

Podemos mostrar a classe AndGate em ação criando uma instância e pedindo para calcular sua saída. O fragmento a seguir mostra um objeto AndGate, g1, que possui um rótulo interno "G1". Quando nós chamamos o método getOutput, o objeto deve primeiro chamar seu método performGateLogic que por sua vez consulta as duas linhas de entrada. Quando os valores são fornecidos, a saída correta é mostrada.

>>> g1 = AndGate("G1")
>>> g1.getOutput()
Digite a entrada do Pino A para a porta G1-->1
Digite a entrada do Pino B para a porta G1-->0
0

O mesmo processo pode ser feito para as portas OR e NOT. A classe OrGate também será uma subclasse de BinaryGate e a classe NotGate estenderá a classe UnaryGate. Ambas estas classes precisarão fornecer suas próprias funções performGateLogic, por esta definir o comportamento específico de cada porta.

Podemos usar uma única porta construindo primeiro uma instância de uma das classes de porta e, em seguida, pedir à porta pela sua saída (que por sua vez precisa de entradas para ser fornecida). Por exemplo:

>>> g2 = OrGate("G2")
>>> g2.getOutput()
Digite a entrada do Pino A para a porta G2-->1
Digite a entrada do Pino B para a porta G2-->1
1
>>> g2.getOutput()
Digite a entrada do Pino A para a porta G2-->0
Digite a entrada do Pino B para a porta G2-->0
0
>>> g3 = NotGate("G3")
>>> g3.getOutput()
Digite a entrada do Pino para a porta G3-->0
1

Agora que temos as portas básicas funcionando, podemos voltar nossa atenção para a construção de circuitos. Para criar um circuito, precisamos conectar portas, a saída de uma deve fluir para a entrada de outra. Para fazer isso, vamos implementar uma nova classe chamada Connector.

A classe Connector não irá residir na hierarquia de portas. Ela irá, no entanto, usar a hierarquia de portas para que cada conector tenha duas portas, uma em cada extremidade (veja a Figura 12). Essa relação é muito importante na programação orientada a objetos. Ela é chamada de relação TEM-UM (HAS-A). Lembre-se de que usamos a frase “relação É-UM” para dizer que uma classe filha está relacionada a uma classe pai, por exemplo, UnaryGate É-UM LogicGate.

../_images/connector.png

Figura 12: Um Conector Conecta a Saída de Uma Porta à Entrada de Outra

Agora, com a classe Connector, dizemos que um Connector TEM-UM LogicGate, o que significa que os conectores terão instâncias da classe LogicGate dentro deles, mas não fazem parte da hierarquia. Ao projetar classes, é muito importante distinguir entre classes que têm o relacionamento É-UM (que requer herança) daquelas que têm relações TEM-UM (sem herança).

A Listagem 12 mostra a classe Connector. As duas instâncias de portas dentro de cada objeto conector serão chamadas de fromgate (porta origem) e togate (porta destino), indicando que valores de dados “fluem” da saída de uma porta para uma linha de entrada da porta seguinte. A chamada para setNextPin é muito importante para fazer conexões (veja a Listagem 13). Nós precisamos adicionar este método às nossas classes para que cada togate possa escolher a linha de entrada apropriada para a conexão.

Listagem 12

class Connector:

    def __init__(self, fgate, tgate):
        self.fromgate = fgate
        self.togate = tgate

        tgate.setNextPin(self)

    def getFrom(self):
        return self.fromgate

    def getTo(self):
        return self.togate

Na classe BinaryGate, para portas com duas linhas possíveis de entrada, o conector deve ser conectado a apenas uma linha. Se ambas estiverem disponíveis, escolheremos pinA por default. Se pinA já estiver conectado, então vamos escolher pinB. Não é possível conectar com um porta sem linhas de entrada disponíveis.

Listagem 13

def setNextPin(self,source):
    if self.pinA == None:
        self.pinA = source
    else:
        if self.pinB == None:
            self.pinB = source
        else:
           raise RuntimeError("Erro: NÃO HÁ PINO LIVRE")

Agora é possível obter informações de dois lugares: externamente, como antes, e da saída de um gate conectado a essa linha de entrada. Isso requer uma mudança nos métodos getPinA e getPinB (veja a Listagem 14). Se a linha de entrada não estiver conectada a nada (None), então pergunte ao usuário externamente como antes. No entanto, se houver uma conexão, a conexão é acessada e o valor de saída do fromgate é recuperado. Isso, por sua vez, faz com que essa porta processe sua lógica. Isso continua até que todas as entradas estejam disponíveis e o valor final de saída torna-se a entrada necessária para a porta em questão. Em certo sentido, o circuito funciona voltando para trás até encontrar a entrada necessária para finalmente produzir a saída.

Listagem 14

def getPinA(self):
    if self.pinA == None:
        return input("Digite a entrada do Pino A para a porta " + self.getName()+"-->")
    else:
        return self.pinA.getFrom().getOutput()

O seguinte fragmento constrói o circuito mostrado anteriormente:

>>> g1 = AndGate("G1")
>>> g2 = AndGate("G2")
>>> g3 = OrGate("G3")
>>> g4 = NotGate("G4")
>>> c1 = Connector(g1,g3)
>>> c2 = Connector(g2,g3)
>>> c3 = Connector(g3,g4)

As saídas das duas portas AND (g1 e g2) são conectadas à porta OR (g3) e essa saída está conectada à porta NOT (g4). A saída da porta NOT é a saída de todo o o circuito. Por exemplo:

>>> g4.getOutput()
Digite a entrada do Pino A para a porta G1-->0
Digite a entrada do Pino B para a porta G1-->1
Digite a entrada do Pino A para a porta G2-->1
Digite a entrada do Pino B para a porta G2-->1
0

Experimente você mesmo usando o ActiveCode 4.

Auto Avaliação

Crie duas novas classes de porta, uma chamada NorGate e a outra chamada NandGate. As NandGates funcionam como AndGates que possuem uma Not ligada à saída. NorGates funcionam como OrGates com uma Not ligada à saída.

Crie uma série de portas que provam que a seguinte igualdade NOT ((A and B) or (C and D)) é a mesma que NOT (A and B) and NOT (C and D). Certifique-se de usar algumas de suas novas portas na simulação.

Next Section - 1.14. Summary