Esta es la segunda y última parte del articulo
http://developinginspanish.com/2019/11/02/apendiendo-python-de-cero-a-heroe/
Clases y objetos
Los objetos son una representación de objetos del mundo real, como coches, perros o bicicletas. Los objetos comparten dos características principales: datos y comportamiento.
Los coches tienen datos, como el número de ruedas, numero de puertas o asientos. También tienen un comportamiento: pueden acelerar, frenar, mostrar la gasolina que les queda y muchas otras cosas.
Identificamos los datos como atributos y al comportamiento como métodos en la programación orientada a objetos. Es decir:
Datos -> atributos y comportamiento -> métodos
Una clase es la plantilla a partir de la que se crean los objetos individuales. En el mundo real, a menudo encontramos objetos del mismo tipo. Como los coches. Todos de la misma marca y modelo (y todos tienen motor, ruedas, puertas y demás). Cada coche se ha construido a partir de la misma plantilla y tiene los mismos componentes.
Python como lenguaje orientado a objetos
Python, como lenguaje de programación orientado a objetos tiene estos dos conceptos: clase y objeto.
Una clase es una plantilla, un modelo para sus objetos.
Así que, una clase no es más que un modelo o modo de definir atributos y comportamiento (como dijimos anteriormente). Por ejemplo, la clase vehículo tiene sus propios atributos que definen que objetos son vehículos. El número de ruedas, el tipo de depósito de combustible, los asientos y la velocidad máxima son todos atributos del vehículo.
Teniendo esto en cuenta, vamos a ver la sintaxis de Python para las clases:
class Vehicle:
pass
Definimos las clases con la sentencia class y eso es todo. Fácil, ¿no?
Los objetos son instancias de una clase. Creamos una instancia dándole un nombre a la clase.
car = Vehicle()
print(car) # <__main__.Vehicle instance at 0x7fb1de6c2638>
Aquí, ‘car’ es un objeto o instancia de la clase ‘Vehicle’.
Recuerda que nuestra clase ‘vehicle’ tiene cuatro atributos: número de ruedas, tipo de depósito, asientos y velocidad máxima. Establecemos todos estos atributos cuando creamos un objeto ‘vehicle’. Así pues, definimos nuestra para que reciba datos cuando se inicializa:
class Vehicle:
def __init__(self, number_of_wheels, type_of_tank, seating_capacity, maximum_velocity):
self.number_of_wheels = number_of_wheels
self.type_of_tank = type_of_tank
self.seating_capacity = seating_capacity
self.maximum_velocity = maximum_velocity
Usamos el método ‘init’. Lo llamamos método constructor. Así, cuando creamos el objeto ‘vehicle’, podemos definir estos atributos. Imagina que nos encanta el Tesla Model S, y queremos crear ese tipo de objeto. Tiene cuatro ruedas, es eléctrico, tiene cinco asientos y y su velocidad máxima son 250km/h. Creemos el objeto:
tesla_model_s = Vehicle(4, 'electric', 5, 250)
Cuatro ruedas + eléctrico + cinco asientos + 250km/h máx.
Hemos establecido todos los atributos. Pero, ¿cómo podemos acceder a los valores de estos atributos? Enviamos un mensaje al objeto preguntando por ellos. Se le llama método. Es el comportamiento del objeto. Implementémoslo.
class Vehicle:
def __init__(self, number_of_wheels, type_of_tank, seating_capacity, maximum_velocity):
self.number_of_wheels = number_of_wheels
self.type_of_tank = type_of_tank
self.seating_capacity = seating_capacity
self.maximum_velocity = maximum_velocity
def number_of_wheels(self):
return self.number_of_wheels
def set_number_of_wheels(self, number):
self.number_of_wheels = number
Esta es una implementación de dos métodos: number_of_wheels y set_number_of_wheels. Se les llama ‘getter’ y ‘setter’. Ya que el primero obtiene (get) el valor del atributo y el segundo estable (set) un nuevo valor para el atributo.
En Python, esto lo podemos hacer usando @property (decoradores) para definir los ‘getters’ y ‘setters’. Veámoslo en el código:
class Vehicle:
def __init__(self, number_of_wheels, type_of_tank, seating_capacity, maximum_velocity):
self.number_of_wheels = number_of_wheels
self.type_of_tank = type_of_tank
self.seating_capacity = seating_capacity
self.maximum_velocity = maximum_velocity
@property
def number_of_wheels(self):
return self.__number_of_wheels
@number_of_wheels.setter
def number_of_wheels(self, number):
self.__number_of_wheels = number
Y podemos usar estos métodos como atributos:
tesla_model_s = Vehicle(4, 'electric', 5, 250)
print(tesla_model_s.number_of_wheels) # 4
tesla_model_s.number_of_wheels = 2 # setting number of wheels to 2
print(tesla_model_s.number_of_wheels) # 2
Esto es ligeramente diferente a definir métodos. Los métodos funcionan como atributos. Por ejemplo, cuando establecemos el nuevo número de ruedas, no aplicamos dos como parámetro, sino que establecemos el valor 2 a ‘number_of_wheels’. Este es un modo de escribir ‘getter’ y ‘setter’ ‘pythonicos’.
Pero también podemos usar métodos para otras cosas, como el método ‘make_noise’ (hacer ruido). Veámoslo:
class Vehicle:
def __init__(self, number_of_wheels, type_of_tank, seating_capacity, maximum_velocity):
self.number_of_wheels = number_of_wheels
self.type_of_tank = type_of_tank
self.seating_capacity = seating_capacity
self.maximum_velocity = maximum_velocity
def make_noise(self):
print('VRUUUUUUUM')
Cuando llamamos a este método devuelve un string ‘VRUUUUUUUM’.
tesla_model_s = Vehicle(4, 'electric', 5, 250)
tesla_model_s.make_noise() # VRUUUUUUUM
Encapsulado: ocultando información
El encapsulado es un mecanismo que restringe el acceso directo a los datos de un objeto y sus métodos. Pero a la vez, facilita las operaciones con esos datos (métodos del objeto).
«El encapsulado puede usarse para ocultar datos y funciones. Bajo esta definición, el encapsulado significa que la representación interna de un objeto está generalmente oculta fuera de la definición del objeto» – Wikipedia
Toda representación interna de un objeto está oculta desde fuera. Sólo el objeto puede interactuar con sus datos internos.
Primero, necesitamos entender como funcionan las variables públicas y no públicas y lo métodos.
Variables de instancia pública
Para una clase de Python, puede inicializarse una variable de instancia pública dentro de nuestro método constructor. Veámoslo:
Dentro del método constructor
class Person:
def __init__(self, first_name):
self.first_name = first_name
Aquí aplicamos el valor ‘first_name’ como argumento de la variable de instancia pública,
tk = Person('TK')
print(tk.first_name) # => TK
Dentro de la clase
class Person:
first_name = 'TK'
Aquí, no necesitamos aplicar ‘first_name’ como argumento y todos las instancias tendrán un atributo de clase inicializado con ‘TK’
tk = Person()
print(tk.first_name) # => TK
Bien. Hemos aprendido que podemos usar variables de instancia pública y atributos de clase. Otra parte interesante acerca de la parte pública es que podemos gestionar el valor de la variable. ¿Qué quiere decir eso? Nuestro objeto puede gestionar su propio valor de variable: valores de variable ‘get’ y ‘set’.
Teniendo en cuenta la clase ‘Person’, queremos asignar otro valor a la variable ‘first_name’.
tk = Person('TK')
tk.first_name = 'Kaio'
print(tk.first_name) # => Kaio
Aquí lo tenemos. Acabamos de asignar otro valor (Kaio) a la variable de instancia ‘first_name’ y actualizar su valor. Así de sencillo. Como es una variable pública, podemos hacerlo.
Variables de instancia no públicas
No usamos el término ‘privado’, ya que no existen atributos realmente privados en Python (sin una innecesaria cantidad de trabajo ) – PEP8
Igual que las variables de instancia públicas, podemos definir variables de instancia no públicas tanto dentro del método constructor como dentro de la clase. La diferencia en la sintaxis es: para variables de instancia no públicas, usamos un subrayado (_) antes del nombre de la variable.
«No existen en Python variables de instancia ‘privadas’ que no puedan accederse más que dentro del objeto. Sin embargo, hay una convención seguida por la mayoría de código Python: un nombre prefijado con subrayado (p.ej ‘_nombre’) debe tratarse como una parte no pública del API (tanto si es una función, un método o un dato)» – Fundación de Software Python
Como ejemplo:
class Person:
def __init__(self, first_name, email):
self.first_name = first_name
self._email = email
¿Te has fijado en la variable ‘email’? Así es como se define una variable no pública:
tk = Person('TK', 'tk@mail.com')
print(tk._email) # tk@mail.com
Podemos acceder a él y actualizarlo. Las variables no públicas son sólo una convención y deben se tratados como una parte no pública del API.
Así pues, usamos un método que nos permite hacerlo dentro de la definición de la clase. Implementemos dos métodos (email y update_email) para comprenderlo:
class Person:
def __init__(self, first_name, email):
self.first_name = first_name
self._email = email
def update_email(self, new_email):
self._email = new_email
def email(self):
return self._email
Ahora podemos actualizar y acceder a las variables no públicas usando estos métodos. Veámoslo:
tk = Person('TK', 'tk@mail.com')
print(tk.email()) # => tk@mail.com
# tk._email = 'new_tk@mail.com' -- treat as a non-public part of the class API
print(tk.email()) # => tk@mail.com
tk.update_email('new_tk@mail.com')
print(tk.email()) # => new_tk@mail.com
- Hemos inicializado un objeto con el valor TK para ‘first_name’ y tk@mail.com para ‘email’.
- Imprimimos el email accediendo a la variable no pública con un método.
- Intentamos establecer un nuevo email fuera de nuestra clase.
- Tenemos que tratar la variable no pública como una parte no pública del API.
- Actualizamos la variable no pública con nuestro método de instancia.
- ¡Conseguido! Podemos actualizarla dentro de nuestra clase con nuestro método.
Método público
Los métodos públicos pueden usarse también fuera de nuestra clase:
class Person:
def __init__(self, first_name, age):
self.first_name = first_name
self._age = age
def show_age(self):
return self._age
Probémoslo:
tk = Person('TK', 25)
print(tk.show_age()) # => 25
Perfecto, pueden usarse sin problema.
Método no público
Pero con los métodos no públicos esto no puede hacerse. Implementemos la misma clase ‘Person’, pero ahora con un método no público ‘show_age’ (mostrar edad) usando un subrayado (_).
class Person:
def __init__(self, first_name, age):
self.first_name = first_name
self._age = age
def _show_age(self):
return self._age
Y ahora intentamos llamar a este método no público con nuestro objeto:
tk = Person('TK', 25)
print(tk._show_age()) # => 25
Podemos acceder a él y actualizarlo. Los métodos no públicos son sólo una convención y deberían tratarse como parte no pública del API.
Aquí un ejemplo de cómo usarlo:
class Person:
def __init__(self, first_name, age):
self.first_name = first_name
self._age = age
def show_age(self):
return self._get_age()
def _get_age(self):
return self._age
tk = Person('TK', 25)
print(tk.show_age()) # => 25
Aquí tenemos un método no público ‘_get_age’ y un método público ‘show_age’. Este puede usarse directamente por nuestro objeto (fuera de la clase) y ‘_get_age’ usarse sólo dentro de la definición de nuestra clase (dentro del método ‘show_age’). Pero, una vez más, se trata tan sólo de una convención.
Resumen de encapsulado
Mediante el encapsulado nos aseguramos de que la representación interna del objeto está oculta desde fuera.
Herencia: comportamiento y características
Ciertos objetos tienen varias cosas en común: su comportamiento y características.
Por ejemplo, hemos heredado algunas características y comportamientos de nuestros padres. Podemos heredar sus ojos y pelo como características, y su impaciencia o timidez como características.
En la programación orientada a objetos, las clases pueden heredar características (datos) y comportamiento (métodos) de otra clase.
Veamos otro ejemplo y lo implementaremos en Python.
Imagina un coche. El número de ruedas, asientos y velocidad máxima son atributos de un coche. Podemos decir que la clase ElectricCar (coche eléctrico) hereda atributos de la clase Car (coche).
class Car:
def __init__(self, number_of_wheels, seating_capacity, maximum_velocity):
self.number_of_wheels = number_of_wheels
self.seating_capacity = seating_capacity
self.maximum_velocity = maximum_velocity
Nuestra clase Car implementada:
my_car = Car(4, 5, 250)
print(my_car.number_of_wheels)
print(my_car.seating_capacity)
print(my_car.maximum_velocity)
Una vez inicializado, podemos usar todas las variables de instancia creadas. Perfecto.
En Python aplicamos una clase padre a la clase hija como parámetro. Una clase ‘ElectricCar’ puede heredar de nuestra clase ‘Car’.
class ElectricCar(Car):
def __init__(self, number_of_wheels, seating_capacity, maximum_velocity):
Car.__init__(self, number_of_wheels, seating_capacity, maximum_velocity)
Tan sencillo como eso. No necesitamos implementar ningún otro método, porque esta clase ya los tiene (heredados de ‘Car’). Probémoslo:
my_electric_car = ElectricCar(4, 5, 250)
print(my_electric_car.number_of_wheels) # => 4
print(my_electric_car.seating_capacity) # => 5
print(my_electric_car.maximum_velocity) # => 250
Excelente.
¡Y eso es todo!
Hemos aprendido muchas cosas básicas en Python:
- Cómo funcionan las variables
- Cómo funcionan los condicionales
- Cómo funciona la iteración
- Como usar listas: Collection y Array
- Diccionarios clave-valor
- Como iterar sobre esas estructuras de datos
- Objetos y clases
- Atributos como datos de objetos
- Métodos como comportamiento de objetos
- Usar ‘getter’, ‘setter’ y decoradores
- Encapsulado: ocultando información
- Herencia: comportamiento y características
¡Felicidades! Has terminado este denso curso sobre Python,
Si quieres un curso completo de Python, aprender más sobre programación y construir proyectos, prueba
One Month Python Bootcamp (en inglés). ¡Nos vemos allí!
Este artículo se publicó originalmente en inglés en: https://medium.com/free-code-camp/learning-python-from-zero-to-hero-120ea540b567