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.
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
>>>
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.
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.
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.
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.
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.
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
.
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 chamadaNandGate
. 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 queNOT (A and B) and NOT (C and D)
. Certifique-se de usar algumas de suas novas portas na simulação.