[Py-OO] Aula 01

Introdução a Orientação a Objetos em Python

O que você vai aprender nesta aula?

Após o término da aula você terá aprendido:

  • Objetos em Python
    • Como funcionam
    • Tipagem
    • Mutabilidade
  • Como funciona atribuição e variáveis
  • Classes
    • Sintaxe básica, criando instâncias, métodos de instância, de classe e estáticos

Revisão dos conceitos de Orientação a Objetos

Vamos começar com uma rápida revisão dos conceitos de Orientação a Objetos e como eles são aplicados em Python.

O paradigma de programação orientada a objetos tem por objetivo fornecer uma abstração do mundo real e aplicá-la para a programação.

Objetos são componentes de software que incluem dados e comportamentos. Por exemplo cães possuem estados (nome, raça e cor) e comportamentos (latir, abanar o rabo e pegar objetos). Já bicicletas possuem outros estados (modelo, marcha atual e velocidade atual) e comportamentos (mudar marcha, frear).

Outro conceito importante é o de classe. Estas representam a estrutura de um objeto, por exemplo: a receita de um bolo. Nesse exemplo a receita seria a classe que contém instruções de como criar o objeto, além de ter informações sobre a instância (bolo).

Em Python os objetos possuem atributos que podem ser tanto métodos (funções vinculadas ao objeto) ou atributos de dados do objeto. Este último geralmente é geralmente chamado de atributo.

É importante saber que em Python instâncias de classes são chamadas exatamente de instâncias. É comum em Java/C++ chamar "instâncias de classes" de "objetos de classes". Isso não acontece em Python, pois nesta linguagem tudo é um objeto e, portanto, chamar a instância de uma classe de objeto é redundante.

Quando falamos sobre linguagens que implementam o paradigma de orientação a objetos elas devem fornecer quatro conceitos básicos:

  • Abstração: habilidade de modelar características do mundo real (?)
  • Encapsulamento: permitir a proteção de dados e que operações internas tenham acesso a esses dados.
  • Herança: mecanismo que possibilitam a criação de novos objetos por meio da alteração de algo já exisitente; e vincular o objeto criado com o antigo.
  • Polimorfismo: capacidade de uma unidade de ter várias formas.

Objetos em Python

Como dito anteriormente tudo em Python é objeto. Vamos começar analisando um objeto dict:


In [62]:
notas = {'bia': 10, 'pedro': 0, 'ana': 7}
notas


Out[62]:
{'ana': 7, 'bia': 10, 'pedro': 0}

O dicionários possui diversos métodos que usamos para alterar os objetos:


In [63]:
notas.keys()


Out[63]:
dict_keys(['bia', 'ana', 'pedro'])

In [64]:
notas.pop('bia')


Out[64]:
10

In [65]:
notas


Out[65]:
{'ana': 7, 'pedro': 0}

Podemos usar a função dir() para inspecionar os métodos e atributos do dict notas:


In [66]:
dir(notas)


Out[66]:
['__class__',
 '__contains__',
 '__delattr__',
 '__delitem__',
 '__dir__',
 '__doc__',
 '__eq__',
 '__format__',
 '__ge__',
 '__getattribute__',
 '__getitem__',
 '__gt__',
 '__hash__',
 '__init__',
 '__iter__',
 '__le__',
 '__len__',
 '__lt__',
 '__ne__',
 '__new__',
 '__reduce__',
 '__reduce_ex__',
 '__repr__',
 '__setattr__',
 '__setitem__',
 '__sizeof__',
 '__str__',
 '__subclasshook__',
 'clear',
 'copy',
 'fromkeys',
 'get',
 'items',
 'keys',
 'pop',
 'popitem',
 'setdefault',
 'update',
 'values']

Aqui vemos vários métodos que o nome contém underscores no começo e fim como __len__, __getitem__, __setitem__. Esses métodos são chamados de métodos especiais que fazem parte do modelo de dados do Python. Esses métodos são chamados pelo interpretador quando uma sintaxe especial é acionada. Como, por exemplo, quando acessamos os itens do dicionário por sua chave o interpretador invoca a função dict.__getitem__():


