clases en Python guía completa init encapsulamiento property

Clases en Python — objetos, encapsulamiento y métodos mágicos desde cero

Las clases en Python son el mecanismo central de la Programación Orientada a Objetos (POO), el paradigma que domina FP2. Una clase es un molde para crear objetos que agrupan datos y comportamiento en una sola unidad.

En este artículo vemos cómo definir clases, inicializarlas con __init__, proteger sus datos con encapsulamiento y dar representaciones útiles con los métodos mágicos __str__ y __repr__.

¿Qué son las clases en Python y por qué usarlas?

En FP1 organizabas el código con funciones. En FP2 das un paso más, agrupas datos relacionados y las funciones que los manipulan en una clase. Un objeto es una instancia concreta de esa clase.

# Sin clases — datos sueltos
nombre = 'Sergio'
edad = 19
nota = 7.5

def es_aprobado(nota):
    return nota >= 5.0

# Con clases — datos y comportamiento juntos
class Estudiante:
    def __init__(self, nombre, edad, nota):
        self.nombre = nombre
        self.edad = edad
        self.nota = nota

    def es_aprobado(self):
        return self.nota >= 5.0

sergio = Estudiante('Sergio', 19, 7.5)
print(sergio.es_aprobado())    # → True

La ventaja no es solo organizativa, cuando tienes 50 estudiantes, cada uno tiene sus propios datos independientes sin que interfieran entre sí.

Definir una clase

class NombreClase:
    """Docstring opcional que documenta la clase"""
    pass

La palabra reservada class seguida del nombre, por convención en PascalCase (primera letra de cada palabra en mayúscula). El pass crea una clase vacía.

Para crear un objeto (instanciar la clase):

mi_objeto = NombreClase()

El método __init__ — el constructor

__init__ es el método que se ejecuta automáticamente cada vez que se crea un objeto. Se usa para inicializar los atributos del objeto:

class Rectangulo:
    def __init__(self, base, altura):
        self.base = base
        self.altura = altura

r1 = Rectangulo(10, 5)
r2 = Rectangulo(3, 8)

print(r1.base)      # → 10
print(r2.altura)    # → 8

__init__ no devuelve nada, su único trabajo es inicializar el objeto.

Parámetro vs atributo — la confusión más común

En __init__ hay una distinción clave que confunde mucho al principio:

class Rectangulo:
    def __init__(self, base, altura):
        self.base = base
        self.altura = altura

base y altura — son parámetros del método __init__. Son variables locales que solo existen mientras se ejecuta __init__. Desaparecen cuando el método termina.

self.base y self.altura son atributos del objeto. Pertenecen al objeto self y existen mientras el objeto exista. Se pueden usar en cualquier método de la clase.

La línea self.base = base hace dos cosas a la vez:

self.base = base
# ↑           ↑
# crea el     valor inicial que
# atributo    viene del parámetro

No tienen que llamarse igual, aunque por claridad normalmente sí:

class Rectangulo:
    def __init__(self, b, h):       # parámetros: b y h
        self.base = b               # atributo: self.base
        self.altura = h             # atributo: self.altura

Y puedes tener atributos que no vienen de parámetros, se calculan o se inicializan con un valor fijo:

class Rectangulo:
    def __init__(self, base, altura):
        self.base = base            # viene del parámetro
        self.altura = altura        # viene del parámetro
        self.color = 'blanco'       # valor fijo, no es parámetro
        self.area_calculada = base * altura    # calculado

Self — el objeto sobre el que actúa el método

self es el primer parámetro de cualquier método de instancia, representa el objeto concreto sobre el que se ejecuta el método. Python lo pasa automáticamente, tú no lo incluyes al llamar al método:

class Rectangulo:
    def __init__(self, base, altura):
        self.base = base        # self = el objeto recién creado
        self.altura = altura

    def area(self):
        return self.base * self.altura    # self = el objeto que llama

r1 = Rectangulo(10, 5)
print(r1.area())    # Python pasa r1 como self automáticamente → 50

El nombre self es una convención, Python acepta cualquier nombre, pero usar otro confunde a cualquiera que lea tu código.

