
Encapsulation is the most core characteristic among the three major features of object-oriented programming.
Encapsulation is the “integration” mentioned above (integration is an informal term, but it is relatively more vivid).
The definition of encapsulation: hiding the attributes and implementation details of an object, only exposing interfaces to the outside, and controlling the access level for reading and modifying attributes in the program.
In the previous chapters, we have been accessing the attributes defined in the class using the “class_object.attribute” method, which violates the principle of encapsulation. Normally, the attributes contained in a class should be hidden from the outside, and access and manipulation of class attributes should only be allowed through methods provided by the class. The class should design getter or setter methods for the attributes, allowing manipulation of attributes through the “class_object.method(parameters)” approach.
Hiding Attributes
Hiding the encapsulated attributes
class Func:
x = 1
def f1(self):
print('from f1()')
Func.x
Func.f1
By adding a __ prefix to the attribute name and function name, the effect of hiding attributes and functions can be achieved.
class Func:
__x = 1
def __f1(self):
print('from f1()')
Func.x
Func.f1
Execution result:
AttributeError: type object 'Func' has no attribute 'x'
class Func:
__x = 1
def __f1(self):
print('from f1()')
Func.__x
Func.__f1
Execution result:
AttributeError: type object 'Func' has no attribute '__x'
By checking the namespace of the class Func, we can see the attributes and functions.
class Func:
__x = 1
def __f1(self):
print('from f1()')
print(Func.__dict__)
Execution result:
{'__module__': '__main__', '_Func__x': 1, '_Func__f1': <function Func.__f1 at 0x10285bb00>, '__dict__': <attribute '__dict__' of 'Func' objects>, '__weakref__': <attribute '__weakref__' of 'Func' objects>, '__doc__': None}
In the Func namespace, x and f1 exist in the form of _Func__x and _Func__f1. That is, _class_name__attribute_name, _class_name__function_name, which can be accessed as follows:
class Func:
__x = 1
def __f1(self):
print('from f1()')
Func._Func__x
Func._Func__f1
Thus, this hiding operation does not strictly limit external access; it is merely a syntactical transformation.
This kind of hiding can be accessed internally within the class using the __attribute_name and __function_name format.
class Func:
__x = 1
def __f1(self):
print('from f1()')
def f2(self):
print(self.__x)
print(self.__f1)
obj = Func()
obj.f2()
Execution result:
1
<bound method Func.__f1 of <__main__.Func object at 0x102dfcb10>>
This means that this kind of hiding is not external but internal, and this transformation only occurs once during the syntax check of the class body.
Hiding attributes does not prevent external calls; rather, it prevents direct external calls, requiring calls through well-defined interfaces in the class.
Open Interfaces
By implementing interfaces for attribute calls within the class, we can open them up for external calls.
Hiding data restricts direct operations on data from outside the class, and the class should provide corresponding interfaces to allow external operations on the data indirectly. Additional logic can be attached to the interface to strictly control data operations. Hiding function attributes is to isolate complexity; functionalities that are not needed by the caller do not need to be exposed.
class People:
def __init__(self, name):
self.__name = name
def get_name(self):
print(self.__name)
def set_name(self, name):
self.__name = name
obj = People('ailin')
obj.get_name()
obj.set_name('nichengwe')
obj.get_name()
Execution result:
ailin
nichengwe
Property
Property is a decorator. (A decorator is a callable object that adds new functionality to the decorated object without modifying the source code or calling method of the decorated object.)
Below is an example of calculating the BMI index for adults.
The BMI value for adults:
Formula: Body Mass Index (BMI) = Weight (kg) รท Height^2^ (m)
Underweight: below 18.4 Normal: 18.5 ~ 23.9 Overweight: 24.0 ~ 27.9 Obese: above 28.0
class People:
def __init__(self, name, weight, height):
self.name = name
self.weight = weight
self.height = height
def bmi(self):
return self.weight / (self.height ** 2)
obj1 = People('ailin', 65, 1.70)
print(obj1.bmi())
Execution result:
22.49134948096886
The above program has implemented the functionality to obtain the BMI value, but BMI is more like an attribute value of a person rather than a function. Next, we will use the property decorator to disguise the bmi function as an attribute.
class People:
def __init__(self, name, weight, height):
self.name = name
self.weight = weight
self.height = height
@property
def bmi(self):
return self.weight / (self.height ** 2)
obj1 = People('ailin', 65, 1.70)
print(obj1.bmi)
Execution result:
22.49134948096886
As we can see, the function bmi decorated with property is called in the form of an attribute, obj1.bmi. If we try to call it as a function, it will result in an error, obj1.bmi().
obj1 = People('ailin', 65, 1.70)
print(obj1.bmi())
Execution result:
TypeError: 'float' object is not callable
Below is the previous example of hiding attributes:
class People:
def __init__(self, name):
self.__name = name
def get_name(self):
return self.__name
def set_name(self, name):
self.__name = name
obj = People('ailin')
obj.get_name()
obj.set_name('nichengwe')
obj.get_name()
Adding the property() function for disguise
The syntax for property() as a function: attribute_name=property(fget=None, fset=None, fdel=None, doc=None)
Attribute_name refers to the name of the attribute to be hidden or disguised. In the above example, the four parameters of the name property correspond to get_name, set_name, del_name, and the documentation string. These four parameters can be specified as only the first one, or only the first two, or only the first three, etc.
class People:
def __init__(self, name):
self.__name = name
def get_name(self):
return self.__name
def set_name(self, name):
self.__name = name
name = property(get_name, set_name)
obj = People('ailin')
print(obj.name)
obj.name = 'nichengwe'
print(obj.name)
Execution result:
ailin
nichengwe
Adding the property decorator for disguise
class People:
def __init__(self, name):
self.__name = name
# Using the property decorator to convert the get_name method into an attribute getter
@property
def name(self):
return self.__name
# Using the property decorator to convert the set_name method into an attribute setter
@name.setter
def name(self, name):
self.__name = name
obj = People('ailin')
print(obj.name)
obj.name = 'nichengwe'
print(obj.name)
Execution result:
ailin
nichengwe
The above program changes the names of the methods that operate on the attributes to the names that need to be operated when called externally, here it is name. For each operation method, the corresponding property name method is added, such as name.setter, name.deleter, etc. When calling the attributes in the class from outside, the hidden attributes can be accessed using the class_object.attribute_name approach.