• Nenhum resultado encontrado

Programação Orientada a Objetos

No documento Luiz Gabriel Olivério Noronha (páginas 78-107)

Programação Orientada a Objetos

A Programação Orientada a Objetos (POO) é um paradigma de programação baseado na composição e interação entre objetos. Um objeto consiste de um conjunto de operações encapsuladas (métodos) e um “estado” (determinado pelo valor dos seus atributos) que grava e recupera os efeitos destas operações.

A idéia por trás da Programação Orientada a Objetos é tentar aproximar a modelagem dos dados e processos (mundo virtual) ao mundo real que é baseado em objetos, como entidades concretas – carro, cliente ou um arquivo de computador – ou entidades conceituais, como uma estratégia de jogo ou uma política de escalonamento de um sistema operacional.

As definições – atributos e métodos – que os objetos devem possuir são definidos em classes. Uma classe chamada Cliente em um sistema de locadora de vídeos, por exemplo,

pode possuir atributos como codigo, nome, telefone e endereco, assim como os métodos insere e altera. Um objeto – chamado também de instância de uma classe – é uma materialização de uma classe. Ao instanciar um objeto de uma classe, como Cliente do exemplo anterior, podemos atribuir o valor “Zaphod” ao campo nome, 42 ao codigo, “9999- 0000” ao telefone e “Rua Magrathea, 1024” ao endereco.

Classes new-style e old-style

Há dois tipos de classes no Python: as old-style e as new-style. Até a versão 2.1 do Python as classes old-style eram as únicas disponíveis e, até então, o conceito de classe não era relacionado ao conceito de tipo, ou seja, se x é uma instância de uma classe old-style, x.__class__ designa o nome da classe de x, mas type(x) sempre retornava <type 'instance'>, o que refletia o fato de que todas as instâncias de classes old-styles,

independentemente de suas classes, eram implementadas com um tipo embutido chamado instance.

As classes new-style surgiram na versão 2.2 do Python com o intuito de unificar classes e tipos. Classes new-style são, nada mais nada menos que tipos definidos pelo usuário,

portanto, type(x) é o mesmo que x.__class__. Essa unificação trouxe alguns benefícios imediatos, como por exemplo, a possibilidade da utilização de Propriedades, que veremos mais adiante e metaclasses, assunto que não veremos nessa apostila.

Por razões de compatibilidade as classes são old-style por padrão. Classes new-style são criadas especificando-se outra classe new-style como classe-base. Veremos mais sobre isso adiante, em Herança. Classes old-style existem somente nas versões 2.x do Python, na 3.0 elas já foram removidas, portanto, é recomendado criar classes new-style a não ser que você tenha códigos que precisem rodar em versões anteriores a 2.2.

Criando classes

Uma classe é definida na forma:

class NomeClasse:

<atributos e métodos>

Os nomes das classes devem seguir as regras de identificadores explicadas na Introdução da apostila, ou seja, devem ser iniciados por letras e sublinhados e conter letras, números e sublinhados. Exemplo:

1 class ClasseSimples(object):

Acima definimos uma classe que não possui nenhum atributo nem método – a palavra- chave pass foi utilizada para suprir a necessidade de pelo menos uma expressão dentro da classe.

Note que após o nome da classe inserimos entre parênteses a palavra object. Como veremos mais a diante, essa é a sintaxe para herdar uma classe de outra (ou outras) no Python. O que fizemos na classe anterior foi derivar a classe ClasseSimples da classe object– que é uma classe new-style – definindo então outra classe new-style.

O acesso aos atributos e métodos de uma classe é realizado da seguinte forma:

1 instancia.atributo 2 instancia.metodo()

A seguir veremos exemplos de como acessar atributos e métodos de uma classe.

Variáveis de instância

As variáveis de instância são os campos que armazenam os dados dos objetos, no caso da classe Cliente são os atributos codigo, nome, telefone e endereco. Abaixo segue a definição da classe Cliente para demonstrar a utilização das variáveis de instância:

1 class Cliente(object):

2 def __init__(self, codigo, nome, telefone, endereco):

3 self.codigo = codigo 4 self.nome = nome

5 self.telefone = telefone 6 self.endereco = endereco 7