In [67]:
notas


Out[67]:
{'ana': 7, 'pedro': 0}

In [68]:
notas.__getitem__('ana')


Out[68]:
7

In [69]:
notas['ana']


Out[69]:
7

In [70]:
notas.__getitem__('joselito')


---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
<ipython-input-70-ea9f32f28f28> in <module>()
----> 1 notas.__getitem__('joselito')

KeyError: 'joselito'

In [71]:
notas['joselito']


---------------------------------------------------------------------------
KeyError                                  Traceback (most recent call last)
<ipython-input-71-d5c7e15779f1> in <module>()
----> 1 notas['joselito']

KeyError: 'joselito'

O dict também possui atributos de dados especiais como __class__, que armazena o nome da classe do objeto, e __doc__ que retém a docstring do objeto:


In [72]:
notas.__class__


Out[72]:
dict

In [73]:
notas.__doc__


Out[73]:
"dict() -> new empty dictionary\ndict(mapping) -> new dictionary initialized from a mapping object's\n    (key, value) pairs\ndict(iterable) -> new dictionary initialized as if via:\n    d = {}\n    for k, v in iterable:\n        d[k] = v\ndict(**kwargs) -> new dictionary initialized with the name=value pairs\n    in the keyword argument list.  For example:  dict(one=1, two=2)"

Para ver a docstring formatada para saída use a função print():


In [74]:
print(notas.__doc__)


dict() -> new empty dictionary
dict(mapping) -> new dictionary initialized from a mapping object's
    (key, value) pairs
dict(iterable) -> new dictionary initialized as if via:
    d = {}
    for k, v in iterable:
        d[k] = v
dict(**kwargs) -> new dictionary initialized with the name=value pairs
    in the keyword argument list.  For example:  dict(one=1, two=2)

Números são objetos:


In [75]:
3 + 4


Out[75]:
7

Possuem métodos e atributos:


In [76]:
print(3 .__doc__)


int(x=0) -> integer
int(x, base=10) -> integer

Convert a number or string to an integer, or return 0 if no arguments
are given.  If x is a number, return x.__int__().  For floating point
numbers, this truncates towards zero.

If x is not a number or if base is given, then x must be a string,
bytes, or bytearray instance representing an integer literal in the
given base.  The literal can be preceded by '+' or '-' and be surrounded
by whitespace.  The base defaults to 10.  Valid bases are 0 and 2-36.
Base 0 means to interpret the base from the string as an integer literal.
>>> int('0b100', base=0)
4

In [77]:
3 .__add__(4)


Out[77]:
7

In [78]:
3 .__sub__(4)


Out[78]:
-1

Só lembrando que os métodos especiais não devem ser chamados diretamente, os exemplos anteriores só servem para ilustrar o funcionamento e existência desses métodos. Caso você queira consultar a documentação de um objeto use a função help():


In [79]:
help(3)


Help on int object:

