Desde la llegada de la versión 3, Python se basa to­ta­l­me­n­te en la pro­gra­ma­ción orientada a objetos (OOP) y sigue la filosofía de diseño “eve­r­y­thi­ng is an object”, es decir, que todo son objetos.

A di­fe­re­n­cia de lo que ocurre con Java, C++ y Python 2.x, a partir de esta versión no hay di­fe­re­n­cia entre los tipos pri­mi­ti­vos y los objetos. En Python, las cifras, los strings y las listas, así como las funciones y las clases, son objetos.

En co­m­pa­ra­ción con otros lenguajes de pro­gra­ma­ción, la OOP basada en clases de Python se ca­ra­c­te­ri­za por una elevada fle­xi­bi­li­dad y pocas li­mi­ta­cio­nes. En este sentido, es el opuesto extremo a Java, cuyo sistema OOP se considera ex­tre­ma­da­me­n­te rígido. Te ex­pli­ca­mos de manera práctica cómo funciona en Python la pro­gra­ma­ción orientada a objetos.

¿Para qué sirve la pro­gra­ma­ción orientada a objetos en Python?

La pro­gra­ma­ción orientada a objetos es una forma de pro­gra­ma­ción im­pe­ra­ti­va. Los objetos combinan datos y fu­n­cio­na­li­dad. Un objeto encapsula su estado interno, y se puede acceder a él mediante una interfaz pública llamada interfaz del objeto. La interfaz del objeto viene definida por sus métodos. Los objetos in­ter­ac­túan unos con otros mediante mensajes que se trasmiten llamando a los métodos.

Consejo

Para entender mejor el contexto de la OOP de Python, consulta nuestros artículos “Qué es OOP”, “Pa­ra­di­g­mas de pro­gra­ma­ción” y “Tutorial de Python”.

En­ca­p­su­lar objetos OOP con Python

Veamos con un ejemplo cómo puedes usar la OOP para en­ca­p­su­lar objetos en Python. Supón que estás es­cri­bie­n­do código para una cocina, un bar o un la­bo­ra­to­rio. Ne­ce­si­ta­rás modelar re­ci­pie­n­tes como botellas, vasos, tazas, etc.; todos objetos que tienen un volumen y se pueden llenar. Una categoría de objetos es una “clase”.

Los objetos que re­pre­se­n­tan esos re­ci­pie­n­tes tienen un estado interno que puede cambiar. Estos co­n­te­ne­do­res pueden re­lle­nar­se, vaciarse y demás. Si tienen tapa, puedes abrirlos y cerrarlos. Sin embargo, ló­gi­ca­me­n­te no es posible modificar el volumen de un re­ci­pie­n­te a po­s­te­rio­ri. Ob­via­me­n­te hay que hacerse múltiples preguntas sobre su estado, por ejemplo:

  • “¿El vaso está lleno?”
  • “¿Qué volumen tiene la botella?”
  • “¿El re­ci­pie­n­te tiene tapa?”

Además, tiene sentido que haya una relación entre los objetos. Por ejemplo, debería poder tra­n­s­fe­ri­r­se el contenido de un vaso a una botella. A co­n­ti­nua­ción, verás cómo cambia el estado interno de un objeto con la pro­gra­ma­ción orientada a objetos en Python. Los cambios o preguntas que se hacen sobre el estado mostrado se llevan a cabo con llamadas a métodos:

# create an empty cup with given capacity
cup = Container(400)
assert cup.volume() == 400
assert not cup.is_full()
# add some water to the cup
cup.add('Water', 250)
assert cup.volume_filled() == 250
# add more water, filling the cup
cup.add('Water', 150)
assert cup.is_full()
Python

Definir tipos con OOP en Python

Los tipos de datos son un concepto básico de la pro­gra­ma­ción. Los distintos tipos de datos pueden usarse de forma y manera distinta: las cifras se procesan mediante ope­ra­cio­nes ari­t­mé­ti­cas y las cadenas de ca­ra­c­te­res (o strings) pueden exa­mi­nar­se.

