Na primeira parte desse curso tratamos de tipos básicos como int e float, e na segunda parte introduzimos listas e strings, que são estruturas mais complexas, capazes de reunir sequências de dados. Nesse terceira parte vamos explorar conceitos de programação orientada à objetos para tratar de tipos de dados ainda mais complexos. Dentre os tópicos que serão cobertos estão:
- Princípios de programação orientada a objetos
- Classes e objetos
- Atributos e métodos
- Tipos abstratos de dados
- Filas
- Pilhas
- Algoritmos de busca e ordenação;
- Busca sequencial e binária
- Algoritmos elementares de ordenação
- Noção de complexidade
Introduzir conceitos de orientação a objetos. Ao final dessa aula você deve saber o que é uma classe e a diferença para um objeto da classe, o que são atributos e métodos. Você saberá também como definir um classe em Python, e utilizar o método __init__ (construtor) e __str__.
- Capítulo 13: Classes e Objetos - Fundamentos;
- Classes e objetos
- Atributos e métodos
A capacidade de representar e abstrair conceitos é fundamental para o nosso pensamento, tornando possível a generalização de coisas e a construção de conceitos cada vez mais complexos.
Na evolução das linguagens de programação, a necessidade de se representar e manipular informações complexas resultou no conceito de classes e objetos, onde classes servem como abstrações (representação) e objetos seriam instâncias de classes que mantém e permitem a manipulação da informação.
Em Python, toda informação que usamos é representada na forma de um objeto. Assim, o número 6 é um objeto da classe int, o número 3.14 é um objeto da classe float, e assim por diante.
>>> type(6) # type mostra o tipo do objeto passado como argumento
<class 'int'>
>>> id(6) # mostra o local na memória (endereço) onde o objeto está armazenado
4297370848
>>> i = 6
>>> j = 6
>>> id(i) # note que i e j correspondem ao mesmo objeto 6
4297370848 # pois estão no mesmo lugar na memória
>>> id(j)
4297370848
>>>
Em computação, criar um objeto significa criar uma instância de uma classe. Linguagens orientadas a objetos permitem a definição de novas classes.
Uma classe é uma abstração de alguma “coisa”, que possui um estado e comportamento. Um estado é definido por um conjunto de variáveis chamadas de atributos. Esses estados podem ser alterados por meio de “ações” sobre o objeto, que definem seu comportamento. Essas ações são funções chamadas de “métodos”.
Por exemplo, para representar um carro podemos definir:
- uma classe Carro:
- com os atributos: ano, modelo, cor, e vel
- e os métodos:
- acelera(vel): acelera até vel
- pare(): vel = 0
Uma classe mínima em Python pode ser definida como:
>>> class Carro:
>>> pass
>>> fusca = Carro()
>>> brasilia = Carro()
Essa classe não possui atributos nem métodos, mas já nos permite criar objetos, ou seja, instâncias da classe Carro.
Para criar atributos, basta fazer atribuições usando nome_do_objeto.nome_do_atributo e, a seguir, usar os atributos como variáveis. Exemplo:
Assim como listas, tome muito cuidado com referências ao mesmo objeto. Crie outros atributos e outros carros e verifique o que acontece.
Como sabemos que todos os carros que queremos representar possuem as propriedades “cor”, “ano”, e “modelo”, podemos deixar essas propriedades como atributos da classe e inicializá-las quando um objeto é instanciado (criado). Para isso, vamos utilizar o método especial __init__, conhecido como construtor da classe.
A nova definição da classe ficaria assim:
Mas o que é esse self?
Todo método de uma classe recebe como primeiro parâmetro uma referência à instância que chama o método, permitindo assim que o objeto acesse os seus próprios atributos e métodos. Por convenção, chamamos esse primeiro parâmetro de self, mas qualquer outro nome poderia ser utilizado.
O método especial __init__ é chamado automaticamente após a criação de um objeto, e no caso de um Carro, na nova instância são criados os atributos ‘modelo’, ‘ano’ e ‘cor’ com os valores dados como argumentos do construtor (no caso Carro(“Brasilia”, 1968, “amarela”)).
Assim, no corpo de qualquer método, um atributo pode ser criado, acessado ou modificado usando self.nome_do_atributo.
Para exemplificar a criação de outros métodos e entender melhor o papel do self vamos criar os métodos imprima, acelere e pare. Por ser um exemplo mais elaborado, vamos também colocar os testes dentro da função main.
Leia esse código com atenção e confira a sua execução.
Na função main, pri e seg são instâncias de Carro. Para chamar o método acelere de Carro, usamos a notação:
`Carro.acelere(pri, 40)`
O método acelere recebe dois parâmetros, self e v. Assim, quando o método é executado, self é o objeto pri e v é o número 40. Observe que na chamada:
`Carro.pare(pri)`
Apenas a instância (self) é passada ao método pare, pois esse método recebe apenas 1 argumento.
Essa notação explicita a passagem dos objetos como primeiro parâmetro em cada método, mas é redundante visto que todo objeto sabe a que classe ele pertence. Uma notação mais comum e enxuta é chamar os métodos usando a notação com . de forma semelhante aos atributos e, como o primeiro argumento é sempre ele mesmo, ele pode ser evitado. Assim a chamada:
`Carro.acelere.(pri, 40)`
pode ser escrita como:
`pri.acelere(40)`
O exemplo abaixa mostra o mesmo código anterior, mas usando a notação mais enxuta.
Observe que tanto o método acelere quanto pare chamam o método imprima usando self.imprima() para exibir o estado da instância.
O método imprima é muito útil para qualquer tipo de objeto mas não é a forma mais comum utilizando objetos. Em muitos casos, é desejável simplesmente utilizar a função print para imprimir uma “representação textual” do objeto.
Na verdade, quando executamos o comando print(6), como o 6 é um objeto da classe int, a função print converte o inteiro 6 no string 6 antes de ser impresso, algo como print( str(6) ). Lembre-se que, dentre as funções para conversão de tipos (lembra do int em int(input(“Digite um número: ”))?), a função str converte o inteiro 6 para o string 6.
Para exibir então o conteúdo de um objeto usando print, devemos definir o método especial __str__, e devolver um string com a representação textual do objeto. A função str também chama e retorna o valor desse método (se existir).
O código abaixo é basicamente o mesmo que o anterior, mas substituimos o método imprima pelo método especial __str__. No corpo desse método, ao invés de chamar a função print, carrega-se um string apropriado em s, que é retornado pelo método para a função print ou str. Veja que nos métodos acelere e pare, basta chamar print(self).
Escreva uma classe Complexo que permita representar números complexos e ao menos os métodos:
- __init__ (esse precisa estar presente em todas as classes)
- soma
- multiplicação
- __str__, que retorna um string na forma a+bj (ou a-bj), onde a e b são as partes real e imaginária do número complexo.
OBS: o Python tem uma classe complex para manipular números complexos.