In Python, a descriptor is an attribute of an object with specialized behavior controlled by methods like get()
, set()
, and delete()
. It is a class that allows us to modify the properties of another class object.
A descriptor implements at least one of the methods __get__()
, __set()__
, or __delete()__
, which define how an attribute is accessed, modified, and deleted when it is part of a class that uses a descriptor.
__get__()
accesses the attribute or when you want to extract some information. It returns the value of the attribute or raises the AttributeError
exception if a requested attribute is not present.
__set__()
is called in an attribute assignment operation that sets the value of an attribute. Returns nothing. But can raise the AttributeError
exception.
__delete__()
controls a delete operation, i.e., when you want to delete the attribute from an object. Returns nothing.
The default behavior for attribute access is to get, set, or delete the attribute from an object’s dictionary. If the looked-up value is an object defining one of the descriptor methods, then Python may override the default behavior and invoke the descriptor method instead. Where this occurs in the precedence chain depends on which descriptor methods were defined.
property()
The property()
function in Python is a built-in function that allows you to create a special kind of attribute known as a property; it is a feature of a class that looks like a regular attribute but is managed using methods, typically getter, setter, and deleter methods.
The property()
function takes up to four parameters (in the given order):
fget
: It is a reference to the method that will be used as the getter. It is called when the property is accessed.
fset
: It is a reference to the method that will be used as the setter. It is called when the property is assigned a new value.
fdel
: It is a reference to the method that will be used as the deleter. It is called when the del
statement is used to delete the property.
doc
: It is an optional parameter that allows you to set the docstring for the property.
class Circle:def __init__(self, radius):self._radius = radius# Getter method to retrieve the radiusdef get(self):print('Get radius value')return self._radius# Setter method to set the radiusdef set(self, value):if value < 0:raise ValueError("Radius cannot be negative")print('Set radius value to', value)self._radius = value# Deleter method to delete the radius attributedef delete(self):print('Delete radius')del self._radius# Define a property using property() functionradius = property(get, set, delete)# Create an instance of Circlemy_circle = Circle(5)# Access the radius using the propertyprint('Radius:', my_circle.radius)# Set a new radius using the propertymy_circle.radius = 7# Delete the radius attribute using the propertydel my_circle.radius
In this example, the Circle
class has a radius
property that allows getting, setting, and deleting the radius. get()
retrieves the radius, set()
assigns a value to the radius (with a check for negative values), and delete()
removes the radius attribute. At last, the property()
function is used to define the radius
property. Let's look at the code line by line:
Line 1: Defines a new class named Circle
.
Line 2: Defines the initializer method (__init__
) for the Circle
class, which takes radius
as an argument.
Line 3: Initializes the instance variable _radius
with the value passed to the constructor.
Line 6: Defines a getter method get
for retrieving the value of _radius
.
Line 7: Prints a message indicating that the getter method is called.
Line 8: Returns the value of _radius
.
Line 11: Defines a setter method set
for setting the value of _radius
.
Line 12: Checks if the given value is less than 0.
Line 13: Raises a ValueError
if the given value is negative.
Line 14: Prints a message indicating the new value being set.
Line 15: Sets the value of _radius
to the given value.
Line 18: Defines a deleter method delete
for deleting the _radius
attribute.
Line 19: Prints a message indicating that the _radius
attribute is being deleted.
Line 20: Deletes the _radius
attribute.
Line 23: Defines a property radius
using the property()
function, linking it to the getter, setter, and deleter methods.
Line 26: Creates an instance of the Circle
class with an initial radius of 5.
Line 29: Prints the current value of the radius
property.
Line 32: Sets a new value for the radius
property.
Line 35: Deletes the radius
attribute using the property.
In this method we create a class and override the descriptor methods __set__
, __ get__
, and __delete__
we need to use. This is especially useful when we need to use the same descriptor for multiple classes or attributes.
class CustomDescriptor:def __init__(self, attribute_name=''):self.attribute_name = attribute_namedef __get__(self, instance, owner):return "{} for {}".format(self.attribute_name, owner.__name__)def __set__(self, instance, value):if isinstance(value, str):self.attribute_name = valueelse:raise TypeError("Attribute name should be a string")class MyClass:attribute = CustomDescriptor()# Create an instance of MyClassmy_instance = MyClass()# Access the descriptor againprint(my_instance.attribute)# Set the descriptormy_instance.attribute = "ExampleAttribute"# Access the descriptor againprint(my_instance.attribute)
In this example, the CustomDescriptor
class is defined with an __init__
method initializing an attribute name, a __get__
method returning a formatted string containing the attribute name and the owning class name when accessed, and a __set__
method updating the attribute name if the assigned value is a string. The MyClass
class utilizes an instance of CustomDescriptor
as the attribute
property. An instance of MyClass
is created, and the attribute
property is set to "ExampleAttribute". Let's look at the explanation of this code:
Line 1: Defines a new class named CustomDescriptor
.
Line 2: Defines the initializer method (__init__
) for the CustomDescriptor
class, which takes an optional attribute_name
argument.
Line 3: Initializes the instance variable attribute_name
with the value passed to the constructor.
Line 5: Defines the __get__
method for the descriptor, which is called to get the attribute's value.
Line 6: Returns a formatted string combining the attribute_name
and the owner's class name.
Line 8: Defines the __set__
method for the descriptor, which is called to set the attribute's value.
Line 9: Checks if the given value is a string.
Line 10: Sets the value of attribute_name
to the given string value.
Line 11: Defines the alternative action if the value is not a string.
Line 12: Raises a TypeError
if the given value is not a string.
Line 14: Defines a new class named MyClass
.
Line 15: Creates an instance of CustomDescriptor
and assigns it to the attribute
class attribute.
Line 18: Creates an instance of the MyClass
class.
Line 21: Prints the value of the attribute
descriptor for the my_instance
object.
Line 27: Prints the value of the attribute
descriptor for the my_instance
object after setting it.
@property
The @property
decorator in Python is used to create read-only properties for class attributes. By decorating a method with @property
, the method is transformed into a property, allowing it to be accessed like an attribute rather than a method. This is particularly useful for encapsulating the logic of attribute access, enabling computed or dynamic attribute values without the need for explicit method calls.
class Person:def __init__(self, name):self._name = name# Retrieve the person's name@propertydef name(self):print('Get name')return self._name# Set the person's name@name.setterdef name(self, new_name):print('Set name to ' + new_name)self._name = new_name# Delete the person's name@name.deleterdef name(self):print('Delete name')del self._name# Create a person instancep = Person('John Doe')# Access and print the person's nameprint(p.name)# Update the person's namep.name = 'Jane Doe'# Delete the person's namedel p.name
In this example, the Person
class has a name
property implemented using the @property
, @name.setter
, and @name.deleter
decorators. The property allows getting, setting, and deleting the person's name, demonstrating the use of decorators in a more personalized context. Now, let's look at the line by line explanation of this code:
Line 1: Defines a new class named Person
.
Line 2: Defines the initializer method (__init__
) for the Person
class, which takes name
as an argument.
Line 3: Initializes the instance variable _name
with the value passed to the constructor.
Line 6: Decorates the method below as a property getter for name
.
Line 7: Defines the getter method name
for retrieving the value of _name
.
Line 8: Prints a message indicating that the getter method is called.
Line 9: Returns the value of _name
.
Line 12: Decorates the method below as a property setter for name
.
Line 13: Defines the setter method name
for setting the value of _name
.
Line 14: Prints a message indicating the new value being set.
Line 15: Sets the value of _name
to the given new_name
.
Line 18: Decorates the method below as a property deleter for name
.
Line 19: Defines the deleter method name
for deleting the _name
attribute.
Line 20: Prints a message indicating that the _name
attribute is being deleted.
Line 21: Deletes the _name
attribute.
Line 24: Creates an instance of the Person
class with the name 'John Doe'.
Line 27: Accesses and prints the value of the name
property, triggering the getter method.
Line 30: Sets a new value for the name
property, triggering the setter method.
Line 33: Deletes the name
attribute using the property, triggering the deleter method.
The purpose of using descriptors in Python is to customize attribute access in classes. Descriptors enable developers to define how attribute retrieval, assignment, and deletion should behave, allowing for controlled and dynamic behavior. This customization is particularly valuable when enforcing constraints, implementing computed attributes, or executing additional logic during attribute operations.
Moreover, descriptors represent a powerful and general-purpose protocol in Python, serving as the mechanism behind properties, methods, static methods, class methods, and super(). They are extensively utilized throughout Python itself, particularly for implementing the new-style classes introduced in version 2.2. Descriptors streamline the underlying C code and provide a versatile set of tools for everyday Python programming tasks.
Free Resources