class int(object)
 |  int(x=0) -> integer
 |  int(x, base=10) -> integer
 |  
 |  Convert a number or string to an integer, or return 0 if no arguments
 |  are given.  If x is a number, return x.__int__().  For floating point
 |  numbers, this truncates towards zero.
 |  
 |  If x is not a number or if base is given, then x must be a string,
 |  bytes, or bytearray instance representing an integer literal in the
 |  given base.  The literal can be preceded by '+' or '-' and be surrounded
 |  by whitespace.  The base defaults to 10.  Valid bases are 0 and 2-36.
 |  Base 0 means to interpret the base from the string as an integer literal.
 |  >>> int('0b100', base=0)
 |  4
 |  
 |  Methods defined here:
 |  
 |  __abs__(self, /)
 |      abs(self)
 |  
 |  __add__(self, value, /)
 |      Return self+value.
 |  
 |  __and__(self, value, /)
 |      Return self&value.
 |  
 |  __bool__(self, /)
 |      self != 0
 |  
 |  __ceil__(...)
 |      Ceiling of an Integral returns itself.
 |  
 |  __divmod__(self, value, /)
 |      Return divmod(self, value).
 |  
 |  __eq__(self, value, /)
 |      Return self==value.
 |  
 |  __float__(self, /)
 |      float(self)
 |  
 |  __floor__(...)
 |      Flooring an Integral returns itself.
 |  
 |  __floordiv__(self, value, /)
 |      Return self//value.
 |  
 |  __format__(...)
 |      default object formatter
 |  
 |  __ge__(self, value, /)
 |      Return self>=value.
 |  
 |  __getattribute__(self, name, /)
 |      Return getattr(self, name).
 |  
 |  __getnewargs__(...)
 |  
 |  __gt__(self, value, /)
 |      Return self>value.
 |  
 |  __hash__(self, /)
 |      Return hash(self).
 |  
 |  __index__(self, /)
 |      Return self converted to an integer, if self is suitable for use as an index into a list.
 |  
 |  __int__(self, /)
 |      int(self)
 |  
 |  __invert__(self, /)
 |      ~self
 |  
 |  __le__(self, value, /)
 |      Return self<=value.
 |  
 |  __lshift__(self, value, /)
 |      Return self<<value.
 |  
 |  __lt__(self, value, /)
 |      Return self<value.
 |  
 |  __mod__(self, value, /)
 |      Return self%value.
 |  
 |  __mul__(self, value, /)
 |      Return self*value.
 |  
 |  __ne__(self, value, /)
 |      Return self!=value.
 |  
 |  __neg__(self, /)
 |      -self
 |  
 |  __new__(*args, **kwargs) from builtins.type
 |      Create and return a new object.  See help(type) for accurate signature.
 |  
 |  __or__(self, value, /)
 |      Return self|value.
 |  
 |  __pos__(self, /)
 |      +self
 |  
 |  __pow__(self, value, mod=None, /)
 |      Return pow(self, value, mod).
 |  
 |  __radd__(self, value, /)
 |      Return value+self.
 |  
 |  __rand__(self, value, /)
 |      Return value&self.
 |  
 |  __rdivmod__(self, value, /)
 |      Return divmod(value, self).
 |  
 |  __repr__(self, /)
 |      Return repr(self).
 |  
 |  __rfloordiv__(self, value, /)
 |      Return value//self.
 |  
 |  __rlshift__(self, value, /)
 |      Return value<<self.
 |  
 |  __rmod__(self, value, /)
 |      Return value%self.
 |  
 |  __rmul__(self, value, /)
 |      Return value*self.
 |  
 |  __ror__(self, value, /)
 |      Return value|self.
 |  
 |  __round__(...)
 |      Rounding an Integral returns itself.
 |      Rounding with an ndigits argument also returns an integer.
 |  
 |  __rpow__(self, value, mod=None, /)
 |      Return pow(value, self, mod).
 |  
 |  __rrshift__(self, value, /)
 |      Return value>>self.
 |  
 |  __rshift__(self, value, /)
 |      Return self>>value.
 |  
 |  __rsub__(self, value, /)
 |      Return value-self.
 |  
 |  __rtruediv__(self, value, /)
 |      Return value/self.
 |  
 |  __rxor__(self, value, /)
 |      Return value^self.
 |  
 |  __sizeof__(...)
 |      Returns size in memory, in bytes
 |  
 |  __str__(self, /)
 |      Return str(self).
 |  
 |  __sub__(self, value, /)
 |      Return self-value.
 |  
 |  __truediv__(self, value, /)
 |      Return self/value.
 |  
 |  __trunc__(...)
 |      Truncating an Integral returns itself.
 |  
 |  __xor__(self, value, /)
 |      Return self^value.
 |  
 |  bit_length(...)
 |      int.bit_length() -> int
 |      
 |      Number of bits necessary to represent self in binary.
 |      >>> bin(37)
 |      '0b100101'
 |      >>> (37).bit_length()
 |      6
 |  
 |  conjugate(...)
 |      Returns self, the complex conjugate of any int.
 |  
 |  from_bytes(...) from builtins.type
 |      int.from_bytes(bytes, byteorder, *, signed=False) -> int
 |      
 |      Return the integer represented by the given array of bytes.
 |      
 |      The bytes argument must be a bytes-like object (e.g. bytes or bytearray).
 |      
 |      The byteorder argument determines the byte order used to represent the
 |      integer.  If byteorder is 'big', the most significant byte is at the
 |      beginning of the byte array.  If byteorder is 'little', the most
 |      significant byte is at the end of the byte array.  To request the native
 |      byte order of the host system, use `sys.byteorder' as the byte order value.
 |      
 |      The signed keyword-only argument indicates whether two's complement is
 |      used to represent the integer.
 |  
 |  to_bytes(...)
 |      int.to_bytes(length, byteorder, *, signed=False) -> bytes
 |      
 |      Return an array of bytes representing an integer.
 |      
 |      The integer is represented using length bytes.  An OverflowError is
 |      raised if the integer is not representable with the given number of
 |      bytes.
 |      
 |      The byteorder argument determines the byte order used to represent the
 |      integer.  If byteorder is 'big', the most significant byte is at the
 |      beginning of the byte array.  If byteorder is 'little', the most
 |      significant byte is at the end of the byte array.  To request the native
 |      byte order of the host system, use `sys.byteorder' as the byte order value.
 |      
 |      The signed keyword-only argument determines whether two's complement is
 |      used to represent the integer.  If signed is False and a negative integer
 |      is given, an OverflowError is raised.
 |  
 |  ----------------------------------------------------------------------
 |  Data descriptors defined here:
 |  
 |  denominator
 |      the denominator of a rational number in lowest terms
 |  
 |  imag
 |      the imaginary part of a complex number
 |  
 |  numerator
 |      the numerator of a rational number in lowest terms
 |  
 |  real
 |      the real part of a complex number

