Aprende leyendo en orden

Variables privadas y encapsulación — Acceso seguro vía getter / setter

Aprende las variables privadas y la encapsulación en Python. Convención _x vs name mangling __x, get_xxx / set_xxx para acceso seguro, y el estilo Pythonic @property / @xxx.setter — práctica incluida.

La última vez cubrimos los dos primeros pilares de la POO — herencia y polimorfismo. Este artículo cierra el tercero: la encapsulación.

Variables privadas — Python no tiene privacidad real

Java y C++ tienen una palabra clave private que bloquea el acceso desde fuera en cuanto la declaras. Python no tiene privacidad reforzada por el lenguaje. En su lugar, el número de guiones bajos al inicio señala «esto es de uso interno» o «no toques esto directamente» — una convención entre programadores, no una regla dura.

0 / 1 / 2 guiones bajos comunican intención
name(ninguno)atributopúblicouso libredesde fuera_name(1)convención:privadono tocar__name(2)
El número de guiones bajos no cambia la aplicación; es solo una etiqueta que señala intención.

Guion bajo simple _x — privacidad solo por convención

Añadir un solo _ delante de un nombre le dice a la comunidad de Python «este atributo es para uso interno de la clase — no accedas a él directamente desde fuera». El parámetro de __init__ sigue usando el nombre normal; solo el campo self. recibe el _ inicial.

class UserAccount:
    def __init__(self, owner_name, balance):
        self._owner_name = owner_name      # interno -> prefijo _
        self._balance    = balance

    def get_info(self):                     # accesor hacia el exterior
        return {"owner": self._owner_name, "balance": self._balance}


user = UserAccount("Ana", 50000)
print(user._balance)        # 50000  <- funciona pero no se recomienda
print(user.get_info())      # {'owner': 'Ana', 'balance': 50000}  <- recomendado

Usa una clase BlogPost para sentir qué significa _ (este es un escenario distinto al ejemplo de arriba — misma forma, otros campos).

① Define class BlogPost: y asigna self._title y self._views en __init__(self, title, views).

② Define summary(self) para return {"title": self._title, "views": self._views}.

③ Crea post = BlogPost("Empezando con Python", 100). Primero, accede directamente con post._views y haz print (funciona pero es un patrón prohibido).

④ Luego usa el patrón recomendadoprint(post.summary()).

(Si el código se ejecuta correctamente, aparecerá la explicación.)

Editor Python

Ejecutar el código para ver el resultado

Guion bajo doble __x — name mangling

Añadir dos _ iniciales hace que Python reescriba el propio nombre del atributo. Si escribes self.__pin = 1234 dentro de una clase Account, el nombre realmente almacenado se vuelve _Account__pin. Esto se llama name manglingobj.__pin desde fuera no encuentra nada, así que el acceso se vuelve mucho más difícil en la práctica.

__pin se reescribe internamente como _Account__pin
self.__pin= 1234Python reescribeel nombre_Account__pin= 1234obj.__pin-> Errorobj._Account__pin-> 1234almacena
Los atributos con doble guion bajo se almacenan bajo «guion bajo + nombre de clase + nombre original». Leer obj.__pin directamente falla con AttributeError porque esa clave no existe.
class Account:
    def __init__(self, owner, pin):
        self._owner = owner       # privacidad solo por convención
        self.__pin  = pin         # con name mangling (se vuelve _Account__pin)

acc = Account("Ana", 1234)

print(acc._owner)              # Ana              <- funciona normal
# print(acc.__pin)             # AttributeError <- no es visible directo
print(acc._Account__pin)       # 1234            <- el nombre mangled lo alcanza

Usa una clase LoginForm para verificar que __password realmente se reescribe (escenario distinto al ejemplo de Account, mismo comportamiento).

① Define class LoginForm: y en __init__(self, username, password) asigna self._username = username y self.__password = password.

② Crea form = LoginForm("ana", "p@ssw0rd"). Usa print(form._username) para confirmar que el lado con un solo guion bajo se lee normalmente.

③ Usa print(form._LoginForm__password) para recuperar la contraseña vía su nombre mangled.

④ Ejecuta print([n for n in dir(form) if not n.startswith('__')]) para volcar los nombres de atributo realmente almacenados en la instancia y ver _LoginForm__password listado.

Editor Python

Ejecutar el código para ver el resultado

«__» tampoco es un muro absoluto

El doble guion bajo evita el acceso directo con obj.__pin, lo cual es una protección un nivel más fuerte. Pero cualquiera que sepa el nombre mangled obj._Account__pin aún puede alcanzarlo. No es privacidad real. En proyectos reales, el guion bajo simple _x es mucho más común salvo que haya una razón específica para usar el mangling.

Encapsulación — restringir el acceso a métodos dedicados

La encapsulación es la idea de diseño «agrupa los atributos de datos y los métodos que operan sobre ellos en una sola clase, y obliga al código exterior a pasar por un pequeño conjunto de puntos de entrada publicados». Entonces, ¿cómo construimos esos puntos de entrada?

Canalizar lecturas/escrituras a través de un solo método
consistenciarotaescritura directa_price = -100❌ fueraproduct._price= -100consistenciapreservadaset_price()valida✅ fueraproduct.set_price(100)tal cualsolo si OK
Las escrituras directas dejan que cualquier valor caiga en _price sin verificar. Pasar por un método significa que el setter valida tipo y rango en un solo lugar.

