First, we will add attributes to a class in Python and then use prefixes to hide specific members of a class. We will use property getters and setters to control how we write and retrieve values to and from related attributes. We will use methods to add behaviors to classes, and we will create the mutable and immutable version of a 3D vector to understand the difference between an object that mutates state and an object that doesn't.
The
TibetanSpaniel
class is a blueprint for dogs that belong to the Tibetan Spaniel breed. This class should inherit from a Dog
superclass, but we will forget about inheritance and other dog breeds for a while. We will use the TibetanSpaniel
class to understand the difference between class attributes and instance attributes.
As happens with any other dog breed, Tibetan Spaniel dogs have some profile values. We will define the following class attributes to store the values that are shared by all the members of the Tibetan Spaniel breed. Note that the valid values for scores are from 0
to 10
; 0
is the lowest skill and 10
the highest.
family
: This is the family to which the dog breed belongsarea_of_origin
: This is the are a of origin of the dog breedlearning_rate
: This is the typical learning rate score for the members of this dog breedobedience
: This is the average obedience score for the members of this dog breedproblem_solving
: This is the average problem solving score for the members of this dog breedThe following lines create a TibetanSpaniel
class and declare the previously enumerated class attributes and a __init__
method within the body of a class:
class TibetanSpaniel: family = "Companion, herding" area_of_origin = "Tibet" learning_rate = 9 obedience = 3 problem_solving = 8 def __init__(self, name, favorite_toy, watchdog_ability): self.name = name self.watchdog_ability = watchdog_ability self.favorite_toy = favorite_toy
The preceding code assigns a value to each class variable after the class header within the class body and without self.
as its prefix. This code assigns a value outside of any method because there is no need to create any instance to access the attributes of a class.
The following command prints the value of the previously declared family
class attribute. Note that we didn't create any instance of the TibetanSpaniel
class. Also, we specify an attribute after the class name and a dot:
print(TibetanSpaniel.family)
The following command creates an instance of the TibetanSpaniel
class and then prints the value of the family class attribute. In this case, we will use an instance to access the class attribute:
brian = TibetanSpaniel("Brian", "Talking Minion", 4) print(brian.family)
You can assign a new value to any class attribute. For example, the following command assigns 4
to the obedience
class attribute:
TibetanSpaniel.obedience = 4
However, we must be very careful when we want to assign a new value to a class variable. We must address the class attribute through a class and not through an instance. If we assign a value to obedience
through an instance named brian
, Python will create a new instance attribute called obedience
, instead of changing the value of the class attribute with the same name.
The following commands illustrate the problem. The first command creates a new instance attribute called obedience
and sets its value to 8
; therefore, brian.obedience
will be 8
. However, if we check the value of the TibetanSpaniel.obedience
or type(brian).obedience
class variable, the value continues to be 4
:
brian.obedience = 8 print(brian.obedience) print(type(brian).obedience) print(TibetanSpaniel.obedience)
The previously declared TibetanSpaniel
class exposes the instance and class attributes without any kind of restriction. Thus, we can access these attributes and change their values. Python uses a special naming convention for attributes to control their accessibility. So far, we have been using attribute names without any kind of prefix; therefore, we could access attribute names within a class definition and outside of a class definition. These kinds of attributes are known as public attributes and are exposed without any restriction.
In Python, we can mark an attribute as protected by prefixing the attribute name with a leading underscore (_
). For example, if we want to convert the name attribute from a public to a protected attribute, we just need to change its name from name
to _name
.
Whenever we mark an attribute as protected, we are telling the users of the class that they shouldn't use these attributes outside of the class definition. Thus, only the code written within the class definition and within subclasses should access attributes marked as protected. We say should, because Python doesn't provide a real shield for the attributes marked as protected; the language just expects users to be honest and take into account the naming convention. The following command shows a new version of the __init__
method for the TibetanSpaniel
class that declares three instance attributes as protected by adding a leading underscore (_
) to names:
def __init__(self, name, favorite_toy, watchdog_ability): self._name = name self._watchdog_ability = watchdog_ability self._favorite_toy = favorite_toy
We can mark an attribute as private by prefixing the attribute name with two leading underscores (__
). For example, if we want to convert the name attribute from a protected to a private attribute, we just need to change its name from _name
to __name
.
Whenever we mark an attribute as private, Python doesn't allow users to access the attribute outside of the class definition. The restriction also applies to subclasses; therefore, only the code written within a class can access attributes marked as private.
Python still provides access to these attributes outside of the class definition with a different name composed of a leading underscore (_
), followed by the class name and the private attribute name. For example, if we use __name
to mark name as a private attribute, it will be accessible with the TibetanSpaniel__name
name. Obviously, the language expects users to be honest, take into account the naming convention, and avoid accessing the renamed private attribute. The following commands show a new version of the __init__
method for the TibetanSpaniel
class that declares three instance attributes as private by adding two leading underscores (__
) to names:
def __init__(self, name, favorite_toy, watchdog_ability): self.__name = name self.__watchdog_ability = watchdog_ability self.__favorite_toy = favorite_toy
Python provides a simple mechanism to define properties and specify the getter and/or setter methods. We want to make the following changes to our TibetanSpaniel
class:
name
attribute with a read-only name
property__favorite_toy
attribute with a favorite_toy
property__watchdog_ability
attribute with a watchdog_ability
property and include the code in the setter method to assign 0
to the underlying attribute if the value specified is lower than 0
and 10
if the value specified is higher than 10
protection_score
read-only property with a getter method that calculates and returns a protection score based on the values of the __watchdog_ability
private instance attribute, the learning_rate
public class attribute, and the problem_solving
public class attributeWe want a read-only name
property; therefore, we just need to define a getter method that returns the value of the related __name
private attribute. We just need to define a method named name
and decorate it with @property
. The following commands within the class body will do the job. Note that @property
is included before the method's header:
@property def name(self): return self.__name
After we add a getter method to define a read-only name property, we can create an instance of the edited class and try to change the value of the read-only property name, as shown in the following command:
merlin = TibetanSpaniel("Merlin", "Talking Smurf", 6) merlin.name = "brian"
The Python console will display the following error because there is no setter method defined for the name
property:
Traceback (most recent call last): File "<input>", line 1, in <module> AttributeError: can't set attribute
We want to encapsulate the __favorite_toy
private attribute with the favorite_toy
property; therefore, we have to define both getter and setter methods. The getter method returns the value of the related __favorite_toy
private attribute. The setter method receives the new favorite toy value as an argument and assigns this value to the related __favorite_toy
private attribute. We have to decorate the setter method with @favorite_toy.setter
, that is, @
, followed by the property name and .setter
. The following commands within the class body will do the job:
@property def favorite_toy(self): return self.__favorite_toy @favorite_toy.setter def favorite_toy(self, favorite_toy): self.__favorite_toy = favorite_toy
The setter method for the favorite_toy
property is very simple. The watchdog_ability
property requires a setter method with more code to transform values lower than 0
in 0
and values higher than 10
in 10
. The following class body will do the job:
@property def watchdog_ability(self): return self.__watchdog_ability @watchdog_ability.setter def watchdog_ability(self, watchdog_ability): if watchdog_ability < 0: self.__watchdog_ability = 0 elif watchdog_ability > 10: self.__watchdog_ability = 10 else: self.__watchdog_ability = watchdog_ability
After we add the watchdog_ability
property, we will create an instance of the edited class and try to set different values to this property, as shown in the following code:
hugo = TibetanSpaniel("Hugo", "Tennis ball", 7) hugo.watchdog_ability = -3 print(hugo.watchdog_ability) hugo.watchdog_ability = 30 print(hugo.watchdog_ability) hugo.watchdog_ability = 8 print(hugo.watchdog_ability)
In the preceding code, after we specified -3
as the desired value for the watchdog_ability
property, we printed its actual value and the result was 0
. After we specified 30
, the actual printed value was 10
. Finally, after we specified 8
, the actual printed value was 8
. The code in the setter method did its job. This is how we could control the values accepted for the underlying private instance attribute.
We want a read-only protection_score
property. However, this time the getter method must calculate and return a protection score based on a private instance attribute and two public class attributes. Note that the code accesses the public class attributes through type(self)
, followed by the attribute name. It's a safe way to access class attributes because we can change the class name or work with inheritance without unexpected issues. The following commands in the class body will do the job:
@property def protection_score(self): return math.floor((self.__watchdog_ability + type(self).learning_rate + type(self).problem_solving) / 3)
After we add the protection_score
property, we will create an instance of the edited class and print the value of this read-only property:
cole = TibetanSpaniel("Cole", "Soccer ball", 4) print(cole.protection_score)
Here is the complete code for the TibetanSpaniel
class along with properties:
class TibetanSpaniel: family = "Companion, herding" area_of_origin = "Tibet" learning_rate = 9 obedience = 3 problem_solving = 8 def __init__(self, name, favorite_toy, watchdog_ability): self.__name = name self.__watchdog_ability = watchdog_ability self.__favorite_toy = favorite_toy @property def name(self): return self.__name @property def favorite_toy(self): return self.__favorite_toy @favorite_toy.setter def favorite_toy(self, favorite_toy): self.__favorite_toy = favorite_toy @property def watchdog_ability(self): return self.__watchdog_ability @watchdog_ability.setter def watchdog_ability(self, watchdog_ability): if watchdog_ability < 0: self.__watchdog_ability = 0 elif watchdog_ability > 10: self.__watchdog_ability = 10 else: self.__watchdog_ability = watchdog_ability @property def protection_score(self): return math.floor((self.__watchdog_ability + type(self).learning_rate + type(self).problem_solving) / 3)
So far, we have added instance methods to classes and used getter and setter methods combined with decorators to define properties. Now, we want to generate a class to represent the mutable version of a 3D vector.
We will use properties with simple getter and setter methods for x
, y
, and z
. The sum
public instance method receives the delta values for x
, y
, and z
and mutates an object, that is, the setter method changes the values of x
, y
, and z
. Here is the initial code of the MutableVector3D
class:
class MutableVector3D: def __init__(self, x, y, z): self.__x = x self.__y = y self.__z = z def sum(self, delta_x, delta_y, delta_z): self.__x += delta_x self.__y += delta_y self.__z += delta_z @property def x(self): return self.__x @x.setter def x(self, x): self.__x = x @property def y(self): return self.__y @y.setter def y(self, y): self.__y = y @property def z(self): return self.__z @z.setter def z(self, z): self.__z = z
It's a very common requirement to generate a 3D vector with all the values initialized to 0
, that is, x = 0
, y = 0
, and z = 0
. A 3D vector with these values is known as an origin vector. We can add a class method to the MutableVector3D
class named origin_vector
to generate a new instance of the class initialized with all the values initialized to 0
. It's necessary to add the @classmethod
decorator before the class method header. Instead of receiving self
as the first argument, a class method receives the current class; the parameter name is usually named cls
. The following code defines the origin_vector
class method:
@classmethod def origin_vector(cls): return cls(0, 0, 0)
The preceding method returns a new instance of the current class (cls
) with 0
as the initial value for the three elements. The class method receives cls
as the only argument; therefore, it will be a parameterless method when we call it because Python includes a class as a parameter under the hood. The following command calls the origin_vector
class method to generate a 3D vector, calls the sum method for the generated instance, and prints the values for the three elements:
mutableVector3D = MutableVector3D.origin_vector() mutableVector3D.sum(5, 10, 15) print(mutableVector3D.x, mutableVector3D.y, mutableVector3D.z)
Now, we want to generate a class to represent the immutable version of a 3D vector. In this case, we will use read-only properties for x
, y
, and z
. The sum
public instance method receives the delta values for x
, y
, and z
and returns a new instance of the same class with the values of x
, y
, and z
initialized with the results of the sum. Here is the code of the ImmutableVector3D
class:
class ImmutableVector3D: def __init__(self, x, y, z): self.__x = x self.__y = y self.__z = z def sum(self, delta_x, delta_y, delta_z): return type(self)(self.__x + delta_x, self.__y + delta_y, self.__z + delta_z) @property def x(self): return self.__x @property def y(self): return self.__y @property def z(self): return self.__z @classmethod def equal_elements_vector(cls, initial_value): return cls(initial_value, initial_value, initial_value) @classmethod def origin_vector(cls): return cls.equal_elements_vector(0)
Note that the sum
method uses type(self)
to generate and return a new instance of the current class. In this case, the origin_vector
class method returns the results of calling the equal_elements_vector
class method with 0
as an argument. Remember that the cls
argument refers to the actual class. The equal_elements_vector
class method receives an initial_value
argument for all the elements of the 3D vector, creates an instance of the actual class, and initializes all the elements with the received unique value. The origin_vector
class method demonstrates how we can call another class method in a class method.
The following command calls the origin_vector
class method to generate a 3D vector, calls the sum
method for the generated instance, and prints the values for the three elements of the new instance returned by the sum
method:
vector0 = ImmutableVector3D.origin_vector() vector1 = vector0.sum(5, 10, 15) print(vector1.x, vector1.y, vector1.z)
As explained previously, we can change the values of the private attributes; therefore, the ImmutableVector3D
class isn't 100 percent immutable. However, we are all adults and don't expect the users of a class with read-only properties to change the values of private attributes hidden under difficult to access names.
18.119.172.243