Conceitos Básicos de Programação Orientada a Objetos
Todo programador moderno já ouviu falar em "programação orientada a objetos". Frequentemente, as pessoas pedem para explicar o que é "programação orientada a objetos" em uma única frase. Vamos primeiro examinar a definição mais formal:
Agrupar estruturas de dados e os métodos que as manipulam em objetos (object), classificar objetos com comportamentos semelhantes em classes (class), através do encapsulamento (encapsulation) ocultar detalhes internos, através da herança (inheritance) especializar (specialization) e generalizar (generalization) classes, e através do polimorfismo (polymorphism) implementar despacho dinâmico baseado no tipo do objeto.
Essa definição pode não ser muito clara. Vamos então ver uma explicação mais acessível, como o conteúdo abaixo:
Nota: O conteúdo acima é da internet e não representa as opiniões do autor, não está relacionado com a posição do autor, e o autor não se responsabiliza por ele.
Anteriormente, mencionamos que "um programa é uma coleção de instruções". As instruções que escrevemos em um programa são executadas como uma ou mais instruções pelo CPU. Para simplificar o design de programas, introduzimos o conceito de função, colocando código relativamente independente e frequentemente reutilizável em funções, que podem ser chamadas quando necessário. Se uma função for muito complexa e volumosa, podemos dividi-la em subfunções para reduzir a complexidade do sistema.
No entanto, independentemente de tudo isso, não se percebeu que a programação é, essencialmente, o programador controlando o computador para realizar várias tarefas de acordo com a maneira como o computador trabalha. O modo de operação do computador é diferente do modo de pensamento normal dos humanos, e a programação exige que abandonemos o pensamento humano normal para acomodar o computador, reduzindo o prazer da programação. Declarações como "todos deveriam aprender programação" permanecem apenas como palavras.
Além disso, o mais importante é que quando precisamos desenvolver um sistema complexo, a complexidade do código torna o desenvolvimento e a manutenção difíceis. Por isso, no final dos anos 1960, conceitos como "crise de software", "engenharia de software" e outros começaram a aparecer na indústria.
É claro que os programadores sabem que na realidade não existe uma "bala de prata" que resolva os problemas mencionados acima. O que realmente deu esperança aos desenvolvedores de software foi o conceito de programação orientada a objetos introduzido na linguagem de programação Smalltalk, que surgiu na década de 1970 (os primeiros indícios da programação orientada a objetos podem ser rastreados até a linguagem Simula, mais antiga). Seguindo essa filosofia de programação, dados e funções que manipulam dados em um programa são uma unidade lógica, chamada de "objeto", e a maneira de resolver problemas é criar os objetos necessários e enviar várias mensagens a eles. O trabalho colaborativo de múltiplos objetos nos permite construir sistemas complexos para resolver problemas do mundo real.
Nota: É claro que a programação orientada a objetos não é a "bala de prata" que resolve todos os problemas no desenvolvimento de software. Por isso, as linguagens de programação de alto nível de hoje oferecem suporte a múltiplos paradigmas de programação, e Python não é exceção.
Classes e Objetos
Em termos simples, uma classe é o blueprint ou modelo para objetos, enquanto objetos são instâncias de classes. Essa explicção pode parecer como se estivesse usando conceitos para explicar conceitos, mas pelo menos podemos ver que a classe é um conceito abstrato, enquanto o objeto é algo concreto. No mundo da programação orientada a objetos, tudo é objeto, os objetos têm atributos e comportamentos, cada objeto é único e pertence a uma classe (tipo). Quando extraímos as características estáticas (atributos) e dinâmicas (comportamentos) de um grande número de objetos com características comuns, podemos definir algo chamado "classe".
Definindo Classes
Em Python, podemos usar a palavra-chave class para definir uma classe. Dentro da classe, podemos usar as funções que aprendemos anteriormente para definir métodos, descrevendo assim as características dinâmicas dos objetos. O código a seguir demonstra isso:
class Estudante(object):
# __init__ é um método especial usado para inicializar objetos ao criá-los
# Por meio deste método, podemos vincular os atributos nome e idade ao objeto estudante
def __init__(self, nome, idade):
self.nome = nome
self.idade = idade
def estudar(self, nome_curso):
print('%s está estudando %s.' % (self.nome, nome_curso))
# PEP 8 exige que identificadores usem letras minúsculas, com múltiplas palavras conectadas por sublinhados
# Mas muitos programadores e empresas preferem usar a convenção de nomenclatura CamelCase
def assistir_filme(self):
if self.idade < 18:
print('%s só pode assistir ao "Bear's Big Adventure".' % self.nome)
else:
print('%s está assistindo a um filme romântico.' % self.nome)
Nota: As funções escritas em uma classe são geralmente chamadas de métodos (do objeto). Esses métodos são as mensagens que o objeto pode receber.
Criando e Usando Objetos
Depois de definir uma classe, podemos criar objetos e enviar mensagens a eles da seguinte maneira:
def principal():
# Criar objeto estudante com nome e idade especificados
estudante1 = Estudante('João', 38)
# Enviar mensagem de estudo para o objeto
estudante1.estudar('Design de Programas em Python')
# Enviar mensagem de assistir filme para o objeto
estudante1.assistir_filme()
estudante2 = Estudante('Maria', 15)
estudante2.estudar('Ética')
estudante2.assistir_filme()
if __name__ == '__main__':
principal()
Problema de Visibilidade de Acesso
Para o código acima, programadores com experiência em C++, Java, C# e outras linguagens podem perguntar quais são as permissões de acesso (também chamadas de visibilidade) dos atributos nome e idade vinculados ao objeto Estudante. Em muitas linguagens de programação orientada a objetos, geralmente definimos os atributos dos objetos como privados (private) ou protegidos (protected), ou seja, não permitimos o acesso do exterior, enquanto os métodos dos objetos geralmente são públicos (public), pois métodos públicos são as mensagens que o objeto pode receber. Em Python, os atributos e métodos têm apenas dois tipos de acesso: público e privado. Se você deseja que um atributo seja privado, pode usar dois sublinhados no início do nome do atributo. O código a seguir pode demonstrar isso:
class Teste:
def __init__(self, foo):
self.__foo = foo
def __bar(self):
print(self.__foo)
print('__bar')
def principal():
teste = Teste('olá')
# AttributeError: objeto 'Teste' não tem atributo '__bar'
teste.__bar()
# AttributeError: objeto 'Teste' não tem atributo '__foo'
print(teste.__foo)
if __name__ == "__main__":
principal()
No entanto, Python não garante estritamente a privacidade de atributos ou métodos em termos de sintaxe. Ele apenas renomeia atributos ou métodos privados para "dificultar" o acesso a eles. Na verdade, se você conhece a regra de renomeação, ainda pode acessá-los. O código a seguir demonstra isso. Essa configuração pode ser explicada pela frase famosa: "We are all consenting adults here". A maioria dos programadores acredita que a abertura é melhor que o fechamento, e os programadores devem ser responsáveis por suas próprias ações.
class Teste:
def __init__(self, foo):
self.__foo = foo
def __bar(self):
print(self.__foo)
print('__bar')
def principal():
teste = Teste('olá')
teste._Teste__bar()
print(teste._Teste__foo)
if __name__ == "__main__":
principal()
Na prática, não recomendamos definir atributos como privativos, pois isso impediria que subclasses os acessassem (abordaremos isso mais tarde). Portanto, a maioria dos programadores Python segue uma convenção de nomenclatura: usar um único sublinhado no início do nome do atributo para indicar que o atributo é protegido. Código fora dessa classe deve acessar esses atributos com cautela. Essa prática não é uma regra de sintaxe; atributos e métodos que começam com um único sublinhado ainda podem ser acessados do exterior. Na verdade, eles são mais uma sugestão ou metáfora. A respeito disso, você pode consultar meu artigo "Python - Os Armadilhos que Encontramos ao Longo dos Anos".
Pilares da Programação Orientada a Objetos
A programação orientada a objetos tem três pilares: encapsulamento, herança e polimorfismo. Os últimos dois conceitos serão explicados detalhadamente no próximo capítulo. Vamos primeiro explicar o que é encapsulamento. Minha própria compreensão de encapsulamento é "ocultar todos os detalhes de implementação que podem ser ocultados, expondo apenas uma interface de programação simples aos usuários externos". Os métodos que definimos em uma classe são, na verdade, a encapsulação de dados e operações neles. Depois de criarmos objetos, basta enviar uma mensagem ao objeto (chamar o método) para executar o código no método. Em outras palavras, só precisamos saber o nome do método e os parâmetros passados (visão externa do método), sem precisar saber os detalhes de implementação internos do método (visão interna do método).
Exercícios
Exercício 1: Definir uma classe descrevendo um relógio digital
class Relogio(object):
"""Relógio digital"""
def __init__(self, hora=0, minuto=0, segundo=0):
"""Método de inicialização
:param hora: hora
:param minuto: minuto
:param segundo: segundo
"""
self._hora = hora
self._minuto = minuto
self._segundo = segundo
def avancar(self):
"""Avançar o tempo"""
self._segundo += 1
if self._segundo == 60:
self._segundo = 0
self._minuto += 1
if self._minuto == 60:
self._minuto = 0
self._hora += 1
if self._hora == 24:
self._hora = 0
def mostrar(self):
"""Mostrar a hora"""
return '%02d:%02d:%02d' % \
(self._hora, self._minuto, self._segundo)
def principal():
relogio = Relogio(23, 59, 58)
while True:
print(relogio.mostrar())
sleep(1)
relogio.avancar()
if __name__ == '__main__':
principal()
Exercício 2: Definir uma classe descrevendo um ponto no plano e fornecer métodos para mover o ponto e calcular a distância para outro ponto.
from math import sqrt
class Ponto(object):
def __init__(self, x=0, y=0):
"""Método de inicialização
:param x: coordenada x
:param y: coordenada y
"""
self.x = x
self.y = y
def mover_para(self, x, y):
"""Mover para uma posição específica
:param x: nova coordenada x
:param y: nova coordenada y
"""
self.x = x
self.y = y
def mover_por(self, dx, dy):
"""Mover por um incremento específico
:param dx: incremento na coordenada x
:param dy: incremento na coordenada y
"""
self.x += dx
self.y += dy
def distancia_para(self, outro):
"""Calcular a distância para outro ponto
:param outro: outro ponto
"""
dx = self.x - outro.x
dy = self.y - outro.y
return sqrt(dx ** 2 + dy ** 2)
def __str__(self):
return '(%s, %s)' % (str(self.x), str(self.y))
def principal():
p1 = Ponto(3, 5)
p2 = Ponto()
print(p1)
print(p2)
p2.mover_por(-1, 2)
print(p2)
print(p1.distancia_para(p2))
if __name__ == '__main__':
principal()
Nota: As ilustrações neste capítulo são do livro "Análise e Design Orientados a Objetos" de Grady Booch e outros, um clássico sobre programação orientada a objetos. Os interessados podem comprar e ler este livro para saber mais sobre programação orientada a objetos.