# addition works for two numbers
39 + 3
# we can search for a letter inside a string
'y' in 'Python'
Python

Si intentas sumar una cifra y un string o buscar dentro de un número, te dará un error de tipo:

# addition doesn't work for a number and a string
42 + 'a'
# cannot search for a letter inside a number
'y' in 42
Python

Los tipos in­te­gra­dos en Python son ab­s­tra­c­tos. Una cifra puede re­pre­se­n­tar de todo: distancia, tiempo, dinero. El si­g­ni­fi­ca­do del valor solo se indica con el nombre de la variable:

# are we talking about distance, time?
x = 51
Python

¿Pero y si quieres modelar conceptos es­pe­cia­li­za­dos? Eso también puede hacerse con la pro­gra­ma­ción orientada a objetos y Python. Los objetos son es­tru­c­tu­ras de datos con tipos ide­n­ti­fi­ca­bles que pueden mostrarse con la función in­co­r­po­ra­da type():

# class 'str'
type('Python')
# class 'tuple'
type(('Walter', 'White'))
Python

Crear ab­s­tra­c­cio­nes con Python OOP

En la pro­gra­ma­ción se usan las ab­s­tra­c­cio­nes para ocultar las co­m­ple­ji­da­des. Estas permiten a los pro­gra­ma­do­res operar a un nivel más elevado. Por ejemplo: es lo mismo preguntar “¿El vaso está lleno?” que “¿El volumen del contenido del vaso es igual al volumen del vaso?”. La primera versión más abstracta es más corta y concisa y, por tanto, pre­fe­ri­ble. Las ab­s­tra­c­cio­nes permiten crear y co­m­pre­n­der sistemas más complejos:

# instantiate an empty glass
glass = Container(250)
# add water to the glass
glass.add('Water', 250)
# is the glass full?
assert glass.is_full()
# a longer way to ask the same question
assert glass.volume_filled() == glass.volume()
Python

En Python, la OOP permite aplicar conceptos ab­s­tra­c­tos a ideas nuevas. He aquí un ejemplo con el operador de suma de Python. El signo más suele conectar dos cifras, pero también puede usarse para sumar el contenido de dos (o más) listas:

assert 42 + 9 == 51
assert ['Jack', 'John'] + ['Jim'] == ['Jack', 'John', 'Jim']
Python

Puedes aplicar fá­ci­l­me­n­te el concepto de la suma al ejemplo. Definir un operador de suma para los re­ci­pie­n­tes permite escribir un código que se lee prá­c­ti­ca­me­n­te como una lengua normal. Más abajo tienes la im­ple­me­n­ta­ción, pero primero veamos un ejemplo de cómo se usa:

# pitcher with 1000 ml capacity
pitcher = Container(1000)
# glass with 250 ml capacity
glass = Container(250)
# fill glass with water
glass.fill('Water')
# transfer the content from the glass to the pitcher
pitcher += glass
# pitcher now contains water from glass
assert pitcher.volume_filled() == 250
# glass is empty
assert glass.is_empty()
Python

¿Cómo funciona la pro­gra­ma­ción orientada a objetos en Python?

Los objetos combinan datos y funciones; ambos llamados atributos. A di­fe­re­n­cia de Java, PHP y C++, la OOP de Python no dispone de palabras clave como private o protected para re­s­tri­n­gir el acceso a los atributos. En vez de eso, aplica la siguiente co­n­ve­n­ción: los atributos que empiezan con un guion bajo se co­n­si­de­ran no públicos. Pueden ser atributos de datos o métodos que sigan el esquema _internal_attr o _internal_method().

En Python, los métodos se definen con la variable self como primer parámetro. Para acceder a un atributo de objeto desde dentro del objeto hay que hacer re­fe­re­n­cia a self. En Python self funciona como marcador de posición de una instancia concreta, por lo que es parecida a la palabra clave this en Java, PHP, Ja­va­S­cri­pt y C++.

