
The __get__() Method in Python
The `__get__()` method in Python is triggered whenever you access an attribute that uses a descriptor.
What is a descriptor? In Python, a descriptor is any object that implements one or more special methods to control how a class attribute is accessed, assigned, or deleted. The method we’re focusing on today is `__get__()`.
The primary purpose of the `__get__()` method is to customize what gets returned when an attribute is accessed.
You have control over what to return, how it’s returned, and can even apply transformations or checks before providing the result.
The basic syntax of this method is:
def __get__(self, instance, owner):
Here’s a breakdown of the parameters:
- `self`: refers to the descriptor instance.
- `instance`: the instance of the class containing the attribute (or `None` if accessing it from the class itself).
- `owner`: the class that owns the descriptor.
Let’s look at a simple example to see how `__get__()` works in practice.
Imagine you want to automatically format attribute values every time they're accessed. For instance, you might want to ensure that a string always has its first letter capitalized.
To do this, you would create a descriptor that automatically capitalizes text.
class CapitalizeDescriptor:
def __get__(self, instance, owner):
# Retrieve the value from the instance using a "private" attribute
value = instance.__dict__.get(f"_{self.attribute}", "")
return value.capitalize() if isinstance(value, str) else value
def __set_name__(self, owner, name):
# Store the attribute name
self.attribute = name
This descriptor is called 'CapitalizeDescriptor'.
The `__set_name__` method stores the name of the attribute that’s assigned to the descriptor, while the `__get__` method returns the attribute value with the first letter capitalized whenever it’s accessed.
Now, let’s say you have a class called 'Person' that represents a person.
class Person:
name = CapitalizeDescriptor() # Using the descriptor for the 'name' attribute
def __init__(self, name, age, city):
self._name = name
self.age = age
self.city = city
In this class, the 'name' attribute is managed by 'CapitalizeDescriptor'.
This means that every time you access the 'name' attribute, it’s automatically returned with the first letter capitalized, regardless of how it was originally input.
To ensure the 'name' attribute isn’t accidentally overwritten in the __init__() constructor, bypassing the descriptor, we store the attribute as a private '_name'. This ensures the descriptor handles it correctly.
Let’s create an instance of the 'Person' class, passing a lowercase string for the name.
p = Person("andrea", 23, 'milano')
Access the 'name' attribute of the object.
print(p.name)
When you access this attribute, Python invokes the `__get__()` method from 'CapitalizeDescriptor', which capitalizes the first letter of the string.
Andrea
So, every time you access the `name` attribute, you get a formatted version of the name, no matter how it was entered.
This behavior only applies to the 'name' attribute, though. For instance, if you print the value of the 'city' attribute, it doesn’t pass through the descriptor and is returned in lowercase.
print(p.city)
milano
If you want both the 'name' and 'city' attributes to be processed by the descriptor and returned with the first letter capitalized, you can modify the 'Person' class like this:
class Person:
name = CapitalizeDescriptor()
city = CapitalizeDescriptor()
def __init__(self, name, age, city):
self._name = name
self.age = age
self._city = city
In conclusion, the `__get__()` method gives you full control over how class attributes are accessed. It's the backbone of descriptors and offers flexibility that goes beyond simple class or instance variables.
Experiment with the code, and you'll see just how powerful and versatile descriptors can be.