Python Intermediate and Advanced

Python Intermediate_007: Understanding @property and Setter Decorators in Python

codeaddict 2025. 3. 9. 18:09

In our last lesson (https://codeaddict.tistory.com/entry/Python-Intermediate006-Decorators-in-Python), we explored basic and practical decorators like access control. Today, we’ll dive into Python’s @property decorator and its companion @<property_name>.setter, which transform methods into manageable attributes. These tools are key for encapsulation in object-oriented programming. We’ll cover syntax, execution order, and some examples!

What Are @property and Setter Decorators?

  • @property: Converts a method into a getter, letting you access it like an attribute (e.g., obj.name instead of obj.name()).
  • @<property_name>.setter: Defines a method to set the attribute’s value, often with validation or logic.

These are built-in Python decorators for controlling class attribute access, replacing the need for explicit getter/setter methods.

Syntax and Execution Order

Syntax

class MyClass:
    def __init__(self):
        self._value = 0  # Private-ish attribute (convention with underscore)

    @property
    def value(self):  # Getter
        return self._value

    @value.setter
    def value(self, new_value):  # Setter
        self._value = new_value
  • Getter: @property decorates a method to act as an attribute.
  • Setter: @value.setter (note the dot notation) decorates a method to handle assignment.

Execution Order

Here’s how it works when using these decorators:

  1. Class Definition: Python registers the @property and @<property_name>.setter methods during class creation.
  2. Object Creation: The __init__ method initializes the underlying attribute (e.g., _value).
  3. Getter Call: Accessing obj.value invokes the @property-decorated method.
  4. Setter Call: Assigning obj.value = x invokes the @value.setter-decorated method.

Example Flow

obj = MyClass()
print(obj.value)  # Step 3: Calls the getter
obj.value = 42    # Step 4: Calls the setter
print(obj.value)

Step-by-Step Explanation

  1. obj = MyClass()
  • What Happens: Creates an instance of MyClass. The __init__ method runs, setting self._value = 0 (the initial value).
  • Function Run: __init__
  • Printed: Nothing is printed yet.

2. print(obj.value) (First print)

  • What Happens: Accesses obj.value, which triggers the @property-decorated value method (the getter). This method returns self._value, which is currently 0.
  • Function Run: The getter method value(self) under @property.
  • Printed: The value returned by the getter, which is 0. So, it prints:

0

3. obj.value = 42

  • What Happens: Assigns 42 to obj.value, triggering the @value.setter-decorated value method (the setter). The setter updates self._value to 42.
  • Function Run: The setter method value(self, new_value) under @value.setter.
  • Printed: Nothing is printed by default (unless the setter has a print statement, which the base example doesn’t). The value of self._value is now 42.

4. print(obj.value) (Second print)

  • What Happens: Accesses obj.value again, calling the @property-decorated getter method. It returns self._value, which is now 42.
  • Function Run: The getter method value(self) under @property.
  • Printed: The updated value, which is 42. So, it prints:

42

Practical Example: Managing Employee Salary

Problem Definition

You’re designing a system to manage employee data. Each employee has a salary, but:

  • The salary must not be negative (validation required).
  • You want to access and update it like an attribute (e.g., emp.salary instead of emp.get_salary()).
  • Updates should log changes for auditing.

How would you implement this? Try solving it before scrolling down!

Solution

class Employee:
    def __init__(self, name, salary):
        self.name = name
        self._salary = salary  # Private-ish attribute

    @property
    def salary(self):
        print(f"   → Fetching salary for {self.name}")
        return self._salary

    @salary.setter
    def salary(self, new_salary):
        if new_salary < 0:
            print("Error: Salary cannot be negative!")
            return  # Ignore invalid updates
        print(f"1. Updating salary for {self.name} to {new_salary}")
        self._salary = new_salary

# Test it
print("START")
emp = Employee("Alice", 50000)
print(f"Initial salary: {emp.salary}")
emp.salary = 60000  # Update salary
print(f"New salary: {emp.salary}")
emp.salary = -1000  # Try invalid update
print(f"Final salary: {emp.salary}")
print("END")

Output

START
   → Fetching salary for Alice
Initial salary: 50000
1. Updating salary for Alice to 60000
   → Fetching salary for Alice
New salary: 60000
Error: Salary cannot be negative!
   → Fetching salary for Alice
Final salary: 60000
END

Detailed Explanation

  • Class Setup: Employee uses _salary as a pseudo-private attribute (underscore convention).
  • @property: The salary method becomes a getter. Accessing emp.salary runs this method, logging the fetch and returning _salary.
  • @salary.setter: The setter validates new_salary. If negative, it rejects the change; otherwise, it updates _salary and logs it.
  • Execution:
  1. emp.salary (getter) fetches the initial value (50000).
  2. emp.salary = 60000 (setter) updates it with validation.
  3. emp.salary = -1000 (setter) fails due to the check, keeping the value at 60000.

This encapsulates _salary, enforces rules, and provides a clean interface.

Summary

  • @property: Turns methods into readable attributes.
  • @<property_name>.setter: Adds logic to attribute assignment (e.g., validation).
  • Practical Use: Ideal for managing data like salaries or settings with control.