Métodos de instancia

Los métodos son funciones definidas dentro de la clase que actúan sobre el objeto:

class Rectangulo:
    def __init__(self, base, altura):
        self.base = base
        self.altura = altura

    def area(self):
        return self.base * self.altura

    def perimetro(self):
        return 2 * (self.base + self.altura)

    def es_cuadrado(self):
        return self.base == self.altura

    def rotar(self):
        self.base, self.altura = self.altura, self.base

r = Rectangulo(10, 5)
print(r.area())          # → 50
print(r.perimetro())     # → 30
print(r.es_cuadrado())   # → False
r.rotar()
print(r.base, r.altura)  # → 5 10

Un método puede llamar a otro método del mismo objeto usando self:

def describir(self):
    estado = 'cuadrado' if self.es_cuadrado() else 'rectángulo'
    return f'{estado} de área {self.area()}'

Atributos de clase vs atributos de instancia

Hasta ahora todos los atributos eran de instancia, cada objeto tiene los suyos. Los atributos de clase son compartidos por todos los objetos:

class Estudiante:
    universidad = 'ULPGC'    # atributo de clase, compartido
    contador = 0

    def __init__(self, nombre, nota):
        self.nombre = nombre    # atributo de instancia — propio
        self.nota = nota
        Estudiante.contador += 1

e1 = Estudiante('Sergio', 7.5)
e2 = Estudiante('María', 8.0)

print(e1.universidad)       # → ULPGC
print(e2.universidad)       # → ULPGC
print(Estudiante.contador)  # → 2

Encapsulamiento — proteger los datos

El encapsulamiento es el principio de ocultar los datos internos de un objeto y controlar cómo se accede y modifica. En Python los atributos que empiezan con __ (doble guion bajo) se ocultan:

class Natural:
    def __init__(self, numero):
        self.__valor = numero    # atributo privado

n = Natural(10)
print(n.__valor)    # AttributeError — no accesible desde fuera

Python implementa esto con name mangling, internamente renombra __valor a _Natural__valor. No es privacidad real, pero es la convención acordada.

Un solo guion bajo _valor significa «trata esto como privado por convención» pero no impide el acceso técnico.

Getters y setters

Cuando ocultas un atributo necesitas métodos para acceder y modificarlo de forma controlada:

class Natural:
    def __init__(self, numero):
        self.__valor = numero

    def get_valor(self):
        return self.__valor

    def set_valor(self, nuevo_valor):
        if nuevo_valor >= 0:
            self.__valor = nuevo_valor
        else:
            self.__valor = 0    # no permite negativos

n = Natural(10)
print(n.get_valor())    # → 10
n.set_valor(-5)
print(n.get_valor())    # → 0

La ventaja del setter es que controla qué valores son válidos, el objeto nunca puede entrar en un estado incorrecto.

El decorador @property — la forma pythónica

Los getters y setters funcionan pero la sintaxis n.get_valor() es verbosa. El decorador @property permite usarlos como si fueran atributos normales:

class Natural:
    def __init__(self, numero):
        self.valor = numero    # llama al setter

    @property
    def valor(self):
        return self.__valor

    @valor.setter
    def valor(self, nuevo_valor):
        self.__valor = nuevo_valor if nuevo_valor >= 0 else 0

n = Natural(10)
print(n.valor)     # llama al getter → 10
n.valor = -5       # llama al setter
print(n.valor)     # → 0

Con @property el código que usa la clase es más limpio, n.valor en vez de n.get_valor(). El control interno es el mismo.

Si defines solo @property sin el setter tienes una propiedad de solo lectura:

@property
def area(self):
    return self.base * self.altura    # no tiene setter — no se puede cambiar

Métodos de clase — @classmethod

Hasta ahora todos los métodos recibían self como primer parámetro, están vinculados a un objeto concreto. Los métodos de clase se vinculan a la propia clase, no a ningún objeto. Se declaran con el decorador @classmethod y su primer parámetro es cls (la clase):