8 cliente1 = Cliente(42, 'Zaphod', '0000-9999', 'Rua Magrathea, S/N')

9 cliente2 = Cliente(1024, 'Ford', '9999-0000', 'Av. Jelz, 42')

A função __init__ definida no exemplo acima é uma função especial, chamada de inicializador, e é executada quando o objeto é criado. A função __init__ nessa classe define 5 parâmetros – o parâmetro obrigatório self e quatro parâmetros pelos quais passamos os valores para os campos codigo, nome, telefone e endereco dos objetos que estamos instanciando nas linhas 8 e 9. O parâmetro self representa a instância da classe e o utilizamos nas linhas 3, 4, 5 e 6 para definir as variáveis de instância da classe Cliente seguindo a mesma forma de acesso de qualquer objeto – instancia.atributo. O parâmetro self deve ser o primeiro da lista de parâmetros, porém seu nome é uma

convenção – ele pode ter qualquer nome, como instancia ou this– mas é uma boa prática manter o nome self.

Nas linhas 8 e 9 instanciamos dois objetos da classe Cliente, passando valores para os parâmetros definidos no inicializador. Note que não há necessidade de passar um valor para

o parâmetro self, isto é porque o interpretador já fornece um valor automaticamente para ele. No inicializador, o parâmetro self representa o objeto recentemente instanciado, e para quem já teve contato com Java, C++, C# ou PHP, ele é como o this para acessar de dentro da classe seus atributos e métodos da instância.

Apesar de as classes definirem quais atributos (variáveis) e métodos os objetos terão em comum, estes podem ser definidos dinamicamente, ou seja, podem ser consultados, incluídos e excluídos em tempo de execução – veremos isso mais adiante.

Variáveis de classe

São variáveis compartilhadas entre as instâncias de uma classe. Um exemplo disso é uma variável que armazena a quantidade de objetos instanciados da classe. Exemplo:

1 class Cliente(object):

2 instancias = 0

3 def __init__(self, codigo, nome, telefone, endereco):

4 Cliente.instancias += 1 5 self.codigo = codigo 6 self.nome = nome

7 self.telefone = telefone 8 self.endereco = endereco 9

10 cliente1 = Cliente(42, 'Zaphod', '0000-9999', 'Rua Magrathea, 1')

11 cliente2 = Cliente(1024, 'Ford', '9999-0000', 'Av. Jelz, 42')

12 print 'Quantidade de instancias: ', Cliente.instancias

Modificamos o exemplo anterior incluindo a variável de classe instancias. Quando um objeto é instanciado, o método __init__ é chamado e esse incrementa o valor da variável de classe instancias em 1 na linha 4. Repare que a variável de classe é definida dentro da classe, mas fora da função __init__, e para acessá-la é utilizado o nome da classe seguido de um ponto ( . ) e o nome da variável. Para quem já teve contato com Java ou C#, as variáveis de classe são similares às variáveis estáticas.

Métodos

Métodos são funções contidas em uma classe e dividem-se em métodos de classe, de instância e estáticos.

Métodos de instância

Métodos de objeto ou instância podem referenciar variáveis de objeto e chamar outros métodos de instância e para isso devem receber o parâmetro self para representar a instância da classe. Exemplo:

1 class Cliente(object):

2 def __init__(self, codigo, nome, telefone, endereco):

4 self.nome = nome

5 self.telefone = telefone 6 self.endereco = endereco 7

8 def insere(self):

9 print 'Cliente #%i - %s inserido!' % (self.codigo, self.nome)

10

11 cliente1 = Cliente(42, 'Ford Prefect', '0000-9999', 'Betelgeuse 7')

12 cliente1.insere() # Imprime 'Contato #42 – Ford Prefect inserido!' No exemplo acima, retiramos a variável de classe instancias e adicionamos o método insere, que recebe somente o parâmetro obrigatório self, e imprime na tela o cliente que foi inserido. Note que utilizamos o parâmetro self para exibir os valores das variáveis de instância da classe Cliente na linha 9 e, assim como no método especial __init__, não precisamos passar um valor ele. O parâmetro self nos métodos de instância é utilizado pelo interpretador para passar a instância pela qual chamamos os métodos. Para constatar o mesmo, basta criar um método que recebe um parâmetro além de self e chamar esse método sem passar um valor – uma mensagem de erro é exibida dizendo que o método recebe dois parâmetros e que só um foi fornecido.

