Programación Orientada a Objetos

El soporte de POO de Python es bastante completo, comparable a Java, C# o C++. El concepto esencial de "objeto" se refiere a la existencia de un estado persistente. Las funciones solo operan en relación a sus parámetros para obtener un resultado y, por regla general, no tienen estado.

Los objetos sin embargo tienen atributos, que son datos accesibles por los métodos (funciones asociadas al objeto) entre invocaciones sucesivas durante la vida del objeto. Los objetos que tienen los mismo métodos y los mismo atributos son instancias de una misma clase. Las clases se definen con la palabra reservada class:


In [1]:
%%file bank/__init__.py
class BankAccount(object):
    def __init__(self):
        self.balance = 0

    def withdraw(self, amount):
        self.balance -= amount
        return self.balance

    def deposit(self, amount):
        self.balance += amount
        return self.balance
    
# ejemplo extraido de "python-practice-book"


Overwriting bank/__init__.py

In [2]:
from bank import BankAccount
BankAccount.withdraw


Out[2]:
<unbound method BankAccount.withdraw>

In [3]:
b = BankAccount()
b.withdraw


Out[3]:
<bound method BankAccount.withdraw of <bank.BankAccount object at 0x7f119c389f50>>

En este ejemplo:

  • Todos los métodos tienen un primer parámetro llamado self que representa a la instancia.
  • El método init es un método especial que se utiliza para inicializar los atributos (no es estrictamente un constructor).
  • La clase BankAccount hereda de object porque es una clase del nuevo estilo (en python-3 todas lo son).

A continuación crearemos 2 instancias de esta clase:


In [4]:
account_a = BankAccount()
account_b = BankAccount()

account_a.balance


Out[4]:
0

In [5]:
account_a.deposit(200)


Out[5]:
200

In [6]:
account_a.deposit(100)


Out[6]:
300
  • [E]: Añade un método transfer_to(self, account, amount)
  • [E]: Modifica el método withdraw() para que eleve una excepción si se intenta retirar una cantidad mayor a la que hay en cuenta.

El atributo __dict__

Es un diccionario que contiene todos los atributos y métodos de la clase:


In [7]:
BankAccount.__dict__


Out[7]:
<dictproxy {'__dict__': <attribute '__dict__' of 'BankAccount' objects>,
 '__doc__': None,
 '__init__': <function bank.__init__>,
 '__module__': 'bank',
 '__weakref__': <attribute '__weakref__' of 'BankAccount' objects>,
 'deposit': <function bank.deposit>,
 'withdraw': <function bank.withdraw>}>

Si se aplica a una instancia, contiene los atributos:


In [8]:
account_a.__dict__


Out[8]:
{'balance': 300}

Métodos de clase

Métodos que toman la clase como estado. No requieren una instancia.


In [9]:
class A(object):
    _size = 20
    
    @classmethod
    def size(cls):
        return cls._size

A.size()


Out[9]:
20

In [10]:
A().size()


Out[10]:
20

Métodos "estáticos"

Son simples funciones vinculadas al ámbito de la clase.


In [11]:
class A(object):
    @staticmethod
    def show():
        return "static"

print(A.show())
print(A().show())


static
static

Atributos de clase

Atributos compartidos por todas las instancias


In [12]:
class A(object):
    names = []
    def add(self, name):
        self.names.append(name)
        
a1 = A()
a2 = A()

a1.add("foo")
a2.add("bar")

print(a1.names)
print(A.names)


['foo', 'bar']
['foo', 'bar']

Herencia

(como medio de especialización)

La herencia de implementación permite crear clases que heredan el comportamiento de otra clase, pero aplicando un comportamiento especial en algún aspecto:


In [13]:
class MinimumBalanceException(Exception):
    pass

class MinimumBalanceAccount(BankAccount):
    def __init__(self, minimum_balance):
        super(MinimumBalanceAccount, self).__init__()
        self.minimum_balance = minimum_balance

    def withdraw(self, amount):
        if self.balance - amount < self.minimum_balance:
            raise MinimumBalanceException(self.balance - amount)
            
        return BankAccount.withdraw(self, amount)
        