class Estudiante:
    _contador = 0    # atributo de clase

    def __init__(self, nombre, nota):
        self.nombre = nombre
        self.nota = nota
        Estudiante._contador += 1

    @classmethod
    def total_estudiantes(cls):
        return cls._contador    # accede al atributo de clase

e1 = Estudiante('Sergio', 7.5)
e2 = Estudiante('Sofía', 8.0)

print(Estudiante.total_estudiantes())    # → 2
print(e1.total_estudiantes())            # → 2 (también funciona desde objeto)

El uso principal de @classmethod es manejar atributos de clase, como el contador del ejemplo. También se usa para crear «métodos factoría», formas alternativas de crear objetos:

class Angulo:
    def __init__(self, grados):
        self.grados = grados

    @classmethod
    def desde_radianes(cls, radianes):
        import math
        return cls(radianes * 180 / math.pi)    # crea un objeto Angulo

a1 = Angulo(90)                          # desde grados
a2 = Angulo.desde_radianes(3.14159 / 2)  # desde radianes
print(a2.grados)    # → 90.0

La diferencia clave con los métodos normales:

# Método de instancia: necesita un objeto
def metodo(self):
    return self.atributo    # accede a datos del objeto

# Método de clase: no necesita objeto
@classmethod
def metodo_clase(cls):
    return cls.atributo_clase    # solo accede a datos de clase

Métodos mágicos — __str__ y __repr__

Los métodos mágicos son métodos especiales que Python llama automáticamente en situaciones concretas. Sus nombres empiezan y terminan con __.

__str__ — representación informal del objeto. Se llama cuando usas print() o str():

class Racional:
    def __init__(self, num, den):
        self.num = num
        self.den = den

    def __str__(self):
        return f'{self.num}/{self.den}'

r = Racional(3, 4)
print(r)        # → 3/4
print(str(r))   # → 3/4

__repr__ — representación formal del objeto. Debe ser una expresión válida para recrear el objeto. Se llama en el intérprete interactivo y en la depuración:

class Racional:
    def __init__(self, num, den):
        self.num = num
        self.den = den

    def __repr__(self):
        return f'Racional({self.num}, {self.den})'

    def __str__(self):
        return f'{self.num}/{self.den}'

r = Racional(3, 4)
print(repr(r))    # → Racional(3, 4)
print(r)          # → 3/4

La diferencia: __str__ es para el usuario, __repr__ es para el programador. Si solo defines uno, define __repr__, Python lo usa como fallback para __str__ cuando este no está definido.

Sin ninguno de los dos:

r = Racional(3, 4)
print(r)    # → <__main__.Racional object at 0x7f1234567890>

Un ejemplo completo — clase Estudiante

Aquí tienes una clase completa que usa todo lo que hemos visto:

class Estudiante:
    universidad = 'ULPGC'
    _contador = 0

    def __init__(self, nombre, edad, nota):
        self.nombre = nombre
        self.edad = edad
        self.nota = nota    # usa el setter
        Estudiante._contador += 1

    @property
    def nota(self):
        return self.__nota

    @nota.setter
    def nota(self, valor):
        if not (0 <= valor <= 10):
            raise ValueError(f'Nota fuera de rango: {valor}')
        self.__nota = valor

    @property
    def aprobado(self):
        return self.__nota >= 5.0

    def clasificacion(self):
        if self.__nota >= 9: return 'Sobresaliente'
        elif self.__nota >= 7: return 'Notable'
        elif self.__nota >= 5: return 'Aprobado'
        else: return 'Suspenso'

    @classmethod
    def total_estudiantes(cls):
        return cls._contador

    def __str__(self):
        estado = '✓' if self.aprobado else '✗'
        return f'{self.nombre} ({self.edad} años) — {self.__nota:.1f} {estado}'

    def __repr__(self):
        return f'Estudiante("{self.nombre}", {self.edad}, {self.__nota})'

# Uso
e1 = Estudiante('Sergio', 22, 7.5)
e2 = Estudiante('María', 20, 4.8)