Métodos de classe

Os métodos de classe são similares aos métodos de instância, porém só podem referenciar as variáveis e métodos de classe. Exemplo:

1 class Cliente(object):

2 instancias = 0

3 def __init__(self, codigo, nome, telefone, endereco):

4 Cliente.instancias += 1 5 self.codigo = codigo 6 self.nome = nome

7 self.telefone = telefone 8 self.endereco = endereco 9

10 def insere(self):

11 print 'Cliente #%i - %s inserido!' % (self.codigo, self.nome)

12

13 @classmethod

14 def imprime_instancias(cls, formatado = False):

15 if formatado:

16 print 'Nº de instâncias de Cliente: ', cls.instancias 17 else: print cls.instancias

18

19 cliente1 = Cliente(42, 'Osmiro', '0000-9999', 'Rua Magrathea, 1')

20 cliente1.imprime_instancias() # Imprime 1 21 Cliente.imprime_instancias() # Imprime 1

O método imprime_instancias recebe dois parâmetros – o primeiro é cls que representa a classe em si, parâmetro pelo qual acessamos a variável de classe instancias. Esse parâmetro é obrigatório e deve ser o primeiro da lista de parâmetros, assim como o self

em métodos de instância. O parâmetro cls possui esse nome por convenção, e assim como self, pode ter seu nome alterado apesar de não ser uma boa prática.

Repare que utilizamos o decorador @classmethod na linha 13 para transformar o método imprime_instancias em um método de classe.

Métodos estáticos

Métodos estáticos não possuem nenhuma ligação com os atributos do objeto ou da classe, e por isso, não podem ser executados a partir de um objeto. A diferença entre os métodos vistos anteriormente e os métodos estáticos é que esses não recebem nenhum parâmetro especial – como self e cls - eles são como funções normais dentro de uma classe e são chamados através da classe, não do objeto. Exemplo:

1 class Cliente(object):

2 instancias = 0

3 def __init__(self, codigo, nome, telefone, endereco):

4 Cliente.instancias += 1 5 self.codigo = codigo 6 self.nome = nome

7 self.telefone = telefone 8 self.endereco = endereco 9

10 def insere(self):

11 print 'Cliente #%i - %s inserido!' % (self.codigo, self.nome)

12

13 @classmethod

14 def imprime_instancias(cls, formatado = False):

15 if formatado:

16 print 'Nº de instâncias de Cliente: ', cls.instancias 17 else: print cls.instancias

18

19 @staticmethod

20 def valida(codigo, nome, telefone, endereco):

21 try:

22 if int(codigo) >= 0 and len(str(nome)) > 0 and \ 23 len(str(telefone)) > 0 and len(str(endereco)):

24 return True 25 else: 26 return False 27 except: 28 return False 29

30 print Cliente.valida(1, 'Zaphod', '(00)9999-9999', 'Rua Magrathea, 42') # Imprime True

31 print Cliente.valida(1, 'Ford', '', '') # Imprime False Note que, assim como usamos o decorador @classmethod para criar um método estático, utilizamos o @staticmethod para transformar o método em estático.

Atributos e métodos dinâmicos

No Python podemos adicionar atributos e métodos em uma instância diretamente, sem precisar defini-las em uma classe. Exemplo seguindo a classe Cliente definida no exemplo anterior:

1 cliente1 = Cliente(42, 'Ford Prefect', '0000-9999', 'Betelgeuse 7')

2 cliente1.email = 'fordprefect@xyz.com' 3 cliente1.celular = '0099-9900'

4

5 def exclui(self):

6 print 'Cliente #%i - %s excluido!' % (self.codigo, self.nome)

7

8 cliente1.exclui = exclui

Adicionamos dois atributos e o método exclui à instância cliente1 nas linhas 2, 3 e 8. Além de adicionar, podemos consultar e excluir atributos com a ajuda de algumas funções embutidas:

 hastattr(instancia, 'atributo'): Verifica se atributo existe em uma instância. Retorna True caso exista e False caso não;

 getattr(instancia, 'atributo'): Retorna o valor de atributo da instância;  setattr(instancia, 'atributo', 'valor'): Passa valor para atributo da

