Encapsulamento, Polimorfismo e Herança em Python


Bernardo Fontes


59º Encontro do PUG-PE

Recife/PE

08 de Junho de 2019

Olar!

Labcodes

aka berin

berinfontes.com

github.com/berinhard

twitter.com/bbfontes

instagram.com/fontes.bernardo

Labcodes
### Roteirinho - Encapsulamento - Information Hiding - Polimorfismo - Herança
### Encapsulamento - Definição básica: mecanismo nas linguagens de promamação para **restringir o acesso direto aos dados** de um objeto - Pelo histórico da nossa indústria, se tornou um conceito muito relacionado ao Java - Em Java, o encapsuamento é **obrigatório** - Exemplos inspirados no blogpost "[Encapsulation is not information hiding](https://www.javaworld.com/article/2075271/encapsulation-is-not-information-hiding.html)"
### Atributos públicos ```java public class Position { public double latitude; public double longitude; } // em uma outra parte do código Position pos = new Position(); pos.latitude = 10.0; pos.longitude = 50.0; ``` - O que acontece se colocarmos 1000.0 como latitude?
### Avaliando estados - Como o intervalo de valores válidos para latitude vai de -90 a 90 teremos problemas ao usarmos 1000 - O problema é o código permitir um **estado inválido** dentro da aplicação - O estado é uma leitura **qualitativa do total de dados** de um objeto - Se um dos dados do objeto, por exemplo o saldo em uma conta bancária, muda, o estado precisa ser **"reavaliado"** - **Estados válidos** configuram os estados com os quais o sistema tem condições de lidar - **Estados inválidos** são grandes causadores de bugs e falhas no software
### Atributos privados + métodos de acesso ```java public class Position { private double latitude; private double longitude; public Position( double latitude, double longitude ) { setLatitude( latitude ); setLongitude( longitude ); } public void setLatitude( double latitude ) { // Ensure -90 <= latitude <= 90 using modulo arithmetic. // Code not shown. // Then set instance variable. this.latitude = latitude; } public void setLongitude( double longitude ) { // Ensure -180 < longitude <= 180 using modulo arithmetic. // Code not shown. // Then set instance variable. this.longitude = longitude; } public double getLatitude() { return latitude; } public double getLongitude() { return longitude; } } // em outra parte do código // *todas* as partes que usarem latitude e longitude vão quebrar // no caso de java, não vamos nem conseguir compilar o código pos.latitude = 10.0; pos.longitude = 50.0; ```
### Tá, mas e no Python? - Python provê mecanismos de encapsulamento **mais elegantes e menos impositivos** sobre a interface dos objetos - Podemos **postergar ao máximo** qualquer decisão de encapsulamento
### Sem encapsuamento ```python class Posicao(): def __init__(self, lat, lng): self.lat, self.lng = lat, lng p1 = Posicao(12.0123, 36.12313) p1.lat = 20.8 p1.lng = 36.9 ```
### Com encapsuamento pythônico ```python class Posicao(): def __init__(self, lat, lng): self.lat, self.lng = lat, lng @property def lat(self): return self._lat @lat.setter def lat(self, value): if not -90 <= value <= 90: raise ValueError("Latitude inválida") self._lat = value @property def lng(self): return self._lng @lng.setter def lng(self, value): if not -180 <= value <= 180: raise ValueError("Longitude inválida") self._lng = value # O código abaixo continua funcionando sem problemas p1 = Posicao(12.0123, 36.12313) p1.lat = 20.8 p1.lng = 36.9 ```
### Da visão geral de encapsulamento - Limitada **aos dados** e a maneira como são expostos - Atributos possuem **restrições**: - `public` - `protected` - `private` - Construções de **métodos de acesso** (`get` ou `set`) para modificar seus valores - **Nem todas as linguagens** viabilizam esses 3 estados - "Protected" em Python: `self._varname` como convenção - "Private" em Python: `self.__varname` - Maaaas.....
*Never, ever use two leading underscores. This is annoyingly private. If name clashes are a concern, use explicit name mangling instead (e.g., _MyThing_blahblah). This is essentially the same thing as double-underscore, only it's transparent where double underscore obscures.* [Ian Bicking - Paste Style Guide](https://paste.readthedocs.io/en/latest/StyleGuide.html)
### Um outro exemplo... ```python class CalendarioEventos(): def __init__(self): self._eventos = [] @property def eventos(self): return self._eventos ####### em outra parte do código: calendario = CalendarioEventos() calendario.evento.append({ "nome": "59º Encontro PUG-PE", "dia": date(2019, 6, 8) }) calendario.evento.append({ "nome": "5ª Noite de Processing - Recife", "dia": date(2019, 6, 26) }) ``` - O que acontece se mudarmos o atributo `_eventos` de um `list` para um `set`?
### Refinando o que é encapsulamento - Só **encapsular o acesso aos dados** não implica necessariamente em ter um bom código OOP - Melhorando a definição: *Mecanismo nas linguagens de promamação para **restringir o acesso direto aos dados** de um objeto através de **métodos que operem sobre eles*** - Mesmo com o encapsulamento sendo usado na classe `CalendarioEventos`, seus clientes sabem de seus **detalhes de implementação** como, por exemplo, de que `eventos` é uma lista. - Não só isso, o cliente é quem está determinando **como os dados de um evento serão armazenados** já que ele modifica diretamente a estrutura de dados que os comporta
### Usando Informatino Hiding ```python class CalendarioEventos(): def __init__(self): self._eventos = set() def adiciona_evento(self, nome, dia): self._eventos.add((nome, dia)) @property def eventos(self): return [{"nome": e[0], "dia": e[1]} for e in self._eventos] # agora é só uma cópia de _eventos ####### em outra parte do código: calendario = CalendarioEventos() calendario.adiciona_evento( nome="59º Encontro PUG-PE", dia=date(2019, 6, 8) ) calendario.adiciona_evento( nome="5ª Noite de Processing - Recife", dia=date(2019, 6, 26) ) ``` - O método `adiciona_evento` escondeu todos os detalhes de implementação de `CalendarioEventos`
### Benefícios de Information Hiding - Um código **baseado em dados** é, por definição, mais **rígido** porque suas integrações estão diretamente acoplados às **decisões de implementação** dos seus diversos objetos; - Em contrapartida, um código **baseado em comportamento** é, por definição, mais **flexível** porque suas integrações dependem de **abstrações** e não em suas implementações concretas; - Portanto, o pote de ouro OOP não é o encapsulamento por si só, mas sim a **troca de mensagens** através de métodos que funcionam como **mecanismos de proteção** da classe;
### Tell, don't ask - Pensamento declarativo VS pensamento procedural - Dados **não possuem semântica** e dizem respeito somente a **decisões de implementação** - Declarativo == troca de **mensagens**
### Alan Kay (criador do termo OOP e da Smalltalk) "OOP to me means only **messaging**, **local retention** and **protection** and **hiding of state-process**, and extreme **late-binding** of all things. It can be done in Smalltalk and in LISP. There are possibly other systems in which this is possible, but I'm not aware of them." [Referência](http://userpage.fu-berlin.de/~ram/pub/pub_jf47ht81Ht/doc_kay_oop_en)
### Late-binding??? - Mecanismos da linguagem que permitem de que o **método ou objeto** que esteja sendo chamado seja **definido em tempo de execução** - **Polimorfismo** é só uma das algumas maneiras atingir late-binding - Por ser praticamente **a única maneira viável** de se fazer em Java, nos parece que é a única - Felizmente, Python é uma linguagem mais orientada a objetos...
### Python Data model ```python def print_info(dado): print(f"Analisando o objeto {dado}") num = len(dado) print(f"O objeto possui {num} elementos.\n") dados = [ "meu nome é bernardo", ["valor 1", "valor 2"], ("eu", "adoro", "tuplas"), {"chave": "valor"}, {1, 2, 3, 4, 5, 5, 5, 5, 6, 1, 1, 1,}, range(10), 42 # será que funciona? ] for dado in dados: print_info(dado) ``` - Será que conseguimos usar `len` com o nosso calendário de eventos?
### `__len__` ```python class CalendarioEventos(): def __init__(self): self._eventos = set() @property def eventos(self): return [{"nome": e[0], "dia": e[1]} for e in self._eventos] # agora é só uma cópia de _eventos def adiciona_evento(self, nome, dia): self._eventos.add((nome, dia)) def __len__(self): return len(self._eventos) ####### em outra parte do código: calendario = CalendarioEventos() calendario.adiciona_evento( nome="59º Encontro PUG-PE", dia=date(2019, 6, 8) ) calendario.adiciona_evento( nome="5ª Noite de Processing - Recife", dia=date(2019, 6, 26) ) num = len(calendario) print(f"Temos {num} eventos no nosso calendario") ```
### Duck typing If it walks like a duck and it quacks like a duck, then it must be a duck ```python def adiciona_elementos(obj, elementos): try: for elem in elementos: obj.append(elem) except AttributeError: print(f"Objeto {obj} não implementa um método append") class CalendarioEventos(): def __init__(self): self._eventos = set() @property def eventos(self): return [{"nome": e[0], "dia": e[1]} for e in self._eventos] # agora é só uma cópia de _eventos def adiciona_evento(self, nome, dia): self._eventos.add((nome, dia)) def __len__(self): return len(self._eventos) def append(self, event_data): self.adiciona_evento(*event_data) lista_simples = [] calendario = CalendarioEventos() dict_simples = {} eventos = [('Evento 1', date(2019, 1, 20)), ('Evento 2', date(2020, 3, 14))] ```
### Mas e o Polimorfismo? - No polimorfismo, definimos uma **única classe** que determina uma **interface** com a qual o sistema vai interagir - As partes do código devem saber interagir **somente** com os métodos definidos por essa interface - Herança é **um mecanismo** para que classes possam implementar interfaces - Herança **não é** um mecanismo para reaproveitamento de código
### Polimorfismo em Python ```python from abc import ABC, abstractmethod class DBAccessInterface(ABC): @abstractmethod def get_by_id(self, id_): pass class JsonDB(DBAccessInterface): def get_by_id(self, id_): return self.json_data.get(id_) class CsvDB(DBAccessInterface): def get_by_id(self, id_): for row in self.csv_data: if row.id == id_: return row return None class TxtDB(DBAccessInterface): """ Como essa classe não implementa a interface, não conseguimos instanciá-la """ ```
### Princípio de Substituição de Liskov - Proposto por Barbara Liskov, uma cientista da computação americana - *Funções que usem ponteiros ou referências **para classes bases** devem ser capazes de utiizar **objetos das classes derivadas** sem precisar saber que está lidando com uma classe derivada*
### Discutindo um pouco de herança ``` class Rect: def __init__(self, w, h): self.w = w self.h = h @property def area(self): return self.w * self.h @property def perimetro(self): return self.w * 2 + self.h * 2 class Square(Rect): def __init__(self, size): self.w = size self.h = size @property def h(self): return self._h @h.setter def h(self, value): self._h = value self._w = value @property def w(self): return self._h @w.setter def w(self, value): self._h = value self._w = value ``` - Acham isso justo?
### O que acontece nesse caso? ``` def renderiza_rect(rect): max_area = 1200 if rect.area > max_area: rect.w = 30 rect.h = 40 # lógica para renderizar ``` - Acham isso justo? - Problema causado por usar Herança como mecanismo para **reutilização de código** e não para **implementação de comportamento** - Sabem onde rola **muito** isso? Django class based views
### Resumo - Encapsulamento nos é útil para **proteger nosso objeto de estados inválidos** - Python viabiliza que nos preocupemos com isso só **quando for necessário** - Temos que usar **Information Hiding** para criar abstrações que escondam nossas implementações - Essas abstrações definem a **interface** de um objeto - Todo nosso código deve **confiar na interface dos objetos** e não nos seus detalhes de implementação - **Interfaces bem definidas** nos permitem usar bastante **late-binding** em Python - Polimorfismo é só **uma das maneiras** de atingir late-binding - Cuidado com soluções por heranças e prefira **composição**

Obrigado!


Bernardo Fontes (berin)

Labcodes

berinfontes.com

twitter.com/bbfontes

github.com/berinhard

instagram.com/fontes.bernardo