Además de la co­n­ve­n­ción que me­n­cio­na­mos arriba, también se crea un esquema sencillo para el en­ca­p­su­la­do: acceder a un atributo interno con la re­fe­re­n­cia self._internal, ya que esta se encuentra dentro del objeto. Acceder desde fuera con algo tipo obj._internal va contra el en­ca­p­su­la­do, por lo que conviene evitarlo:

class ExampleObject:
    def public_method(self):
        self._internal = 'changed from inside method'
# instantiate object
obj = ExampleObject()
# this is fine
obj.public_method()
assert obj._internal == 'changed from inside method'
# works, but not a good idea
obj._internal = 'changed from outside'
Python

Clases

Una clase es como una plantilla para los objetos. Se dice que un objeto se instancia a partir de las clases, o sea, que se crea según la plantilla. Según la co­n­ve­n­ción, los nombres de las clases definidos por el usuario empiezan con mayúscula.

A di­fe­re­n­cia de lo que sucede con Java, C++, PHP y Ja­va­S­cri­pt, en Python OOP no existe la palabra clave new. En su lugar, el nombre de la clase se invoca como una función y sirve de co­n­s­tru­c­tor que devuelve una nueva instancia. Im­plí­ci­ta­me­n­te, el co­n­s­tru­c­tor invoca la función de ini­cia­li­za­ción __init__(), que ini­cia­li­za los datos del objeto.

La siguiente tabla recopila los patrones ex­pli­ca­dos hasta el momento con ejemplos de código. Ahora verás cómo se modela el concepto de co­n­te­ne­dor con una clase llamada Container y cómo se definen métodos para in­ter­ac­cio­nes im­po­r­ta­n­tes:

Método Ex­pli­ca­ción
__init__ Ini­cia­li­za el co­n­te­ne­dor nuevo con los valores iniciale.
__repr__ Muestra el estado del co­n­te­ne­dor en forma de texto.
volume Muestra la capacidad del co­n­te­ne­dor.
volume_filled Expresa cómo de lleno está el co­n­te­ne­dor.
volume_available Indica el espacio que queda libre en el co­n­te­ne­dor.
is_empty Indica si el co­n­te­ne­dor está vacío.
is_full Indica si el co­n­te­ne­dor está lleno.
empty Vacía el re­ci­pie­n­te y devuelve el contenido.
_add Método interno que añade una sustancia sin hacer co­m­pro­ba­cio­nes.
add Método público que añade la cantidad es­ta­ble­ci­da de una sustancia siempre y cuando haya espacio su­fi­cie­n­te.
fill Rellena el espacio que queda libre en el co­n­te­ne­dor con una sustancia.
pour_into Vierte todo el contenido del re­ci­pie­n­te en otro co­n­te­ne­dor.
__add__ Im­ple­me­n­ta el operador de la suma para re­ci­pie­n­tes; recurre al método pour_into.

Este es el código de la clase Container. Una vez que lo hayas ejecutado en tu REPL de Python lo­ca­l­me­n­te, podrás hacer pruebas con los otros ejemplos de código del artículo:

class Container:
    def __init__(self, volume):
        # volume in ml
        self._volume = volume
        # start out with empty container
        self._contents = {}
    
    def __repr__(self):
        """
        Textual representation of container
        """
        repr = f"{self._volume} ml Container with contents {self._contents}"
        return repr
    
    def volume(self):
        """
        Volume getter
        """
        return self._volume
    
    def is_empty(self):
        """
        Container is empty if it has no contents
        """
        return self._contents == {}
    
    def is_full(self):
        """
        Container is full if volume of contents equals capacity
        """
        return self.volume_filled() == self.volume()
    
    def volume_filled(self):
        """
        Calculate sum of volumes of contents
        """
        return sum(self._contents.values())
    
    def volume_available(self):
        """
        Calculate available volume
        """
        return self.volume() - self.volume_filled()
    
    def empty(self):
        """
        Empty the container, returning its contents
        """
        contents = self._contents.copy()
        self._contents.clear()
        return contents
    
    def _add(self, substance, volume):
        """
        Internal method to add a new substance / add more of an existing substance
        """
        # update volume of existing substance
        if substance in self._contents:
            self._contents[substance] += volume
        # or add new substance
        else:
            self._contents[substance] = volume
    
    def add(self, substance, volume):
        """
        Public method to add a substance, possibly returning left over
        """
        if self.is_full():
            raise Exception("Cannot add to full container")
        # we can fit all of the substance
        if self.volume_filled() + volume <= self.volume():
            self._add(substance, volume)
            return self
        # we can fit part of the substance, returning the left over
        else:
            leftover = volume - self.volume_available()
            self._add(substance, volume - leftover)
            return {substance: leftover}
    
    def fill(self, substance):
        """
        Fill the container with a substance
        """
        if self.is_full():
            raise Exception("Cannot fill full container")
        self._add(substance, self.volume_available())
        return self
    
    def pour_into(self, other_container):
        """
        Transfer contents of container to another container
        """
        if other_container.volume_available() < self.volume_filled():
            raise Exception("Not enough space")
        # get the contents by emptying container
        contents = self.empty()
        # add contents to other container
        for substance, volume in contents.items():
            other_container.add(substance, volume)
        return other_container
    
    def __add__(self, other_container):
        """
        Implement addition for containers:
        `container_a + container_b` <=> `container_b.pour_into(container_a)`
        """
        other_container.pour_into(self)
        return self
Python

Aquí tienes algunos ejemplos de la im­ple­me­n­ta­ción de nuestro co­n­te­ne­dor. Primero se instancia un vaso y se llena de agua. Como es de esperar, el vaso luego estará lleno:

glass = Container(300)
glass.fill('Water')
assert glass.is_full()
Python

En el siguiente paso, se vacía el vaso, que volverá a tener la cantidad de agua que tenía an­te­rio­r­me­n­te. La im­ple­me­n­ta­ción parece funcionar: el vaso está vacío.

contents = glass.empty()
assert contents == {'Water': 300}
assert glass.is_empty()
Python

Ahora un ejemplo algo más complejo. Para mezclar en una jarra vino y zumo de naranja, primero es necesario crear los re­ci­pie­n­tes y re­lle­nar­los de los líquidos co­rre­s­po­n­die­n­tes:

pitcher = Container(1500)
bottle = Container(700)
carton = Container(500)
# fill ingredients
bottle.fill('Red wine')
carton.fill('Orange juice')
Python

Luego se usa el operador de suma y asi­g­na­ción += para verter el contenido de ambos re­ci­pie­n­tes en la jarra.

# pour ingredients into pitcher
pitcher += bottle
pitcher += carton
# check that everything worked
assert pitcher.volume_filled() == 1200
assert bottle.is_empty() and carton.is_empty()
Python

Esto ha sido posible porque la clase Container ha im­ple­me­n­ta­do el método __add__(). Entre ba­s­ti­do­res, la asi­g­na­ción pitcher += bottle se convierte en pitcher = pitcher + bottle. Además, Python traduce pitcher + bottle al llamar el método pitcher. __add__(bottle). Nuestro método __add__() devuelve el recibidor, en este caso la jarra o pitcher, para que funcione la asi­g­na­ción.

Atributos es­ta­dí­s­ti­cos

Hasta ahora has visto cómo acceder a los atributos de los objetos: desde fuera a través de métodos públicos; dentro de los métodos con la re­fe­re­n­cia self. El estado interno de un objeto se alcanza mediante atributos de datos que pe­r­te­ne­cen al objeto co­rre­s­po­n­die­n­te. Pero los métodos de un objeto también están vi­n­cu­la­dos a una instancia concreta. Sin embargo, hay atributos que pe­r­te­ne­cen a clases, lo cual tiene sentido ya que, en Python, las clases también son objetos.