print(e1)                           # → Sergio (22 años) — 7.5 ✓
print(e2)                           # → María (20 años) — 4.8 ✗
print(repr(e1))                     # → Estudiante("Sergio", 22, 7.5)
print(e1.clasificacion())           # → Notable
print(Estudiante.total_estudiantes())  # → 2

try:
    e3 = Estudiante('Juan', 21, 11)
except ValueError as err:
    print(err)    # → Nota fuera de rango: 11

Visualízalo con Python Tutor

Copia este código en pythontutor.com:

class Rectangulo:
    def __init__(self, base, altura):
        self.base = base
        self.altura = altura

    def area(self):
        return self.base * self.altura

r1 = Rectangulo(10, 5)
r2 = Rectangulo(3, 8)
print(r1.area())
print(r2.area())

Observa cómo cada objeto tiene sus propios atributos base y altura, independientes entre sí, y cómo self apunta al objeto correcto en cada llamada a area().

Resumen rápido

# DEFINIR UNA CLASE
class MiClase:
    atributo_clase = 0    # compartido por todos los objetos

    def __init__(self, param):
        self.atributo = param    # propio de cada objeto

    def metodo(self):
        return self.atributo

# INSTANCIAR
obj = MiClase(valor)

# ENCAPSULAMIENTO
class MiClase:
    def __init__(self, valor):
        self.valor = valor    # usa el setter

    @property
    def valor(self):
        return self.__valor

    @valor.setter
    def valor(self, nuevo):
        if nuevo >= 0:
            self.__valor = nuevo

# MÉTODOS MÁGICOS
def __str__(self):     # para print() — legible por el usuario
    return f'...'

def __repr__(self):    # para repr() — para el programador
    return f'MiClase({self.atributo})'

# ATRIBUTOS PRIVADOS
self.__privado    # oculto — name mangling
self._convención  # convención privado — accesible pero no recomendado

# MÉTODOS DE CLASE
@classmethod
def metodo_clase(cls):
    return cls.atributo_clase

En el próximo artículo vemos herencia y polimorfismo, cómo crear clases que heredan de otras y redefinen su comportamiento.


Classes in Python — objects, encapsulation and magic methods from scratch

What is a class and why use it?

# Without classes
name = 'Sergio'
grade = 7.5

# With classes — data and behaviour together
class Student:
    def __init__(self, name, grade):
        self.name = name
        self.grade = grade

    def passed(self):
        return self.grade >= 5.0

sergio = Student('Sergio', 7.5)
print(sergio.passed())    # → True

Defining a class

class ClassName:
    """Optional docstring"""
    pass

my_object = ClassName()

__init__ — the constructor

class Rectangle:
    def __init__(self, base, height):
        self.base = base
        self.height = height

r1 = Rectangle(10, 5)
r2 = Rectangle(3, 8)
print(r1.base)      # → 10

Parameter vs attribute — the most common confusion

class Rectangle:
    def __init__(self, base, height):
        self.base = base
        self.height = height

base and heightparameters of __init__. Local variables that only exist while __init__ runs. Gone when the method ends.

self.base and self.heightattributes of the object. Belong to self and exist as long as the object exists. Accessible from any method in the class.

self.base = base does two things at once:

self.base = base
# ↑           ↑
# creates     initial value from
# attribute   the parameter

They don’t have to share the same name, though they usually do for clarity:

class Rectangle:
    def __init__(self, b, h):    # parameters: b and h
        self.base = b            # attribute: self.base
        self.height = h          # attribute: self.height

Attributes don’t have to come from parameters:

class Rectangle:
    def __init__(self, base, height):
        self.base = base          # from parameter
        self.height = height      # from parameter
        self.color = 'white'      # fixed value, not a parameter
        self.calculated = base * height    # calculated

Self

self is the first parameter of any instance method, represents the specific object the method acts on. Python passes it automatically:

class Rectangle:
    def __init__(self, base, height):
        self.base = base
        self.height = height

    def area(self):
        return self.base * self.height

r = Rectangle(10, 5)
print(r.area())    # Python passes r as self → 50

Class vs instance attributes

class Student:
    university = 'ULPGC'    # class attribute — shared
    count = 0

    def __init__(self, name, grade):
        self.name = name    # instance attribute — own
        self.grade = grade
        Student.count += 1