Como explicado na [py-intro] Aula 05 funções também são objetos. Na terminologia utilizada pelos livros isso quer dizer que, em Python, as funções são objetos de primeira classe ou cidadãos de primeira classe.


In [80]:
def soma(a, b):
    """ retorna a + b """
    soma = a + b
    return soma

In [81]:
soma(1, 2)


Out[81]:
3

In [82]:
soma


Out[82]:
<function __main__.soma>

Podemos a atribuir funções a variáveis:


In [83]:
adição = soma
adição


Out[83]:
<function __main__.soma>

Acessar atributos:


In [84]:
adição.__name__


Out[84]:
'soma'

In [85]:
adição.__doc__


Out[85]:
' retorna a + b '

Podemos ver o bytecode que a função executa usando o módudlo dis (disassembly), enviando a função soma() como argumento da função dis.dis():


In [86]:
import dis
dis.dis(soma)


  3           0 LOAD_FAST                0 (a)
              3 LOAD_FAST                1 (b)
              6 BINARY_ADD
              7 STORE_FAST               2 (soma)

  4          10 LOAD_FAST                2 (soma)
             13 RETURN_VALUE

Tipagem dos objetos

Tipagem forte

Os objetos em Python possuem tipagem forte, isso quer dizer que dificilmente são feitas conversões de tipos implícitas na realização de operações. Vamos ver alguns exemplos que ilustram esse conceito:


In [87]:
"1" + 10


---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-87-37dec59fff1f> in <module>()
----> 1 "1" + 10

TypeError: Can't convert 'int' object to str implicitly

Tentamos concatenar o número 10 à string "1", porém uma exceção do tipo TypeError foi levantada dizendo que não foi possível converter um objeto int para str de forma implicita.

Em Javascript e PHP, linguagens que possuem tipagem fraca, não seria levantado uma exceção e o interpretador faria a conversão de um dos tipos. No Javascript (1.5) o resultado seria uma string "110" e no PHP (5.6) o número 11.