instância;

 delattr(instancia, 'atributo'): Remove atributo da instância.

Exemplos:

1 cliente1 = Cliente(42, 'Ford Prefect', '0000-9999', 'Betelgeuse 7')

2 cliente1.email = 'fordprefect@@xyz.com ' 3 if not hasattr(cliente1, 'celular'):

4 setattr(cliente1, 'celular', '0099-9900')

5 print getattr(cliente1, 'celular') # Imprime '0099-9900' 6 delattr(cliente1, 'celular')

Note que na linha 2 adicionamos o atributo email ao objeto cliente1 por meio de atribuição, na linha 4 adicionamos o atributo celular pela função setattr– tanto pela atribuição quanto pela função setattr o resultado é o mesmo. O mesmo acontece com a função getattr, onde poderíamos sem problemas fazer:

print cliente1.celular # Imprime '0099-9900'

Como tudo em Python é um objeto, podemos fazer isso, por exemplo:

1 def ola_mundo():

2 return "Ola mundo" 3

4 ola_mundo.atributo = "Atributo de ola_mundo"

5 print ola_mundo.atributo # Imprime Atributo de ola_mundo

No exemplo acima definimos dinamicamente um atributo à função ola_mundo e o recuperamos como qualquer atributo de objeto – instancia.atributo. Repare que não executamos a função criada anteriormente, caso fizéssemos, por exemplo,

ola_mundo().atributo, a exceção AttributeError seria acionada, já que o tipo str (do valor retornado pela função) não possui o atributo que tentamos recuperar.

Referências a objetos

Sabemos que variáveis são utilizadas para armazenar valores, e estivemos fazendo isso por todo o decorrer dessa apostila. Com instâncias de classes a coisa funciona de forma um pouco diferente – não é a instância propriamente dita que é armazenada, porém uma referência a ela.

No capítulo de Operadores vimos os operadores is e is not, que usamos para comparar se dois objetos são iguais, ou seja, se as variáveis que os armazenam apontam para o mesmo lugar na memória. Repare no seguinte exemplo:

1 >>> class Teste(object):

2 ... def __init__(self):

3 ... self.atributo = 0 4 ... 5 >>> x = Teste() 6 >>> x.atributo = 10 7 >>> y = x 8 >>> x.atributo 9 10 10 >>> y.atributo 11 10 12 >>> x.atributo = 20 13 >>> y.atributo 14 20

Nesse exemplo criamos uma classe no interpretador interativo chamada de Teste, e por meio do método __init__ definimos o campo atributo. Na linha 5 instanciamos nossa classe e armazenamos uma referência à variável x, e atribuímos 10 ao seu único atributo. Em seguida, na linha 7, informamos que y é igual a x, ou seja, a variável y“aponta” para o mesmo objeto em memória que x. A graça começa aqui: na linha 12 mudamos o valor de atributo de 10 para 20, e o mesmo pode ser constatado na linha 13 – o campo atributo de y também possui o valor 20.

É importante notar que alterar o valor de 10 para 20 em x não mudou a variável y - como as duas referenciam o mesmo objeto, se ele é modificado, suas alterações podem ser percebidas em quaiquer variáveis que apontam para ele.

Assim como podemos criar variáveis e armazenar valores e referências a elas, podemos também excluí-las através do comando del. Tomando como base o exemplo anterior, podemos excluir a variável x, deixando somente y referenciando um objeto Teste em memória:

1 >>> del x 2 >>> x.atributo

3 Traceback (most recent call last):

4 File "<stdin>", line 1, in <module>

5 NameError: name 'x' is not defined 6 >>> y.atributo

7 20

Herança

A Herança é um dos pilares da Programação Orientada a Objeto. Com ela, pode-se criar uma classe baseando-se em outra, ou outras, já que Python suporta herança múltipla. Criar uma classe baseada em outra faz com que a classe herdada, ou classe-filha, literalmente herde todos os atributos e métodos das classes-base (chamadas também de superclasses, dentre outros termos). Para mencionar a, ou as superclasses, basta listá-las entre parênteses, separando-as por vírgulas depois do nome da classe-filha (ou subclasse / classes especializada). Para exemplificar, vamos modificar a estrutura da classe Cliente, criando uma classe