Encapsulation

Attributes starting with __ are hidden:

class Natural:
    def __init__(self, number):
        self.__value = number    # private

n = Natural(10)
print(n.__value)    # AttributeError

Getters and setters

class Natural:
    def __init__(self, number):
        self.__value = number

    def get_value(self):
        return self.__value

    def set_value(self, new_value):
        self.__value = new_value if new_value >= 0 else 0

@property — the pythonic way

class Natural:
    def __init__(self, number):
        self.value = number

    @property
    def value(self):
        return self.__value

    @value.setter
    def value(self, new_value):
        self.__value = new_value if new_value >= 0 else 0

n = Natural(10)
print(n.value)    # getter → 10
n.value = -5      # setter
print(n.value)    # → 0

Class methods — @classmethod

All methods so far received self, bound to a specific object. Class methods are bound to the class itself, not any object. Declared with @classmethod, first parameter is cls:

class Student:
    _count = 0

    def __init__(self, name, grade):
        self.name = name
        self.grade = grade
        Student._count += 1

    @classmethod
    def total_students(cls):
        return cls._count

e1 = Student('Sergio', 7.5)
e2 = Student('María', 8.0)
print(Student.total_students())    # → 2

Also used as factory methods, alternative ways to create objects:

class Angle:
    def __init__(self, degrees):
        self.degrees = degrees

    @classmethod
    def from_radians(cls, radians):
        import math
        return cls(radians * 180 / math.pi)

a = Angle.from_radians(3.14159 / 2)
print(a.degrees)    # → 90.0

Key difference:

def method(self):           # instance method — needs object
    return self.attribute

@classmethod
def class_method(cls):      # class method — no object needed
    return cls.class_attr

Magic methods — __str__ and __repr__

class Rational:
    def __init__(self, num, den):
        self.num = num
        self.den = den

    def __str__(self):
        return f'{self.num}/{self.den}'    # for print() — user-facing

    def __repr__(self):
        return f'Rational({self.num}, {self.den})'    # for repr() — programmer-facing

r = Rational(3, 4)
print(r)        # → 3/4
print(repr(r))  # → Rational(3, 4)

Without either: <__main__.Rational object at 0x...>

Complete example

class Student:
    university = 'ULPGC'
    _count = 0

    def __init__(self, name, age, grade):
        self.name = name
        self.age = age
        self.grade = grade
        Student._count += 1

    @property
    def grade(self):
        return self.__grade

    @grade.setter
    def grade(self, value):
        if not (0 <= value <= 10):
            raise ValueError(f'Grade out of range: {value}')
        self.__grade = value

    @property
    def passed(self):
        return self.__grade >= 5.0

    def classification(self):
        if self.__grade >= 9:   return 'Outstanding'
        elif self.__grade >= 7: return 'Good'
        elif self.__grade >= 5: return 'Passed'
        else:                   return 'Failed'

    @classmethod
    def total_students(cls):
        return cls._count

    def __str__(self):
        status = '✓' if self.passed else '✗'
        return f'{self.name} ({self.age}) — {self.__grade:.1f} {status}'

    def __repr__(self):
        return f'Student("{self.name}", {self.age}, {self.__grade})'

Quick summary

# DEFINE
class MyClass:
    class_attr = 0

    def __init__(self, param):
        self.attr = param

    def method(self):
        return self.attr

# INSTANTIATE
obj = MyClass(value)

# ENCAPSULATION WITH @property
@property
def value(self):
    return self.__value

@value.setter
def value(self, new):
    if new >= 0: self.__value = new

# MAGIC METHODS
def __str__(self):   return f'...'     # print() — user-facing
def __repr__(self):  return f'...'     # repr() — programmer-facing

# PRIVATE
self.__private    # hidden — name mangling
self._convention  # convention only

# CLASS METHOD
@classmethod
def class_method(cls): ...

Publicaciones Similares

4 comentarios

Deja una respuesta

Tu dirección de correo electrónico no será publicada. Los campos obligatorios están marcados con *