Aqui percebemos que a operação "1" + 10 pode produzir dois resultados: uma string ou um número. Conforme consta no Zen do Python: "In the face of ambiguity, refuse the temptation to guess" (Ao encontrar uma ambiguidade recuse a tentação de inferir) e é exatamente o que o Python faz: a linguagem recusa-se a inferir o tipo do resultado e levanta uma exceção.

Para fazermos esse exemplo funcionar precisamos converter os tipos explicitamente:


In [88]:
"1" + str(10)


Out[88]:
'110'

In [89]:
int("1") + 10


Out[89]:
11

Tipagem dinâmica

Dizemos que uma linguagem possui tipagem dinâmica quando não é necessário especificar explicitamente os tipos das váriaveis. Os objetos possuem tipos, porém as variáveis podem referenciar objetos de quaisquer tipos. Verificações de tipos são feitas em tempo de execução e não durante a compilação.

Quando definimos uma função dobra(x) que retorna o valor recebido multiplicado por 2 podemos receber qualquer tipo de objeto como argumento:


In [90]:
def dobra(x):
    return x * 2

Podemos dobrar int:


In [91]:
dobra(2)


Out[91]:
4

Dobrar float:


In [92]:
dobra(1.15)


Out[92]:
2.3

strings:


In [93]:
dobra('bo')


Out[93]:
'bobo'

sequências:


In [94]:
dobra([1, 2, 3])


Out[94]:
[1, 2, 3, 1, 2, 3]

In [95]:
dobra((4, 5, 6))


Out[95]:
(4, 5, 6, 4, 5, 6)

Tipos que não suportam multiplicação por inteiros levantarão exceção quando executados:


In [96]:
dobra(None)


---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-96-e271b53c9321> in <module>()
----> 1 dobra(None)

<ipython-input-90-885d1e050b6c> in dobra(x)
      1 def dobra(x):
----> 2     return x * 2

TypeError: unsupported operand type(s) for *: 'NoneType' and 'int'

A função type() nos permite verificar os tipos dos objetos:


In [97]:
type(1)


Out[97]:
int

In [98]:
type([1, 2, 3])


Out[98]:
list

In [99]:
type((1, 2, 3))


Out[99]:
tuple

In [100]:
type({})


Out[100]:
dict

In [101]:
type('lalala')


Out[101]:
str

In [102]:
type(False)


Out[102]:
bool

Mutabilidade

No Python existem objetos mutáveis e imutáveis, já vimos vários exemplos disso ao longo do curso. O estado (atributo) de objetos mutáveis podem ser alterados, já obetos imutáveis não podem ser alterados de forma alguma.

A tabela abaixo mostra a mutabilidade dos tipos embutidos do Python:

Imutáveis Mutáveis
tuple list
números (int, float, complex) dict
frozenset set
str, bytes objetos que permitem alteração de atributos por acesso direto, setters ou métodos

Vamos ver alguns exemplos que demonstram isso:


In [103]:
a = 10
a


Out[103]:
10

Todo objeto python possui uma identidade, um número único que diferencia esse objeto. Podemos acessar a identidade de um objeto usando a função id():


In [104]:
id(a)


Out[104]:
10894368

Isso quer dizer que a identidade do objeto a é 10894368.

Agora vamos tentar mudar o valor de a:


In [105]:
b = 3
b


Out[105]:
3

In [106]:
a += b
a


Out[106]:
13

In [107]:
id(a)


Out[107]:
10894464

A identidade mudou, isso significa que a variável a está referenciando outro objeto que foi criado quando executamos a += b.

Vamos ver agora um exemplo de objeto mutável:


In [108]:
lista = [1, 2, 3, 4]
lista


Out[108]:
[1, 2, 3, 4]

Vamos verificar a identidade dessa lista:


In [109]:
id(lista)


Out[109]:
139981043598984

In [110]:
lista.append(10)
lista.remove(2)
lista += [-4, -3]
lista


Out[110]:
[1, 3, 4, 10, -4, -3]

In [111]:
id(lista)


Out[111]:
139981043598984

Mesmo modificando a lista através da inserção e remoção de valores sua identidade continua a mesma.

Strings também são imutáveis:


In [112]:
s = 'abcd'

In [113]:
id(s)


Out[113]:
139981043607232

In [114]:
s[0] = 'z'


---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-114-83b06971f05e> in <module>()
----> 1 s[0] = 'z'

TypeError: 'str' object does not support item assignment

Como vimos na aula dois do módulo de introdução strings são imutáveis e para alterar seu valor precisamos usar slicing:


In [115]:
s = 'z' + s[1:]
s


Out[115]:
'zbcd'

In [116]:
id(s)


Out[116]:
139981043607904

Comparando a identidade de s antes e depois da mudança vemos que trata-se de objetos diferentes.

Variáveis

Variáveis são apenas referências para objetos, assim como acontece em Java. Variáveis são apenas rótulos (ou post-its) associados a objetos. Diferentemente de C ou Pascal as variáveis em Python não são caixas que armazenam objetos ou valores.

Por exemplo:


In [117]:
a = [1, 2, 3]
a


Out[117]:
[1, 2, 3]

In [118]:
b = a

In [119]:
a.append(4)

In [120]:
b


Out[120]:
[1, 2, 3, 4]

As variáveis a e b armazenam referências à mesma lista em vez de cópias.

É importante notar que objetos são criados antes da atribuição. A operação do lado direito de uma atribuição ocorre antes que a atribuição:


In [121]:
c = 1 / 0


---------------------------------------------------------------------------
ZeroDivisionError                         Traceback (most recent call last)
<ipython-input-121-a1be501e842d> in <module>()
----> 1 c = 1 / 0

ZeroDivisionError: division by zero

Como não foi possível criar o número - por representar uma operação inválida (divisão por zero) para a linguagem - a variável c não foi atribuída a nenhum objeto:


In [122]:
c


---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-122-2cd6ee2c70b0> in <module>()
----> 1 c

NameError: name 'c' is not defined

Como as variáveis são apenas rótulos a forma correta de falar sobre atribuição é "a variável x foi atribuída à (instância) lâmpada" e não "a lâmpada foi atribuída à variável x". Pois é como se colocássemos um "post-it" x em um objeto, e não guardássemos esse objeto em uma caixa x.

Por serem rótulos podemos atribuir diversos rótulos a um mesmo objeto. Isso faz com que apelidos (aliases) sejam criados:


In [123]:
josé = {'nome': 'José Silva', 'idade': 10}
 = josé
 is josé


Out[123]:
True

In [124]:
id(), id(josé)


Out[124]:
(139981116056456, 139981116056456)

In [125]:
['ano_nascimento'] = 2006
josé


Out[125]:
{'ano_nascimento': 2006, 'idade': 10, 'nome': 'José Silva'}

Vamos supor que exista um impostor - o João - que possua as mesmas credenciais que o José Silva. Suas credenciais são as mesmas, porém João não é José:


In [126]:
joão = {'nome': 'José Silva', 'idade': 10, 'ano_nascimento': 2006}
joão == josé


Out[126]:
True

O valor de seus dados (ou credenciais) são iguais, porém eles não são os mesmos:


In [127]:
joão is josé


Out[127]:
False

Nesse exemplo vimos o apelidamento (aliasing). josé e são apelidos (aliases): duas variáveis associadas ao mesmo objeto. Por outro lado vimos que joão não é um apelido de josé: essas variáveis estão associadas a objetos distintos. O que acontece é que joão e josé possuem o mesmo valor - que é isso que == compara - mas têm identidades diferentes.

O operador == realiza a comparação dos valores de objetos (os dados armazenados por eles), enquanto is compara suas identidades. É mais comum comparar valores do que identidades, por esse motivo == aparece com mais frequência que is em códigos Python. Um caso em que o is é bastante utilizada é para comparação com None:


In [128]:
a = 10
a is None


Out[128]:
False

In [129]:
b = None
b is None


Out[129]:
True

Classes

Vamos ver como é a sintaxe de definição de classes no Python, para isso vamos fazer alguns exemplos.