Los atributos de clases también se denominan atributos “estáticos” porque ya existen antes de in­s­ta­n­ciar el objeto. Pueden ser tanto atributos de datos como métodos. Esto es útil para las co­n­s­ta­n­tes que son iguales para todas las in­s­ta­n­cias de una clase, al igual que para métodos que no operan con self. Las rutinas de co­n­ve­r­sión suelen im­ple­me­n­tar­se como métodos estáticos.

A di­fe­re­n­cia de otros lenguajes como Java y C++, Python no usa a la palabra clave static para di­s­ti­n­guir entre atributos de objeto y atributos de clase. En su lugar, recurre a un decorador llamado @sta­ti­c­me­thod. Más abajo verás un ejemplo de método estático para la clase Container. En él se im­ple­me­n­ta una rutina de co­n­ve­r­sión para pasar de onzas líquidas a mi­li­li­tros:

# inside of class `Container`
    ...
    @staticmethod
    def floz_from_ml(ml):
        return ml * 0.0338140227
Python

A los atributos estáticos se accede como siempre haciendo re­fe­re­n­cia al atributo con la notación del punto siguiendo la es­tru­c­tu­ra obj.attr. La única di­fe­re­n­cia es que a la izquierda del punto va el nombre de la clase: ClassName.static_method(). Esta lógica es co­n­si­s­te­n­te ya que en la pro­gra­ma­ción orientada a objetos en Python las clases también son objetos. De esta manera, para realizar la co­n­ve­r­sión de la clase Container, habrá que escribir:

floz = Container.floz_from_ml(1000)
assert floz == 33.8140227
Python

In­te­r­fa­ces

La interfaz es la agru­pa­ción de todos los métodos públicos de un objeto. Esta define y documenta el co­m­po­r­ta­mie­n­to de un objeto y sirve como API. Python, a di­fe­re­n­cia de C++, no tiene planos distintos para la interfaz (archivos de en­ca­be­za­do) y la im­ple­me­n­ta­ción; y a di­fe­re­n­cia de Java y PHP, tampoco tiene una palabra clave explícita de interface. En estos lenguajes, las in­te­r­fa­ces tienen firmas de métodos y sirven como de­s­cri­p­ción de sus funciones.

Dado que en Python la in­fo­r­ma­ción sobre los métodos de los que dispone un objeto y la clase a partir de la cual se ha in­s­ta­n­cia­do se determina di­ná­mi­ca­me­n­te en tiempo de ejecución, el lenguaje no requiere in­te­r­fa­ces ex­plí­ci­tas. En realidad, Python OOP aplica el principio del “Duck Typing”:

Cita

“If it walks like a duck and it quacks like a duck, then it must be a duck” — Fuente: https://docs.python.org/3/glossary.html#term-duck-typing Tra­du­c­ción: “Si camina como un pato y grazna como un pato, tendrá que ser un pato”. (Tra­du­c­ción de IONOS)

¿Qué significa Duck Typing? Bá­si­ca­me­n­te, un objeto de Python puede usar una clase como un objeto de otra clase siempre y cuando contenga los métodos ne­ce­sa­rios para ello. Para ilu­s­trar­lo, imagina un pato ficticio que hace los mismos sonidos que un pato, nada como un pato y los patos de verdad lo perciben como tal.

Tra­n­s­mi­sión por herencia

Al igual que en la mayoría de los lenguajes orie­n­ta­dos a objetos, la OOP de Python también aplica el concepto de herencia en la que una clase se puede definir como es­pe­cia­li­za­ción de una clase madre. Al continuar este proceso se va creando una jerarquía de clases en forma de árbol con la clase Object pre­de­fi­ni­da como raíz. Tanto Python como C++ (pero no Java ni PHP) permiten la herencia múltiple: una clase puede proceder de varias clases madre.

La herencia múltiple ofrece cierta fle­xi­bi­li­dad. Por ejemplo, permite ejecutar los “mixins” conocidos de Ruby o los “traits” de PHP. Asimismo, la división de las funciones de Java en in­te­r­fa­ces y clases ab­s­tra­c­tas puede aplicarse en Python con la herencia múltiple.