Contato e especializando a classe Cliente:

1 class Contato(object):

2 'Classe Contato'

3 def __init__(self, codigo, nome, telefone, email):

4 self.codigo = codigo 5 self.nome = nome

6 self.telefone = telefone 7 self.email = email

8

9 def insere(self):

10 print 'Contato # %i - %s' % (self.codigo, self.nome)

11

12 def imprime(self):

13 print u"""\ 14 Código: %i

15 Nome: %s 16 Telefone: %s

17 Email: %s""" % (self.codigo, self.nome, self.telefone, self.email)

18

19 class Cliente(Contato):

20 'Classe Cliente, que herda da classe Contato'

21 def __init__(self, codigo, nome, telefone, email, cpf,

cartao_credito):

22 Contato.__init__(self, codigo, nome, telefone, email)

23 self.cpf = cpf

24 self.cartao_credito = cartao_credito 25

27 cliente1 = Cliente(42, 'Zaphod', '0000-9999', 'zaphod@xyz', '128',

'256')

28 cliente1.imprime()

No exemplo acima, definimos na classe Contato os atributos codigo, nome, telefone, email e endereco, o método insere e imprime. Derivamos da classe Contato a classe Cliente, que adiciona os atributos cpf e cartao_credito. No inicializador da classe Cliente chamamos o inicializador da classe Contato, passando os parâmetros recebidos no __init__ de Cliente para o __init__ de Contato. Note que precisamos passar self explicitamente para o inicializador da classe-base Contato. Ao chamarmos o método imprime do objeto cliente1 o código, nome, telefone e email são impressos, mas faltam o CPF e o nº do cartão de crédito presentes na classe Cliente. Para isso, precisamos sobrescrever o método imprime da classe herdada Cliente. Veremos isso a seguir.

Sobrescrita de métodos (Method overriding)

Sobrescrever um método permite a uma subclasse fornecer uma implementação específica de um método já definido em uma superclasse. A implementação desse método na subclasse “substitui” a implementação herdada da superclasse fornecendo um método com o mesmo nome e mesmos parâmetros. No caso da subclasse Cliente, precisamos imprimir os campos cpf e cartao_credito, além daqueles já presentes na implementação de imprime da classe Contato. Seguem as alterações abaixo:

1 class Cliente(Contato):

2 'Classe Cliente, que herda da classe Contato'

3 def __init__(self, codigo, nome, telefone, email, cpf,

cartao_credito):

4 Contato.__init__(self, codigo, nome, telefone, email)

5 self.cpf = cpf

6 self.cartao_credito = cartao_credito 7

8 def imprime(self):

9 print u"""\ 10 Código: %i 11 Nome: %s 12 Telefone: %s 13 Email: %s 14 self.cpf: %s

15 self.cartao_credito: %s""" % (self.codigo, self.nome,

self.telefone, self.email)

16

17 cliente1 = Cliente(42, 'Arthur Dent', '0000-9999',

'arthur@xyz.com', '123', '558')

18 cliente1.imprime()

No exemplo acima redefinimos o método imprime, fazendo com que ele imprima além dos dados presentes na sua implementação da classe Contato, os dados CPF e Nº do

cartão de crédito do cliente. Quando chamamos o método imprime na linha 18, é a implementação do método definida na classe Cliente que é executada, e não o método herdado da classe Contato.

Sobrecarga de métodos (Method overloading)

Em linguagens como Java, C# e C++ é possível sobrecarregar funções e métodos definindo várias versões dele baseado na sua lista de parâmetros, assim, podemos definir um método que não recebe parâmetros, outro que recebe um parâmetro, outro que recebe dois, e assim por diante, todos com o mesmo nome.

Python ao contrário dessas linguagens não suporta esse recurso, porém, podemos utilizar os parâmetros com argumentos default, vistos anteriormente, para uma

implementação bastante similar. Como Python não suporta o sobrecarregamento de métodos, definir um método, por exemplo, imprime(), e depois definir um imprime(parametro), faz com que este útimo sobrescreva o primeiro.

Atenção

Há bastante confusão entre os termos sobrecarregamento de métodos (ou method overloading) e sobrescrita de métodos (ou method overriding) – eles são termos

