Encapsulation in POO

Hi everyone I hope this message finds you well. In fact I want to talk about a subject that is focused on encapsulation in python with access levels as in other languages ​​like C++, Java, and C# …, in fact there is a principle on encapsulation in python that is secured by convention but not by use. Lately I have been working in fact I was just having fun writing python code in OOP on the encapsulation part with the basic attributes at the level of the nomenclature, the _ refers to a protected attribute or method __ private attribute or method according to the convention I must not access the attributes __ and _ outside the class the _ we do it but it is not recommended. But afterwards I see that even with the __ I can have access outside the class I just have to go through the class itself in order to have access to this attribute and so I told myself that it is not secure. So I thought of making a more elegant and organized way which was to make a restriction on the access of these attributes outside the class; the convention is one thing but to the extent that an unconscious dev takes your code he can do without the mangling system so I made another combination of code using existing python code in order to bring a more voluminous encapsulation layer. Not only based on the convention, but that we block access to the resource when its access is not authorized and I produced this code. Now I want to ask you what do you think of this code? start of the code:
#Started since 02-10-2024
import datetime as date

class Vehicule:
“”"
Classe pour tester les attributs privés, les conventions Python
et l’usage de @property pour contrôler l’accès.
“”"
def init(self, mark, speed, engin, people_name, year_made=None):
self.__mark = mark
self.__speed = speed
self.__engin = engin
self.__year_made = year_made if year_made is not None else self.__generate_year_made()
self._people_name = people_name

# generation de la date pour chaque instance
def __generate_year_made(self):
    create_at = date.datetime.now()
    return create_at.strftime("%Y-%m-%d")
""" Ma combinaison magique"""
@property
def engin(self):
    return self.__engin

@engin.setter
def engin(self, new_engin_by_user):
    if not isinstance(new_engin_by_user, str):
        raise ValueError("Vous devez saisir une chaîne de caractères pour l'engin.")
    self.__engin = new_engin_by_user

@property
def mark(self):
    return self.__mark

@mark.setter
def mark(self, new_mark_user):
    if not isinstance(new_mark_user, str):
        raise ValueError("Vous devez saisir une chaîne de caractères pour le mark.")
    self.__mark = new_mark_user

@property
def speed(self):
    return self.__speed

@speed.setter
def speed(self, new_speed):
    if not isinstance(new_speed, int):
        raise ValueError("Vous devez saisir un entier pour la vitesse.")
    if new_speed <= 0 or new_speed > 300:
        raise ValueError("La vitesse doit être comprise entre 1 et 300.")
    self.__speed = new_speed

@property
def year_made(self):
    return self.__year_made

@year_made.setter
def year_made(self, _):
    raise AttributeError("La modification de l'année de création est interdite !")

@property
def people_name(self):
    return self._people_name

@people_name.setter
def people_name(self, new_people_name_user):
    if not isinstance(new_people_name_user, str):
        raise ValueError("Le format du nom est invalide, veuillez saisir un nom valide.")
    self._people_name = new_people_name_user

main prog

if name == ‘main’:
vehicule1 = Vehicule(‘RANGE ROVER’, 250, ‘Range VVS 456’, ‘Fodé Kerfala Camara’)
print(“Nom du propriétaire:”, vehicule1.people_name)
print(“Marque:”, vehicule1.mark)
print(“Vitesse:”, vehicule1.speed)
print(“Année de création:”, vehicule1.year_made)

#End 08-11-2024

Python does not have encapsulation. The single underscore (_) by convention means something is an implementation detail/considered private, but Python will not prevent you from accessing it. Two underscores (__) in front of a name lead to name mangling — Python will rename the symbol to include the name of the class, but again, it’s trivial to access it. Even if you try to prevent access with some magic method overrides, there are still ways to work around it.

Encapsulation as defined by Java et al. does not really bring meaningful benefits. It mainly makes it harder to shoot yourself in the foot. Java et al. will stop you from calling a private method/accessing a private field the normal way (foo.bar()), but you can use reflection to do it anyway.

Another oft-cited argument for getters and setters is the ability to change the implementation details while leaving the interface the same — but this tends to work only in very simple cases detached from real life. You may start your Person class with a name property, and then want to split it into first_name and last_name. You could try to define a getter for name that returns first_name + " " + last_name, but this does not work for all cultures. A setter for name that splits by space will explode for many people. And for people with two exactly names, they may provide their last name first. Same goes for migrating from age to birthdate: the getter will work, the setter is not possible.

Having two ways to do something tends to lead to subtle bugs. For example, your setters validate that most properties are strings, and that the speed is between 1 and 300. But your constructor does not use the custom setters, so I can end up with a negative-speed car whose mark is -1 and engine is -2. Your class will allow this:

negative_car = Vehicule(mark = -1, speed = -9999, engin = -2, people_name = -3)

In Pythonic code, name mangling with __ and @property would be used fairly rarely. Your class would be more Pythonic if it just had the properties directly available — without any @property or __ in use. The __generate_year_made method could be named _generate_year_made. (Also, the name of the property does not match what it contains.)

You could also simplify it even more by using something like dataclasses.
If you want data validation, I would recommend adopting an existing library, like Pydantic.

1 Like

Hello can help me to understand this part : Having two ways to do something tends to lead to subtle bugs. For example, your setters validate that most properties are strings, and that the speed is between 1 and 300. But your constructor does not use the custom setters, so I can end up with a negative-speed car whose mark is -1 and engine is -2. Your class will allow this:

negative_car = Vehicule(mark = -1, speed = -9999, engin = -2, people_name = -3)
``` how i gave here à negative speed ?

You can test this code snippet. Your constructor is setting self.__speed = speed, which means it does not run the validation in speed.setter.