Volviendo al ejemplo de los re­ci­pie­n­tes, ahora verás cómo funciona la herencia múltiple en Python. Algunos co­n­te­ne­do­res pueden tener tapa; debes modificar la clase Container para ello. Tendrás que definir una nueva clase heredada de Container, Sea­la­ble­Co­n­tai­ner, así como una nueva clase Sealable que contiene métodos para poner y quitar la tapa. La clase Sealable es un “mixin” porque solo sirve para dar a otra clase más im­ple­me­n­ta­cio­nes de métodos:

class Sealable:
    """
    Implementation needs to:
    - initialize `self._seal`
    """
    def is_sealed(self):
        return self._seal is not None
    
    def is_open(self):
        return not self.is_sealed()
    
    def is_closed(self):
        return not self.is_open()
    
    def open(self):
        """
        Opening removes and returns the seal
        """
        seal = self._seal
        self._seal = None
        return seal
    
    def seal_with(self, seal):
        """
        Closing attaches the seal and returns the Sealable
        """
        self._seal = seal
        return self
Python

Sea­la­ble­Co­n­tai­ner proviene de la clase Container y del mixin Sealable. Ahora debes so­bre­s­cri­bir el método __init__() y definir dos pa­rá­me­tros nuevos que te permitan es­ta­ble­cer el contenido y la tapa del Sea­la­ble­Co­n­tai­ner al in­s­ta­n­ciar­lo. Esto es necesario para crear re­ci­pie­n­tes cerrados con contenido. Dentro del método __init__(), usa super() para ini­cia­li­zar la clase madre:

class SealableContainer(Container, Sealable):
    """
    Start out with empty, open container
    """
    def __init__(self, volume, contents = {}, seal = None):
        # initialize `Container`
        super().__init__(volume)
        # initialize contents
        self._contents = contents
        # initialize `self._seal`
        self._seal = seal
    
    def __repr__(self):
        """
        Append 'open' / 'closed' to textual container representation
        """
        state = "Open" if self.is_open() else "Closed"
        repr = f"{state} {super().__repr__()}"
        return repr
    
    def empty(self):
        """
        Only open container can be emptied
        """
        if self.is_open():
            return super().empty()
        else:
            raise Exception("Cannot empty sealed container")
    
    def _add(self, substance, volume):
        """
        Only open container can have its contents modified
        """
        if self.is_open():
            super()._add(substance, volume)
        else:
            raise Exception("Cannot add to sealed container")
Python

Tal y como has hecho con el método __init__(), anula otros métodos que quieras para di­fe­re­n­ciar los Sea­la­ble­Co­n­tai­ner de los re­ci­pie­n­tes sin tapa. So­bre­s­cri­be __repr__() para que además se indique el estado abierto/cerrado. Asimismo, so­bre­s­cri­be los métodos empty() y_add(), que solo tienen sentido con re­ci­pie­n­tes abiertos. De esta manera, obligas a abrir un co­n­te­ne­dor cerrado antes de vaciarlo o re­lle­nar­lo. De nuevo, utiliza super() para acceder a las funciones de la clase madre.

Veamos un ejemplo. Imagina que quieres hacer un cuba libre. Para ello necesitas un vaso, una botella pequeña de cola y un vaso de chupito con 20 cl de ron:

glass = Container(330)
cola_bottle = SealableContainer(250, contents = {'Cola': 250}, seal = 'Bottlecap')
shot_glass = Container(40)
shot_glass.add('Rum', 20)
Python

Debes meter hielo en el vaso y añadir el ron. Como la botella de cola está cerrada, primero hay que abrirla y echar el contenido en el vaso:

glass.add('Ice', 50)
# add rum
glass += shot_glass
# open cola bottle
if cola_bottle.is_closed():
    cola_bottle.open()
# pour cola into glass
glass += cola_bottle
Python
Ir al menú principal