El estilo más básico es escribir get_xxx / set_xxx a mano. Dentro del setter, haz una comprobación con isinstance para el tipo y una comprobación de rango, y raise ValueError(...) si algo no encaja. Con eso, ningún valor basura llega nunca a _price.

class Product:
    def __init__(self, name, price, stock):
        self._name  = name
        self._price = price
        self._stock = stock

    def get_price(self):
        return self._price

    def set_price(self, price):
        if isinstance(price, int) and price >= 0:
            self._price = price
        else:
            raise ValueError("price must be a non-negative integer")


product = Product("Camiseta", 1500, 30)
print(product.get_price())     # 1500
product.set_price(2000)
print(product.get_price())     # 2000
# product.set_price(-100)      # ValueError

Añade un getter y setter solo de edad a una clase de formulario de registro UserProfile. La edad no puede ser negativa ni 200+, así que el setter debe proteger el rango.

① Define class UserProfile: y en __init__(self, name, age) asigna _name y _age.

② Define get_age(self) y return self._age.

③ Define set_age(self, age). Solo cuando age sea int y 0 <= age <= 150 debe ejecutar self._age = age; de lo contrario raise ValueError("age must be an integer in [0, 150]").

④ Crea user = UserProfile("Ana", 30), haz print de user.get_age(), luego user.set_age(31) y vuelve a hacer print del resultado.

⑤ Confirma la ruta de rechazo: envuelve user.set_age(-5) en try / except ValueError as e: y print("NG:", e).

Editor Python

Ejecutar el código para ver el resultado

@property y @xxx.setter

El estilo get_price() / set_price(...) es claro, pero los sitios de llamada acaban viéndose muy «llamada de método» — no es lo más limpio. La idiomática más pulida de Python usa dos decoradores: @property y @xxx.setter.

Con ellos, el sitio de llamada se queda como product.price / product.price = 2000acceso de atributo plano — pero por debajo, se llaman los métodos getter y setter. Es una estructura de dos capas donde la sintaxis se mantiene simple pero la lógica sigue corriendo.

product.price se ve igual; los métodos corren por debajo
product.price@propertydef pricereturnself._priceproduct.price= 2000@price.setterdef pricevalida yactualiza self._priceleeescribe
La sintaxis de acceso de atributo se queda igual; @property redirige las lecturas y @price.setter redirige las escrituras a llamadas de método. La validación vive dentro del setter.
class Product:
    def __init__(self, name, price):
        self._name  = name
        self._price = price

    @property
    def price(self):                 # getter
        return self._price

    @price.setter
    def price(self, value):          # setter — el nombre debe coincidir con el getter
        if not isinstance(value, int) or value < 0:
            raise ValueError("price must be a non-negative integer")
        self._price = value

    @property
    def label(self):                 # propiedad calculada — valor derivado
        return f"{self._name} ({self._price})"


product = Product("Camiseta", 1500)
print(product.price)         # 1500              <- se invoca @property
product.price = 2000         # <- se invoca @price.setter
print(product.price)         # 2000
print(product.label)         # Camiseta (2000)   <- propiedad calculada

Mantén el nombre del setter idéntico al del getter

El price en @price.setter debe coincidir con el nombre del método del @property def price previo. Python interpreta el decorador como «adjunta una versión de escritura al mismo objeto price que ya tiene una versión de lectura» — si el nombre cambia, se tratan como cosas distintas.

Reescribe el mismo UserProfile de la Práctica 3 para usar @property / @age.setter, y añade una propiedad calculada age_group (under 18 / adult / senior).

① Define class UserProfile: con __init__(self, name, age) asignando _name / _age.

② Usa @property def age(self): para return self._age.

③ Usa @age.setter def age(self, value):; solo cuando isinstance(value, int) and 0 <= value <= 150 asigna self._age = value, si no raise ValueError("age must be an integer in [0, 150]").

④ Usa @property def age_group(self): para devolver "under 18" si self._age < 18, "adult" si self._age < 65, y si no "senior" (sin setter — solo lectura).

⑤ Crea user = UserProfile("Ana", 30), haz print de user.age y user.age_group, luego user.age = 70 y print de age_group una vez más.

Editor Python

Ejecutar el código para ver el resultado

Los tres pilares de la POO

Lo que sostiene la encapsulación
Encapsulación
  • Protección de datos — separa el estado interno de la API pública con _x
  • Consistencia — concentra la validación en los setters, en un solo lugar
  • Libertad de implementación — cambia los internos sin cambiar la API pública
  • Estilo Pythonic — convención más @property, no aplicación del lenguaje
Herencia
  • Reutiliza la maquinaria del padre
Polimorfismo
  • Mismo nombre de método, comportamiento distinto por tipo
Encapsulación
  • Canaliza el acceso desde fuera por unas pocas puertas
Herencia, polimorfismo y encapsulación son los tres pilares de la POO. La herencia reutiliza, el polimorfismo unifica los sitios de llamada, y la encapsulación hace que todo sea difícil de romper — asignar cada papel conscientemente mantiene el diseño de las clases limpio.
QUIZ

Verificación de conocimientos

Responde cada pregunta una a una.

Pregunta 1¿Cuál es la afirmación más precisa sobre las variables privadas de Python?

Pregunta 2¿Cuál es el mayor beneficio de usar @property y @xxx.setter?

Pregunta 3Dentro de class Account: escribiste self.__pin = 1234. Desde fuera, acc.__pin lanza AttributeError. ¿Qué nombre se almacena realmente en la instancia?