Clases

Definir una clase

Para crear una clase de objeto que sea perro, asumimos que tiene elementos en común, como el nombre, la edad, y algunas funciones que hacen los perros1

doggy.py

#!/usr/bin/env python

class Dog:
    """A simple attempt to model a dog."""
    def __init__(self, name, age):
        """Initialize name and age attributes."""
        self.name = name
        self.age = age

    def sit(self):
        """Simulate a dog sitting in response to a command."""
        print(f"{self.name} is now sitting.")

    def roll_over(self):
        """Simulate rolling over in response to a command."""
        print(f"{self.name} rolled over!")

my_dog = Dog('Willy',6)
my_dog.roll_over()
my_dog.sit()
print(f"My dog's name is {my_dog.name}, and he is {my_dog.age} years old.")
$ ./doggy.py
# Willy rolled over!
# Willy is now sitting.
# My dog's name is Willy, and he is 6 years old.

Lo primero que se define es el método __init__(); este método se ejecuta por defecto cuando creamos una nueva instancia de una clase (por eso init). self no hace falta pasarlo como argumento (es el hecho de crear una instancia lo que pasa el argumento - el nombre de la nueva instancia. Los argumentos name y age debemos pasarlos al crear un nuevo objeto de la clase Dog).

Las funciones definidas al final son métodos, porque siempre hacen referencia al propio objeto de esa clase. Utilizan self.name porque es el atributo propio de esa instancia. Todas las referencias a las variables deben hacerse con self. dentro de la clase.

Dentro de la función __init__(), se pueden declarar otras variables por defecto.

class Car:
    def __init__(self, make, model, year):
        """Initialize attributes to describe a car."""
        self.make = make
        self.model = model
        self.year = year
        self.odometer_reading = 0

    def get_descriptive_name(self):
        """Return a neatly formatted descriptive name."""
        long_name = f"{self.year} {self.manufacturer} {self.model}"
        return long_name.title()

    def read_odometer(self):
        """Print a statement showing the car's mileage."""
        print(f"This car has {self.odometer_reading} miles on it.")

my_new_car = Car('audi', 'a4', 2019)
print(my_new_car.get_descriptive_name())
my_new_car.read_odometer()
$ ./car.py
# 2019 Audi A4
# This car has 0 miles on it.

Por ejemplo, en este caso self.odometer_reading = 0 es una variables por defecto de la clase. Si quisiéramos modificarla, habría que hacer:

>>> my_new_car.odometer_reading = 23
>>> my_new_car.read_odometer()
# This car has 23 miles on it.

También se puede construir un método dentro de la clase Car para actualizar el odómetro (en este caso, mileage no es self.mileage porque el valor viene de fuera de la clase):

#!/usr/bin/env python

class Car:
    def __init__(self, make, model, year):
        """Initialize attributes to describe a car."""
        self.make = make
        self.model = model
        self.year = year
        self.odometer_reading = 0

    def get_descriptive_name(self):
        """Return a neatly formatted descriptive name."""
        long_name = f"{self.year} {self.make} {self.model}"
        return long_name.title()

    def read_odometer(self):
        """Print a statement showing the car's mileage."""
        print(f"This car has {self.odometer_reading} miles on it.")

    def update_odometer(self, mileage):
        """Set the odometer to a given value"""
        if mileage >= self.odometer_reading:
            self.odometer_reading = mileage
        else:
            print("You can't roll back the odometer!")

    def increment_odometer(self, mile_increment):
        """Add miles to the odometer"""
        if mile_increment >= 0:
            self.odometer_reading += mile_increment
        else:
            print("You can't roll back the odometer!")


my_new_car = Car('audi', 'a4', 2019)
print(my_new_car.get_descriptive_name())
my_new_car.read_odometer()
my_new_car.update_odometer(23)
my_new_car.read_odometer()
my_new_car.update_odometer(10)
my_new_car.read_odometer()
my_new_car.increment_odometer(1000)
my_new_car.read_odometer()         
$ ./car.py
# 2019 Audi A4
# This car has 0 miles on it.
# This car has 23 miles on it.
# You can't roll back the odometer!
# This car has 23 miles on it.
# This car has 1023 miles on it.

Inheritance

Se puede crear un child class de otra clase que comparte ciertos elementos, sin que sea necesario definirlos de nuevo, utilizando super()

electricar.py

#!/usr/bin/env python

class Car:
---snip--- # La parent class tiene que estar declarada

class ElectricCar(Car):

    def __init__(self, make, model, year):
        """Initialize attributes of the parent class."""
        super().__init__(make, model, year)