completamente distintos, portanto, não os confunda.

Atributos e métodos embutidos

Como vimos anteriormente, módulos possuem atributos embutidos – como __name__ e __file__. Toda classe e objeto criado em Python também possui alguns atributos e

métodos embutidos que podem ser acessados como qualquer outro membro:

Atributos

 __dict__: Retorna um dicionário contendo o namespace da classe ou objeto;  __doc__: String de documentação da classe;

 __name__: Retorna o nome da classe;

 __module__: O nome do módulo em que a classe foi definida;

 __bases__: Retorna uma tupla com os nomes das classes-base. Caso a classe não tenha sido derivada de uma classe explicitamente a tupla estará vazia.

Abaixo segue um exemplo demonstrando a utilização dos atributos embutidos seguindo a classe Cliente– derivada de Contato - definida no último exemplo:

1 cliente1 = Cliente(42, 'Arthur Dent', '0000-9999', 'R. Bet., 7')

3 atrs_Cliente = Cliente.__dict__ 4

5 print "Objeto 'cliente1'" 6 print 'Namespace:'

7 for atr in atrs_cliente1:

8 print atr, '->', atrs_cliente1[atr]

9

10 print "Classe 'Cliente'"

11 print 'DocString:', Cliente.__doc__

12 print 'Classes-base:', Cliente.__bases__

13 print 'Namespace:'

14 for atr in atrs_Cliente:

15 print atr, '->', atrs_Cliente[atr]

Nas linhas 7 e 8 iteramos no dicionário atrs_cliente, que contém o namespace do objeto cliente1 e exibindo-o. Nas linhas 14 e 15, iteramos e exibimos o namespace da classe Cliente. Repare que ao exibirmos o namespace do objeto cliente1, somente suas variáveis de instância foram exibidas, e ao exibirmos o namespace da classe Cliente, além das

variáveis de instância, o método insere, __init__, atributos e métodos embutidos foram exibidos. Se tivéssemos declarado uma variável de classe, como instancias– definida em um dos primeiros exemplos - ela também seria exibida na listagem.

Métodos

Os métodos embutidos de classes possuem um comportamento padrão, e como quaisquer outros métodos, podem (e às vezes devem) ser sobrescritos. Vejamos alguns deles:

 __init__: Chamado quando a instância é criada;

 __del__: Chamado quando a instância está para ser destruída, chamado de destrutor;  __lt__, __le__, __eq__, __ne__, __gt__ e __ge__: Chamados de método de

comparação rica. São executados por operadores de comparação, respectivamente <, <=, ==, !=, > e >=;

 __cmp__: Executado pelos operadores de comparação quando os operadores de comparação rica descritos anteriormente não foram definidos;

 __repr__ e __str__: Definem como os objetos de uma classe devem ser representados em forma de string – veremos detalhadamente esses métodos na próxima seção.

Para mais informações sobre os métodos acima, e outros não descritos nesta apostila, consulte a documentação oficial da linguagem, em

http://docs.python.org/reference/datamodel.html?#basic-customization.

Representação de objetos

Lembra que no primeiro capítulo da apostila falamos que normalmente não

vira” para imprimir o valor que mais faz sentido de acordo com seu tipo? O exemplo que demos foi: 1 >>> 'String' 2 'String' 3 >>> 1+1 4 2 5 >>> object() 6 <object object at 0x006F14B0>

No exemplo acima, para o interpretador do Python saber como representar cada tipo de objeto, ele utiliza seus métodos __repr__. O motivo pelo qual o object do exemplo acima imprime seu endereço de memória é bastante simples – esse é o comportamento padrão desse método, portanto, é bem comum sobrescrevê-lo em nossas classes, já que seu comportamento padrão não é necessariamente útil na maioria dos casos.

Além do método __repr__ existe ainda o __str__, e apesar de aparentemente possuírem a mesma função, há diferenças fundamentais entre eles, como veremos a seguir.

__repr__

De acordo com a documentação oficial do Python, o método __repr__ é chamado pela função embutida reprpara criar a representação “oficial” de um objeto. O objetivo desse

No documento Luiz Gabriel Olivério Noronha (páginas 78-107)

Documentos relacionados