lettura simple

The __setattr__() Method in Python

Today, we’re going to explore the __setattr__() method in Python. This method is automatically called every time you assign a value to an object’s attribute.

object.__setattr__('attribute', value)

It’s part of what are known as Python’s magic or special methods.

How does it work?

Whenever you assign a value to an object’s attribute, Python automatically invokes the object’s `__setattr__()` method to handle the operation.

For example, when you write something like:

obj.attr = value

Behind the scenes, Python is essentially doing this:

obj.__setattr__('attr', value)

In other words, the `__setattr__()` method defines how the object should respond when someone tries to modify one of its attributes.

Note. Python provides a default implementation for this method, which simply assigns the attribute value as you’d expect. However, the real power of `__setattr__()` lies in the ability to override it, allowing you to customize what happens when attributes are modified. Essentially, you can tell Python, "Every time someone tries to change this attribute, I want something specific to happen".

A Practical Example

Let’s start by defining a simple `Person` class:

class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age

Now, create an instance of the class:

p = Person("Luca", 20)

When you update an attribute, like this:

p.age = 21

Python automatically calls the default `__setattr__()` method, which updates the `age` attribute:

print(p.age)

21

You can achieve the same result by directly invoking `__setattr__()`:

p.__setattr__('age', 22)

Now, the `age` attribute is set to 22.

print(p.age)

22

So far, we’ve been using Python’s default `__setattr__()`. Next, let’s look at how we can override it to customize behavior.

Customizing the __setattr__() Method in a Class

Python allows you to override the `__setattr__()` method in a class through method overriding. This enables you to customize the behavior when setting an attribute.

For example, imagine you want to track every change made to an object’s attributes:

class ChangeTracker:
    def __init__(self):
        # Create a dictionary to track changes
        self.changes = {}

    def __setattr__(self, name, value):
        # Avoid infinite loop by checking if we're modifying 'changes'
        if name == 'changes':
            super().__setattr__(name, value)
        else:
            # Log the change to the dictionary
            self.changes[name] = value
            # Set the attribute using the default behavior
            super().__setattr__(name, value)

Create an instance of the class:

t = ChangeTracker()

Then modify some attributes:

t.name = "Andrea"
t.age = 42

You can now check the modification history:

print(t.changes)

{'name': 'Andrea', 'age': 42}

What’s going on here?

Each time we assign a value to an attribute (e.g., `t.name = "Andrea"`), Python calls `__setattr__()`, which logs the change in the `changes` dictionary.

Then it uses the base method via `super().__setattr__()`, avoiding an infinite loop, to actually update the attribute.

This way, you can track all attribute changes without disrupting the class’s normal behavior.

Why can’t we assign directly? The key point to remember is that inside `__setattr__()`, you can’t use the usual syntax like:

self.name = value

This would cause `__setattr__()` to be called again, creating an infinite loop. To prevent this, we use super() to call the base method, bypassing our custom `__setattr__()` and avoiding the loop.

super().__setattr__(name, value)

Let’s look at another example of how this method can be used.

You can also use `__setattr__()` for attribute validation.

For instance, let’s say you want to ensure that only positive integers can be assigned to the `age` attribute of a `Person` class:

class Person:
    def __init__(self, name=None, age=None):
        # Initialize attributes to avoid errors
        self.name = name
        self.age = age

    def __setattr__(self, name, value):
        if name == "age" and (not isinstance(value, int) or value < 0):
            raise ValueError("Age must be a positive integer.")
        super().__setattr__(name, value)

In this class, the `age` attribute is restricted to positive integers.

This rule is enforced through the `__setattr__()` method.

Create an instance of the class:

p = Person()

Then assign some values:

p.name = "Andrea"
p.age = 42 

The program works fine because 42 is a valid positive integer.

print(p.age)

42

However, if you try to assign a negative value to `age`, the program will throw an exception:

p.age = -5 

ValueError: Age must be a positive integer.

Again, remember to use `super().__setattr__()` inside your `__setattr__()` method to avoid infinite loops when assigning values!

I hope this gives you a solid understanding of the usefulness of `__setattr__()` and how to apply it in practical scenarios.




Report a mistake or post a question




FacebookTwitterLinkedinLinkedin