Começaremos por criar uma classe que representa um cão. Armazenaremos: o nome, a quantidade de patas, se o cão é carnívoro e se ele está nervoso.


In [130]:
class Cão:
    qtd_patas = 4
    carnívoro = True
    nervoso = False
    
    def __init__(self, nome):
        self.nome = nome

Na primeira linha definimos uma classe de nome Cão.

Da segunda até a quarta linha definimos os atributos de classe qtd_patas, carnívoro, nervoso. Os atributos de classe representam dados que aparecem em todas as classes.

Na sexta linha definimos o inicializador (também pode ser chamado de construtor) que deve receber o nome do Cão.

Na última linha criamos o atributo da instância nome e associamos à string enviada para o construtor.

Vamos agora criar uma instância de Cão:


In [131]:
rex = Cão('Rex')
type(rex)


Out[131]:
__main__.Cão

Vamos verificar seus atributos:


In [132]:
rex.qtd_patas


Out[132]:
4

In [133]:
rex.carnívoro


Out[133]:
True

In [134]:
rex.nervoso


Out[134]:
False

In [135]:
rex.nome


Out[135]:
'Rex'

Podemos também alterar esses atributos:


In [136]:
rex.nervoso = True
rex.nervoso


Out[136]:
True

Mudamos apenas o atributo nervoso da instância rex. O valor de Cão.nervoso continua o mesmo:


In [137]:
Cão.nervoso


Out[137]:
False

Também podemos criar atributos dinamicamente para nossa instância rex:


In [138]:
rex.sujo = True
rex.sujo


Out[138]:
True

In [139]:
rex.idade = 5
rex.idade


Out[139]:
5

Lembrando mais uma vez que essas mudanças ocorrem somente na instância e não na classe:


In [140]:
Cão.sujo


---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-140-2c0350f0a0ec> in <module>()
----> 1 Cão.sujo

AttributeError: type object 'Cão' has no attribute 'sujo'

In [141]:
Cão.idade


---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-141-f66800d55968> in <module>()
----> 1 Cão.idade

AttributeError: type object 'Cão' has no attribute 'idade'

Classes também são objetos e podemos acessar seus atributos:


In [142]:
Cão.__name__


Out[142]:
'Cão'

In [143]:
Cão.qtd_patas


Out[143]:
4

In [144]:
Cão.nervoso


Out[144]:
False

In [145]:
Cão.carnívoro


Out[145]:
True

In [146]:
Cão.nome


---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-146-f81e0a922209> in <module>()
----> 1 Cão.nome

AttributeError: type object 'Cão' has no attribute 'nome'

Não podemos acessar o nome, pois nome é um atributo que é associado somente a instâncias da classe.


In [147]:
fido = Cão('Fido')
fido.nome


Out[147]:
'Fido'

Os atributos de classe são usados para fornecerer valores padrão para dados que são compartilhados por todos os "cães" como, por exemplo, a quantidade de patas.

Agora vamos criar métodos (funções associadas a classes) para a classe Cão:


In [148]:
class Cão:
    qtd_patas = 4
    carnívoro = True
    nervoso = False
    
    def __init__(self, nome):
        self.nome = nome
        
    def latir(self, vezes=1):
        """ Latir do cão. Quanto mais nervoso mais late. """
        
        vezes += self.nervoso * vezes
        latido = 'Au! ' * vezes
        print('{}: {}'.format(self.nome, latido))

In [149]:
rex = Cão('Rex')
rex.latir()


Rex: Au! 

In [150]:
rex.nervoso = True
rex.latir()


Rex: Au! Au! 

In [157]:
rex.latir(10)


Rex: Au! Au! Au! Au! Au! Au! Au! Au! Au! Au! Au! Au! Au! Au! Au! Au! Au! Au! Au! Au! 

Vamos brincar um pouco mais com o Cão e implementar ainda mais métodos:


In [152]:
class Cão:
    qtd_patas = 4
    carnívoro = True
    nervoso = False
    
    def __init__(self, nome, truques=None):
        self.nome = nome
        if not truques:
            self.truques = []
        else:
            self.truques = list(truques)
        
    def latir(self, vezes=1):
        """ Latir do cão. Quanto mais nervoso mais late. """
        
        vezes += self.nervoso * vezes
        latido = 'Au! ' * vezes
        print('{}: {}'.format(self.nome, latido))
    
    def ensina_truque(self, truque):
        if truque not in self.truques:
            self.truques.append(truque)

In [153]:
fido = Cão('Fido', truques=['Pegar'])

In [154]:
fido.truques


Out[154]:
['Pegar']

In [155]:
fido.ensina_truque('Rolar')
fido.truques


Out[155]:
['Pegar', 'Rolar']

In [156]:
fido.ensina_truque('Pegar')
fido.truques


Out[156]:
['Pegar', 'Rolar']

Métodos de instância, classe e estático

Por padrão os metódos de uma classe são métodos de instância (ou instance methods), isso significa que os métodos recebem, obrigatoriamente, uma instância da classe.

Como por exemplo:


In [13]:
class ExemploInstancia:
    def metodo_instancia(self):
        print('Recebi {}'.format(self))

Não podemos chamar o método de instância somente com a classe:


In [14]:
ExemploInstancia.metodo_instancia()


---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-14-a158cf1c2d73> in <module>()
----> 1 ExemploInstancia.metodo_instancia()

TypeError: metodo_instancia() missing 1 required positional argument: 'self'

Precisamos criar uma instância para utilizá-lo:


In [15]:
inst = ExemploInstancia()
inst.metodo_instancia()


Recebi <__main__.ExemploInstancia object at 0x7ff1440e7080>

Já os métodos de classe (ou class methods) são métodos referentes à classe como um todo e recebem - não uma instância mas o - objeto da classe.

Para tornar o método de uma classe um classmethod usamos o decorador @classmethod. Decoradores são usados para "decorar" (ou marcar) funções e modificar seu comportamento de alguma maneira. Na Aula 05 deste módulo (de orientação a objetos em python) falaremos mais sobre decoradores.

Métodos de classe são definidos e utilizados assim:


In [16]:
class ExemploClasse:
    @classmethod
    def metodo_classe(cls):
        print("Recebi {}".format(cls))

Podemos chamar o método usando o objeto de classe ExemploClasse:


In [17]:
ExemploClasse.metodo_classe()


Recebi <class '__main__.ExemploClasse'>

Também podemos chamar o método a partir de uma instância dessa classe. Por ser um classmethod o método continuará a receber como argumento o objeto da classe e não a instância:


In [18]:
inst = ExemploClasse()
inst.metodo_classe()


Recebi <class '__main__.ExemploClasse'>

Por fim também temos os métodos estáticos que funcionam como funções simples agregadas a objetos ou classes. Eles não recebem argumentos de forma automática:


In [19]:
class Exemplo:
    @staticmethod
    def metodo_estático():
        print('Sou estátio e não recebo nada')

In [20]:
Exemplo.metodo_estático()


Sou estátio e não recebo nada

Também podemos chamar o método estático a partir de uma instância:


In [21]:
inst = Exemplo()
inst.metodo_estático()


Sou estátio e não recebo nada

Se for criar um método estático pense bem se esse método realmente precisa estar associado àquela classe. Muitas vezes podemos, ao invés de usar staticmethod, criar uma função e deixá-la associado ao módulo.

Por exemplo, no framework web Django, a autenticação de usuários são feitos com funções e não métodos estáticos da classe do usuário:

from django.contrib.auth import authenticate, login, logout

def exemplo_login_view(request):
    user = authenticate('usuario', 'senha')
    login(request, user)  # usuário é logado no sistema
    ...

def exemplo_logout_view(request):
    logout(request.user)  # usuário associado aquela requisição é deslogado do sistema
    ...

Exercícios (para casa):

Implementar outras características (atributos) e comportamentos (metodos) para a classe Cão.

Extra:

Implementar testes unitários da classe Cão modificada. Bônus: reimplementar a classe Cão usando TDD.