minimum_account = MinimumBalanceAccount(100)
minimum_account.deposit(400)
minimum_account.withdraw(350)


---------------------------------------------------------------------------
MinimumBalanceException                   Traceback (most recent call last)
<ipython-input-13-35af6a797aaf> in <module>()
     15 minimum_account = MinimumBalanceAccount(100)
     16 minimum_account.deposit(400)
---> 17 minimum_account.withdraw(350)

<ipython-input-13-35af6a797aaf> in withdraw(self, amount)
      9     def withdraw(self, amount):
     10         if self.balance - amount < self.minimum_balance:
---> 11             raise MinimumBalanceException(self.balance - amount)
     12 
     13         return BankAccount.withdraw(self, amount)

MinimumBalanceException: 50

Información de tipo

Tipo de un objeto:


In [14]:
type(account_a)


Out[14]:
bank.BankAccount

In [15]:
type(minimum_account)


Out[15]:
__main__.MinimumBalanceAccount

Pertenencia a una clase:


In [16]:
help(isinstance)


Help on built-in function isinstance in module __builtin__:

isinstance(...)
    isinstance(object, class-or-type-or-tuple) -> bool
    
    Return whether an object is an instance of a class or of a subclass thereof.
    With a type as second argument, return whether that is the object's type.
    The form using a tuple, isinstance(x, (A, B, ...)), is a shortcut for
    isinstance(x, A) or isinstance(x, B) or ... (etc.).


In [17]:
isinstance(account_a, BankAccount)


Out[17]:
True

In [18]:
isinstance(account_a, MinimumBalanceAccount)


Out[18]:
False

Comprobación de la jerarquía:


In [19]:
help(issubclass)


Help on built-in function issubclass in module __builtin__:

issubclass(...)
    issubclass(C, B) -> bool
    
    Return whether class C is a subclass (i.e., a derived class) of class B.
    When using a tuple as the second argument issubclass(X, (A, B, ...)),
    is a shortcut for issubclass(X, A) or issubclass(X, B) or ... (etc.).


In [20]:
issubclass(MinimumBalanceAccount, BankAccount)


Out[20]:
True

In [21]:
issubclass(MinimumBalanceAccount, object)


Out[21]:
True

De primera clase

Tanto las clases como los métodos son ciudadanos de primera clase, es decir, se pueden almacenar, pasar por parámetro o usar como valor de retorno:


In [22]:
f = account_a.deposit
f(100)


Out[22]:
400

In [23]:
BankAccount


Out[23]:
bank.BankAccount

In [24]:
class A(object):
    pass

class B(A):
    pass

def make(cls):
    return cls()

a = make(A)
a


Out[24]:
<__main__.A at 0x7f119c0a3990>

type es la clase de las clases, en decir, una metaclase. Concretamente es la superclase de todas las metaclases.


In [25]:
type(A)


Out[25]:
type

In [26]:
type(type)


Out[26]:
type

Creando una clase al vuelo con type():


In [27]:
SomeClass = type("SomeClass", (), {})
some = SomeClass()
some


Out[27]:
<__main__.SomeClass at 0x7f119c0a3750>

Introspección

Clases base de una clase


In [28]:
B.__bases__


Out[28]:
(__main__.A,)

Clases que especializan una clase:


In [29]:
A.__subclasses__()


Out[29]:
[__main__.B]

Orden de resolución de métodos:


In [30]:
B.mro()


Out[30]:
[__main__.B, __main__.A, object]

Clase de una instancia:


In [31]:
some.__class__


Out[31]:
__main__.SomeClass

Averiguar si un objeto tiene un atributo:


In [32]:
hasattr(some, "missing_attribute")


Out[32]:
False

Obtener o cambiar el valor de un atributo dado su nombre:


In [33]:
setattr(some, "foo", 5)
getattr(some, "foo")


Out[33]:
5