my_tesla = ElectricCar('tesla', 'model s', 2019)
my_tesla.get_descriptive_name()
$ ./electricar.py
# 2019 Tesla Model S

El __init__() del child class (o subclass) utiliza la función super() para recoger __init__() de la clase superior (el parent o superclass) a través de super().__init__(make, model, year). En este no hace falta definir self. Con esto hereda los atributos de la clase superior.

Se puede configurar la subclase a demanda:

class ElectricCar(Car):

    def __init__(self, make, model, year):
        """Initialize attributes of the parent class."""
        super().__init__(make, model, year)
        self.battery_size = 75

    def describe_battery(self):
        "Print battery size"
        print(f"This car has a {self.battery_size}-KWh battery.")

Es posible eliminar variables existentes en la superclass si es necesario; por ejemplo, si en Car hubiese un tuviese un método para el tanque de gasolina (fill_gas_tank()) y quisiésemos bloquearlo en ElectricCar, añadiríamos:

class ElectricCar(Car):
--snip--
    def fill_gas_tank(self):
        """Electric cars don't have gas tanks."""
        print("This car doesn't need a gas tank!")

Se pueden utilizar las clases para dividir apartados de otra clase; por ejemplo, si quisiésemos definir las cosas relacionadas con la batería en una superclase específica, luego la podríamos integrar en ElectricCar:

class Car:
---snip---

class Battery:
    """Battery management for electric cars"""

    def __init__(self, battery_size=75):
        """Initialize battery attributes"""
        self.battery_size = battery_size

    def describe_battery(self):
        "Print battery size"
        print(f"This car has a {self.battery_size}-KWh battery.")

class ElectricCar(Car):
    """ Specifics for electric cars"""

    def __init__(self, make, model, year):
        """
        Initialize attributes of the parent class.
        Then initialize attributes specific to an electric car.
        """
        super().__init__(make, model, year)
        self.battery_size = Battery()

    def get_range(self):
        """Print a statement about the range this battery provides."""
        if self.battery_size == 75:
            range = 260
        elif self.battery_size == 100:
            range = 315
        print(f"This car can go about {range} miles on a full charge.")


my_tesla = ElectricCar('tesla', 'model s', '2019')
my_tesla.get_descriptive_name()
my_tesla.battery.describe_battery()

Aquí no hace falta especificar el valor de la batería porque tiene un valor por defecto de 75. Se crea una Battery instance dentro de ElectricCar porque está llamada en ElectricCar.__init__().

Para poder llamar los métodos incluidos en la instancia de Battery, hay que utilizarlo como un módulo:

my_tesla.battery.get.range ()

Importar clases

Las clases se pueden guardar dentro de módulos para ser importadas; se pueden enumerar las clases que contiene un módulo en su docstring

Si la clase Car está en car.py, importa utilizando from car import Car.

Ver módulos para ver cómo se hace referencia a módulos previos; pero básicamente es:

car
├── module
│   ├── car.py
│   └── __init__.py
└── my_car.py

my_car.py

#!/usr/bin/env python

import sys
sys.path.append('./module')
# Indica que el archivo que se ejecuta tiene módulos en './module', que contiene
# './module/__init__.py' y './module/car.py'.
from car import ElectricCar

my_tesla = ElectricCar('tesla', 'roadster', '2018')
print(my_tesla.get_descriptive_name())

Mientras las superclases estén sobre las subclases em el archivo, no hace falta importar todas las clases; basta con importar la necesaria, aunque tenga instancias incluidas o se defina desde otra clase.

También se puede importar el módulo entero, pero entonces hay que usar las clases así:

>>> import car
>>> my_car = car.Car('VW', 'beetle', '2010')

Esto también se puede hacer desde otros módulos; por ejemplo,

"""A set of classes for cars."""

from car import Car

class ElectricCar(Car):
---snip---

Más cosas de clases que aún no entiendo

Aquí hay una cosa sobre variables privadas… dentro de una clase… que no se propagan más allá de esa clase si definen con dos _ antes (__name)… pero de momento es muy avanzado. https://docs.python.org/3.9/tutorial/classes.html#private-variables

class Employee:
    pass

john = Employee()  # Create an empty employee record

# Fill the fields of the record
john.name = 'John Doe'
john.dept = 'computer lab'
john.salary = 1000

Referencias

1.
Matthes, E. (2019). Python Crash Course: A Hands-on, Project-Based Introduction to Programming 2nd edition. (No Starch Press).