OOP Advanced
@property
Managed Attributes
Your Circle class has radius. You want area to look like an attribute but compute on access. You want radius to validate on assignment. @property turns methods into attribute-like access - clean API with hidden logic.
Basic property
Turn a method into a read-only attribute.
# Basic @property Usage
class Person:
"""Demonstrates basic property usage."""
def __init__(self, name, age):
self._name = name
self._age = age
@property
def name(self):
"""Get the person's name."""
print(f" Getting name: {self._name}")
return self._name
@name.setter
def name(self, value):
"""Set the person's name."""
print(f" Setting name: {value}")
self._name = value
@property
def age(self):
"""Get the person's age."""
return self._age
@age.setter
def age(self, value):
"""Set the person's age."""
if value < 0:
raise ValueError("Age cannot be negative")
self._age = value
def main():
print("=== Basic @property Usage ===\n")
# Create person
print("--- Creating Person ---")
person =
print(f"Created: {person._name}, {person._age}\n")
# Access via property (calls getter)
print("--- Getting via Property ---")
name = person.name
print(f"Name is: {name}\n")
# Set via property (calls setter)
print("--- Setting via Property ---")
person.name = "Bob"
print(f"Name changed to: {person.name}\n")
# Age with validation
print("--- Age with Validation ---")
print(f"Current age: {person.age}")
person.age = 31
print(f"New age: {person.age}")
try:
person.age = -5
except ValueError as e:
print(f"Error: {e}")
print("\n=== Key Points ===")
print("""
1. @property decorator converts method to getter
2. @<property>.setter creates setter
3. Access like regular attribute (no parentheses)
4. Underlying value often stored with _ prefix
5. Enables validation, logging, etc.
""")
if __name__ == "__main__":
main()
# Basic @property Usage
class Person:
"""Demonstrates basic property usage."""
def __init__(self, name, age):
self._name = name
self._age = age
@property
def name(self):
"""Get the person's name."""
print(f" Getting name: {self._name}")
return self._name
@name.setter
def name(self, value):
"""Set the person's name."""
print(f" Setting name: {value}")
self._name = value
@property
def age(self):
"""Get the person's age."""
return self._age
@age.setter
def age(self, value):
"""Set the person's age."""
if value < 0:
raise ValueError("Age cannot be negative")
self._age = value
def main():
print("=== Basic @property Usage ===\n")
# Create person
print("--- Creating Person ---")
person =
print(f"Created: {person._name}, {person._age}\n")
# Access via property (calls getter)
print("--- Getting via Property ---")
name = person.name
print(f"Name is: {name}\n")
# Set via property (calls setter)
print("--- Setting via Property ---")
person.name = "Bob"
print(f"Name changed to: {person.name}\n")
# Age with validation
print("--- Age with Validation ---")
print(f"Current age: {person.age}")
person.age = 31
print(f"New age: {person.age}")
try:
person.age = -5
except ValueError as e:
print(f"Error: {e}")
print("\n=== Key Points ===")
print("""
1. @property decorator converts method to getter
2. @<property>.setter creates setter
3. Access like regular attribute (no parentheses)
4. Underlying value often stored with _ prefix
5. Enables validation, logging, etc.
""")
if __name__ == "__main__":
main()
# Basic @property Usage
class Person:
"""Demonstrates basic property usage."""
def __init__(self, name, age):
self._name = name
self._age = age
@property
def name(self):
"""Get the person's name."""
print(f" Getting name: {self._name}")
return self._name
@name.setter
def name(self, value):
"""Set the person's name."""
print(f" Setting name: {value}")
self._name = value
@property
def age(self):
"""Get the person's age."""
return self._age
@age.setter
def age(self, value):
"""Set the person's age."""
if value < 0:
raise ValueError("Age cannot be negative")
self._age = value
def main():
print("=== Basic @property Usage ===\n")
# Create person
print("--- Creating Person ---")
person =
print(f"Created: {person._name}, {person._age}\n")
# Access via property (calls getter)
print("--- Getting via Property ---")
name = person.name
print(f"Name is: {name}\n")
# Set via property (calls setter)
print("--- Setting via Property ---")
person.name = "Bob"
print(f"Name changed to: {person.name}\n")
# Age with validation
print("--- Age with Validation ---")
print(f"Current age: {person.age}")
person.age = 31
print(f"New age: {person.age}")
try:
person.age = -5
except ValueError as e:
print(f"Error: {e}")
print("\n=== Key Points ===")
print("""
1. @property decorator converts method to getter
2. @<property>.setter creates setter
3. Access like regular attribute (no parentheses)
4. Underlying value often stored with _ prefix
5. Enables validation, logging, etc.
""")
if __name__ == "__main__":
main()
@property decorator makes method accessible like attribute.
Property with validation
Validate values before setting.
# Properties with Validation
class BankAccount:
"""Bank account with validation."""
def __init__(self, account_number, balance=0):
self._account_number = account_number
self._balance = balance
@property
def balance(self):
"""Get current balance."""
return self._balance
@balance.setter
def balance(self, value):
"""Set balance with validation."""
if value < 0:
raise ValueError("Balance cannot be negative")
self._balance = value
class User:
"""User with multiple validated properties."""
def __init__(self, username, email, age):
self.username = username # Uses setter
self.email = email # Uses setter
self.age = age # Uses setter
@property
def username(self):
"""Get username."""
return self._username
@username.setter
def username(self, value):
"""Set username - must be 3-20 chars."""
if not isinstance(value, str):
raise TypeError("Username must be a string")
if not (3 <= len(value) <= 20):
raise ValueError("Username must be 3-20 characters")
self._username = value
@property
def email(self):
"""Get email."""
return self._email
@email.setter
def email(self, value):
"""Set email - must contain @."""
if "@" not in value:
raise ValueError("Invalid email format")
self._email = value
@property
def age(self):
"""Get age."""
return self._age
@age.setter
def age(self, value):
"""Set age - must be 0-150."""
if not isinstance(value, int):
raise TypeError("Age must be an integer")
if not (0 <= value <= 150):
raise ValueError("Age must be between 0 and 150")
self._age = value
def main():
print("=== Properties with Validation ===\n")
# Bank account validation
print("--- Bank Account ---")
account = BankAccount("12345", 1000)
print(f"Initial balance: ${account.balance}")
account.balance = 1500
print(f"After deposit: ${account.balance}")
try:
account.balance = -100
except ValueError as e:
print(f"Error: {e}")
# User validation
print("\n--- User Validation ---")
try:
user = User("alice", "alice@example.com", 25)
print(f"Created user: {user.username}, {user.email}, {user.age}")
except (ValueError, TypeError) as e:
print(f"Error: {e}")
# Invalid username
print("\n--- Invalid Username ---")
try:
user.username = "ab" # Too short
except ValueError as e:
print(f"Error: {e}")
# Invalid email
print("\n--- Invalid Email ---")
try:
user.email = "not-an-email"
except ValueError as e:
print(f"Error: {e}")
# Invalid age type
print("\n--- Invalid Age Type ---")
try:
user.age = "25" # String instead of int
except TypeError as e:
print(f"Error: {e}")
# Invalid age range
print("\n--- Invalid Age Range ---")
try:
user.age = 200
except ValueError as e:
print(f"Error: {e}")
# Validation during construction
print("\n--- Validation During Construction ---")
try:
invalid_user = User("a", "bad", 300)
except (ValueError, TypeError) as e:
print(f"Construction failed: {e}")
print("\n=== Key Points ===")
print("""
1. Properties enable validation on assignment
2. Validation runs even during __init__
3. Can check type, range, format, etc.
4. Raises appropriate exceptions
5. Keeps object in valid state
""")
if __name__ == "__main__":
main()
@name.setter defines setter. Validate and raise exception if invalid.
Computed properties
Calculate values on demand.
# Computed Properties
import math
class Circle:
"""Circle with computed properties."""
def __init__(self, radius):
self._radius = radius
@property
def radius(self):
"""Get radius."""
return self._radius
@radius.setter
def radius(self, value):
"""Set radius."""
if value <= 0:
raise ValueError("Radius must be positive")
self._radius = value
@property
def diameter(self):
"""Computed: diameter = 2 * radius."""
return 2 * self._radius
@diameter.setter
def diameter(self, value):
"""Set diameter (updates radius)."""
self.radius = value / 2 # Uses radius setter
@property
def area(self):
"""Computed: area = π * r²."""
return math.pi * self._radius ** 2
@property
def circumference(self):
"""Computed: circumference = 2 * π * r."""
return 2 * math.pi * self._radius
class Temperature:
"""Temperature with Celsius/Fahrenheit conversion."""
def __init__(self, celsius=0):
self._celsius = celsius
@property
def celsius(self):
"""Get temperature in Celsius."""
return self._celsius
@celsius.setter
def celsius(self, value):
"""Set temperature in Celsius."""
if value < -273.15:
raise ValueError("Below absolute zero!")
self._celsius = value
@property
def fahrenheit(self):
"""Computed: convert to Fahrenheit."""
return self._celsius * 9/5 + 32
@fahrenheit.setter
def fahrenheit(self, value):
"""Set temperature in Fahrenheit (updates celsius)."""
self.celsius = (value - 32) * 5/9
@property
def kelvin(self):
"""Computed: convert to Kelvin."""
return self._celsius + 273.15
@kelvin.setter
def kelvin(self, value):
"""Set temperature in Kelvin (updates celsius)."""
self.celsius = value - 273.15
class Rectangle:
"""Rectangle with computed properties."""
def __init__(self, width, height):
self._width = width
self._height = height
@property
def width(self):
return self._width
@width.setter
def width(self, value):
if value <= 0:
raise ValueError("Width must be positive")
self._width = value
@property
def height(self):
return self._height
@height.setter
def height(self, value):
if value <= 0:
raise ValueError("Height must be positive")
self._height = value
@property
def area(self):
"""Computed: area = width * height."""
return self._width * self._height
@property
def perimeter(self):
"""Computed: perimeter = 2 * (width + height)."""
return 2 * (self._width + self._height)
@property
def aspect_ratio(self):
"""Computed: aspect ratio = width / height."""
return self._width / self._height
@property
def is_square(self):
"""Computed: check if it's a square."""
return self._width == self._height
def main():
print("=== Computed Properties ===\n")
# Circle with computed properties
print("--- Circle ---")
circle = Circle(5)
print(f"Radius: {circle.radius}")
print(f"Diameter: {circle.diameter}")
print(f"Area: {circle.area:.2f}")
print(f"Circumference: {circle.circumference:.2f}")
# Update via diameter
print("\n--- Update via Diameter ---")
circle.diameter = 20
print(f"New radius: {circle.radius}")
print(f"New area: {circle.area:.2f}")
# Temperature conversions
print("\n--- Temperature Conversions ---")
temp = Temperature(25)
print(f"Celsius: {temp.celsius}°C")
print(f"Fahrenheit: {temp.fahrenheit}°F")
print(f"Kelvin: {temp.kelvin}K")
# Set via Fahrenheit
print("\n--- Set via Fahrenheit ---")
temp.fahrenheit = 212
print(f"Celsius: {temp.celsius}°C")
print(f"Kelvin: {temp.kelvin}K")
# Rectangle computed properties
print("\n--- Rectangle ---")
rect = Rectangle(10, 5)
print(f"Dimensions: {rect.width} x {rect.height}")
print(f"Area: {rect.area}")
print(f"Perimeter: {rect.perimeter}")
print(f"Aspect ratio: {rect.aspect_ratio}")
print(f"Is square: {rect.is_square}")
# Make it a square
print("\n--- Make it a Square ---")
rect.width = 5
print(f"Dimensions: {rect.width} x {rect.height}")
print(f"Is square: {rect.is_square}")
print("\n=== Key Points ===")
print("""
1. Computed properties calculate values on-the-fly
2. No need to store derived values
3. Always consistent with source data
4. Can have setters that update source data
5. Useful for conversions, calculations, flags
""")
if __name__ == "__main__":
main()
# import_math
# import math
#
# For π and other math functions.
#
# init_circle
# def __init__(self, radius):
#
# Store only radius, compute rest.
#
# store_radius
# self._radius = radius
#
# Single source of truth.
#
# radius_property
# @property
#
# Radius getter.
#
# radius_getter
# def radius(self):
#
# Get radius.
#
# return_radius
# return self._radius
#
# Return stored radius.
#
# radius_setter
# @radius.setter
#
# Radius setter with validation.
#
# radius_setter_method
# def radius(self, value):
#
# Set radius.
#
# check_positive
# if value <= 0:
#
# Must be positive.
#
# raise_radius
# raise ValueError("Radius must be positive")
#
# Validation error.
#
# store_valid_radius
# self._radius = value
#
# Store valid radius.
#
# diameter_property
# @property
#
# Computed diameter.
#
# diameter_getter
# def diameter(self):
#
# Calculated from radius.
#
# calc_diameter
# return 2 * self._radius
#
# diameter = 2r
#
# diameter_setter
# @diameter.setter
#
# Can set diameter too.
#
# diameter_setter_method
# def diameter(self, value):
#
# Set diameter.
#
# set_radius_from_diameter
# self.radius = value / 2
#
# Updates radius (uses radius setter).
#
# area_property
# @property
#
# Computed area.
#
# area_getter
# def area(self):
#
# Calculate area.
#
# calc_area
# return math.pi * self._radius ** 2
#
# area = pi * r^2
#
# circumference_property
# @property
#
# Computed circumference.
#
# circumference_getter
# def circumference(self):
#
# Calculate circumference.
#
# calc_circumference
# return 2 * math.pi * self._radius
#
# circumference = 2 * pi * r
#
# init_temp
# def __init__(self, celsius=0):
#
# Store in Celsius.
#
# store_celsius
# self._celsius = celsius
#
# Single source (Celsius).
#
# celsius_property
# @property
#
# Celsius getter.
#
# celsius_getter
# def celsius(self):
#
# Get Celsius.
#
# return_celsius
# return self._celsius
#
# Return stored value.
#
# celsius_setter
# @celsius.setter
#
# Celsius setter.
#
# celsius_setter_method
# def celsius(self, value):
#
# Set Celsius.
#
# check_absolute_zero
# if value < -273.15:
#
# Check absolute zero.
#
# raise_absolute_zero
# raise ValueError("Below absolute zero!")
#
# Can't go below -273.15°C.
#
# store_valid_celsius
# self._celsius = value
#
# Store valid Celsius.
#
# fahrenheit_property
# @property
#
# Computed Fahrenheit.
#
# fahrenheit_getter
# def fahrenheit(self):
#
# Convert to Fahrenheit.
#
# calc_fahrenheit
# return self._celsius * 9/5 + 32
#
# F = C x 9/5 + 32
#
# fahrenheit_setter
# @fahrenheit.setter
#
# Can set via Fahrenheit.
#
# fahrenheit_setter_method
# def fahrenheit(self, value):
#
# Set Fahrenheit.
#
# set_celsius_from_fahrenheit
# self.celsius = (value - 32) * 5/9
#
# Convert to Celsius and set.
# C = (F - 32) x 5/9
#
# kelvin_property
# @property
#
# Computed Kelvin.
#
# kelvin_getter
# def kelvin(self):
#
# Convert to Kelvin.
#
# calc_kelvin
# return self._celsius + 273.15
#
# K = C + 273.15
#
# kelvin_setter
# @kelvin.setter
#
# Can set via Kelvin.
#
# kelvin_setter_method
# def kelvin(self, value):
#
# Set Kelvin.
#
# set_celsius_from_kelvin
# self.celsius = value - 273.15
#
# Convert to Celsius.
# C = K - 273.15
#
# init_rectangle
# def __init__(self, width, height):
#
# Store width and height.
#
# store_width
# self._width = width
#
# Store width.
#
# store_height
# self._height = height
#
# Store height.
#
# width_property
# @property
#
# Width getter.
#
# width_getter
# def width(self):
#
# Get width.
#
# return_width
# return self._width
#
# Return width.
#
# width_setter
# @width.setter
#
# Width setter.
#
# width_setter_method
# def width(self, value):
#
# Set width.
#
# check_width
# if value <= 0:
#
# Validate positive.
#
# raise_width
# raise ValueError("Width must be positive")
#
# Width error.
#
# store_valid_width
# self._width = value
#
# Store valid width.
#
# height_property
# @property
#
# Height getter.
#
# height_getter
# def height(self):
#
# Get height.
#
# return_height
# return self._height
#
# Return height.
#
# height_setter
# @height.setter
#
# Height setter.
#
# height_setter_method
# def height(self, value):
#
# Set height.
#
# check_height
# if value <= 0:
#
# Validate positive.
#
# raise_height
# raise ValueError("Height must be positive")
#
# Height error.
#
# store_valid_height
# self._height = value
#
# Store valid height.
#
# area_rect_property
# @property
#
# Computed area.
#
# area_rect_getter
# def area(self):
#
# Calculate area.
#
# calc_rect_area
# return self._width * self._height
#
# area = width x height
#
# perimeter_property
# @property
#
# Computed perimeter.
#
# perimeter_getter
# def perimeter(self):
#
# Calculate perimeter.
#
# calc_perimeter
# return 2 * (self._width + self._height)
#
# perimeter = 2(w + h)
#
# aspect_ratio_property
# @property
#
# Computed aspect ratio.
#
# aspect_ratio_getter
# def aspect_ratio(self):
#
# Calculate aspect ratio.
#
# calc_aspect_ratio
# return self._width / self._height
#
# ratio = width / height
#
# is_square_property
# @property
#
# Boolean computed property.
#
# is_square_getter
# def is_square(self):
#
# Check if square.
#
# check_square
# return self._width == self._height
#
# True if width == height.
#
# circle_demo
# Circle with computed properties
#
# Demonstrate computed properties.
#
# create_circle
# circle = Circle(5)
#
# Radius = 5
#
# print_radius
# print(f"Radius: {circle.radius}")
#
# "Radius: 5"
#
# print_diameter
# print(f"Diameter: {circle.diameter}")
#
# "Diameter: 10" (computed: 2 x 5)
#
# print_area
# print(f"Area: {circle.area:.2f}")
#
# "Area: 78.54" (computed: pi * 5^2)
#
# print_circumference
# print(f"Circumference: {circle.circumference:.2f}")
#
# "Circumference: 31.42" (computed: 2*pi * 5)
#
# update_diameter
# Update via diameter
#
# Set diameter, radius updates.
#
# set_diameter
# circle.diameter = 20
#
# Sets radius to 10.
#
# print_new_radius
# print(f"New radius: {circle.radius}")
#
# "New radius: 10"
#
# print_new_area
# print(f"New area: {circle.area:.2f}")
#
# "New area: 314.16" (pi * 10^2)
#
# temp_demo
# Temperature conversions
#
# Multiple unit support.
#
# create_temp
# temp = Temperature(25)
#
# 25°C
#
# print_celsius
# print(f"Celsius: {temp.celsius}°C")
#
# "Celsius: 25°C"
#
# print_fahrenheit
# print(f"Fahrenheit: {temp.fahrenheit}°F")
#
# "Fahrenheit: 77.0°F" (computed)
#
# print_kelvin
# print(f"Kelvin: {temp.kelvin}K")
#
# "Kelvin: 298.15K" (computed)
#
# set_fahrenheit
# Set via Fahrenheit
#
# Set in different unit.
#
# set_boiling
# temp.fahrenheit = 212
#
# Boiling point of water.
#
# print_boiling_celsius
# print(f"Celsius: {temp.celsius}°C")
#
# "Celsius: 100.0°C"
#
# print_boiling_kelvin
# print(f"Kelvin: {temp.kelvin}K")
#
# "Kelvin: 373.15K"
#
# rectangle_demo
# Rectangle computed properties
#
# Multiple computed properties.
#
# create_rect
# rect = Rectangle(10, 5)
#
# 10 x 5 rectangle.
#
# print_dimensions
# print(f"Dimensions: {rect.width} x {rect.height}")
#
# "Dimensions: 10 x 5"
#
# print_rect_area
# print(f"Area: {rect.area}")
#
# "Area: 50"
#
# print_perimeter
# print(f"Perimeter: {rect.perimeter}")
#
# "Perimeter: 30"
#
# print_aspect_ratio
# print(f"Aspect ratio: {rect.aspect_ratio}")
#
# "Aspect ratio: 2.0"
#
# print_is_square
# print(f"Is square: {rect.is_square}")
#
# "Is square: False"
#
# make_square
# Make it a square
#
# Update to square.
#
# set_width_5
# rect.width = 5
#
# Now 5 x 5.
#
# print_square_dimensions
# print(f"Dimensions: {rect.width} x {rect.height}")
#
# "Dimensions: 5 x 5"
#
# print_is_now_square
# print(f"Is square: {rect.is_square}")
#
# "Is square: True"
#
area computed from radius. Always up-to-date, no storage needed.
Read-only properties
Prevent modification after creation.
# Read-only Properties
from datetime import datetime
class Person:
"""Person with read-only birth_year."""
def __init__(self, name, birth_year):
self._name = name
self._birth_year = birth_year
@property
def birth_year(self):
"""Read-only birth year."""
return self._birth_year
# No @birth_year.setter - read-only!
@property
def age(self):
"""Computed age based on birth year."""
current_year = datetime.now().year
return current_year - self._birth_year
class ImmutablePoint:
"""Point with read-only coordinates."""
def __init__(self, x, y):
self._x = x
self._y = y
@property
def x(self):
"""Read-only x coordinate."""
return self._x
@property
def y(self):
"""Read-only y coordinate."""
return self._y
@property
def distance_from_origin(self):
"""Computed distance from origin."""
return (self._x ** 2 + self._y ** 2) ** 0.5
class BankAccount:
"""Bank account with read-only account number."""
def __init__(self, account_number, balance=0):
self._account_number = account_number
self._balance = balance
self._transactions = []
@property
def account_number(self):
"""Read-only account number."""
return self._account_number
@property
def balance(self):
"""Read-only balance (use deposit/withdraw)."""
return self._balance
@property
def transactions(self):
"""Read-only copy of transactions."""
return self._transactions.copy() # Return copy, not original!
def deposit(self, amount):
"""Deposit money."""
if amount <= 0:
raise ValueError("Deposit must be positive")
self._balance += amount
self._transactions.append(f"Deposit: +${amount}")
def withdraw(self, amount):
"""Withdraw money."""
if amount <= 0:
raise ValueError("Withdrawal must be positive")
if amount > self._balance:
raise ValueError("Insufficient funds")
self._balance -= amount
self._transactions.append(f"Withdrawal: -${amount}")
class Counter:
"""Counter with read-only count."""
def __init__(self):
self._count = 0
@property
def count(self):
"""Read-only count."""
return self._count
def increment(self):
"""Increment counter."""
self._count += 1
def reset(self):
"""Reset counter."""
self._count = 0
def main():
print("=== Read-only Properties ===\n")
# Person with read-only birth year
print("--- Person with Read-only Birth Year ---")
person = Person("Alice", 1990)
print(f"Name: {person._name}")
print(f"Birth year: {person.birth_year}")
print(f"Age: {person.age}")
try:
person.birth_year = 1995 # Attempt to modify
except AttributeError as e:
print(f"Error: {e}")
# Immutable point
print("\n--- Immutable Point ---")
point = ImmutablePoint(3, 4)
print(f"Point: ({point.x}, {point.y})")
print(f"Distance from origin: {point.distance_from_origin}")
try:
point.x = 10
except AttributeError as e:
print(f"Error: {e}")
# Bank account
print("\n--- Bank Account ---")
account = BankAccount("12345", 1000)
print(f"Account: {account.account_number}")
print(f"Balance: ${account.balance}")
account.deposit(500)
account.withdraw(200)
print(f"New balance: ${account.balance}")
print("Transactions:")
for transaction in account.transactions:
print(f" - {transaction}")
try:
account.balance = 999999 # Attempt to cheat
except AttributeError as e:
print(f"Error: {e}")
# Counter
print("\n--- Counter ---")
counter = Counter()
print(f"Initial count: {counter.count}")
counter.increment()
counter.increment()
counter.increment()
print(f"After increments: {counter.count}")
try:
counter.count = 100
except AttributeError as e:
print(f"Error: {e}")
counter.reset()
print(f"After reset: {counter.count}")
print("\n=== Key Points ===")
print("""
1. Property without setter is read-only
2. Attempting to set raises AttributeError
3. Useful for IDs, computed values, immutable data
4. Provide methods (deposit/withdraw) instead of direct access
5. Return copies of mutable internal data
""")
if __name__ == "__main__":
main()
# import_datetime
# from datetime import datetime
#
# For calculating current year.
#
# init_person
# def __init__(self, name, birth_year):
#
# Store name and birth year.
#
# store_name
# self._name = name
#
# Store name.
#
# store_birth_year
# self._birth_year = birth_year
#
# Store birth year (immutable).
#
# birth_year_property
# @property
#
# Birth year getter only.
#
# birth_year_getter
# def birth_year(self):
#
# Get birth year.
#
# return_birth_year
# return self._birth_year
#
# Return birth year.
#
# no_setter
# No @birth_year.setter - read-only!
#
# No setter = read-only property.
#
# age_property
# @property
#
# Computed age.
#
# age_getter
# def age(self):
#
# Calculate current age.
#
# get_current_year
# current_year = datetime.now().year
#
# Get current year.
#
# calc_age
# return current_year - self._birth_year
#
# Calculate age.
#
# init_point
# def __init__(self, x, y):
#
# Store coordinates.
#
# store_x
# self._x = x
#
# Store x.
#
# store_y
# self._y = y
#
# Store y.
#
# x_property
# @property
#
# Read-only x.
#
# x_getter
# def x(self):
#
# Get x.
#
# return_x
# return self._x
#
# Return x.
#
# y_property
# @property
#
# Read-only y.
#
# y_getter
# def y(self):
#
# Get y.
#
# return_y
# return self._y
#
# Return y.
#
# distance_property
# @property
#
# Computed distance.
#
# distance_getter
# def distance_from_origin(self):
#
# Calculate distance from (0, 0).
#
# calc_distance
# return (self._x ** 2 + self._y ** 2) ** 0.5
#
# Pythagorean theorem.
#
# init_account
# def __init__(self, account_number, balance=0):
#
# Bank account constructor.
#
# store_account_number
# self._account_number = account_number
#
# Immutable account number.
#
# store_balance
# self._balance = balance
#
# Private balance.
#
# init_transactions
# self._transactions = []
#
# Transaction log.
#
# account_number_property
# @property
#
# Read-only account number.
#
# account_number_getter
# def account_number(self):
#
# Get account number.
#
# return_account_number
# return self._account_number
#
# Return account number.
#
# balance_property
# @property
#
# Read-only balance.
#
# balance_getter
# def balance(self):
#
# Get balance (no setter).
#
# return_balance
# return self._balance
#
# Return balance.
#
# transactions_property
# @property
#
# Read-only transactions.
#
# transactions_getter
# def transactions(self):
#
# Get transactions list.
#
# return_copy
# return self._transactions.copy()
#
# Return copy, not original!
# Prevents: account.transactions.append(...)
#
# deposit_method
# def deposit(self, amount):
#
# Method to modify balance.
#
# check_deposit_amount
# if amount <= 0:
#
# Validate positive.
#
# raise_deposit
# raise ValueError("Deposit must be positive")
#
# Reject non-positive.
#
# add_to_balance
# self._balance += amount
#
# Update balance.
#
# log_deposit
# self._transactions.append(f"Deposit: +${amount}")
#
# Log transaction.
#
# withdraw_method
# def withdraw(self, amount):
#
# Method to withdraw.
#
# check_withdraw_amount
# if amount <= 0:
#
# Validate positive.
#
# raise_withdraw_amount
# raise ValueError("Withdrawal must be positive")
#
# Reject non-positive.
#
# check_sufficient_funds
# if amount > self._balance:
#
# Check sufficient funds.
#
# raise_insufficient
# raise ValueError("Insufficient funds")
#
# Reject overdraft.
#
# subtract_from_balance
# self._balance -= amount
#
# Update balance.
#
# log_withdrawal
# self._transactions.append(f"Withdrawal: -${amount}")
#
# Log transaction.
#
# init_counter
# def __init__(self):
#
# Counter constructor.
#
# init_count
# self._count = 0
#
# Initialize to 0.
#
# count_property
# @property
#
# Read-only count.
#
# count_getter
# def count(self):
#
# Get count.
#
# return_count
# return self._count
#
# Return count.
#
# increment_method
# def increment(self):
#
# Method to increment.
#
# do_increment
# self._count += 1
#
# Increment count.
#
# reset_method
# def reset(self):
#
# Method to reset.
#
# do_reset
# self._count = 0
#
# Reset to 0.
#
# person_demo
# Person with read-only birth year
#
# Demonstrate read-only property.
#
# create_person
# person = Person("Alice", 1990)
#
# Create person.
#
# print_name
# print(f"Name: {person._name}")
#
# Access private attribute directly.
#
# print_birth_year
# print(f"Birth year: {person.birth_year}")
#
# Via property.
#
# print_age
# print(f"Age: {person.age}")
#
# Computed property.
#
# try_modify_birth_year
# try:
#
# Try to modify.
#
# attempt_modify
# person.birth_year = 1995
#
# Attempt to set read-only property.
#
# catch_attribute_error
# except AttributeError as e:
#
# Raised when no setter.
#
# print_readonly_error
# print(f"Error: {e}")
#
# "Error: can't set attribute"
#
# point_demo
# Immutable point
#
# Read-only coordinates.
#
# create_point
# point = ImmutablePoint(3, 4)
#
# Create point at (3, 4).
#
# print_point
# print(f"Point: ({point.x}, {point.y})")
#
# "Point: (3, 4)"
#
# print_distance
# print(f"Distance from origin: {point.distance_from_origin}")
#
# "Distance from origin: 5.0"
#
# try_modify_point
# try:
#
# Try to modify.
#
# attempt_modify_x
# point.x = 10
#
# Attempt to set x.
#
# catch_point_error
# except AttributeError as e:
#
# No setter for x.
#
# print_point_error
# print(f"Error: {e}")
#
# "Error: can't set attribute"
#
# account_demo
# Bank account
#
# Read-only balance and account number.
#
# create_account
# account = BankAccount("12345", 1000)
#
# Create account.
#
# print_account_number
# print(f"Account: {account.account_number}")
#
# "Account: 12345"
#
# print_balance
# print(f"Balance: ${account.balance}")
#
# "Balance: $1000"
#
# do_deposit
# account.deposit(500)
#
# Add $500.
#
# do_withdraw
# account.withdraw(200)
#
# Withdraw $200.
#
# print_new_balance
# print(f"New balance: ${account.balance}")
#
# "New balance: $1300"
#
# print_transactions_header
# print("Transactions:")
#
# Show transactions.
#
# loop_transactions
# for transaction in account.transactions:
#
# Loop through copy.
#
# print_transaction
# print(f" - {transaction}")
#
# - Deposit: +$500
# - Withdrawal: -$200
#
# try_modify_balance
# try:
#
# Try to cheat.
#
# attempt_modify_balance
# account.balance = 999999
#
# Attempt to set balance directly.
#
# catch_balance_error
# except AttributeError as e:
#
# No setter for balance.
#
# print_balance_error
# print(f"Error: {e}")
#
# "Error: can't set attribute"
#
# counter_demo
# Counter
#
# Read-only count.
#
# create_counter
# counter = Counter()
#
# Create counter.
#
# print_initial_count
# print(f"Initial count: {counter.count}")
#
# "Initial count: 0"
#
# inc1
# counter.increment()
#
# Count: 1
#
# inc2
# counter.increment()
#
# Count: 2
#
# inc3
# counter.increment()
#
# Count: 3
#
# print_after_increments
# print(f"After increments: {counter.count}")
#
# "After increments: 3"
#
# try_modify_count
# try:
#
# Try to modify.
#
# attempt_modify_count
# counter.count = 100
#
# Attempt to set count.
#
# catch_count_error
# except AttributeError as e:
#
# No setter for count.
#
# print_count_error
# print(f"Error: {e}")
#
# "Error: can't set attribute"
#
# do_reset
# counter.reset()
#
# Reset to 0.
#
# print_after_reset
# print(f"After reset: {counter.count}")
#
# "After reset: 0"
#
Provide getter without setter. Assignment raises AttributeError.
Migrating to properties
Change implementation without breaking API.
# Migrating from Direct Attributes to Properties
# Version 1: Direct attribute access (old way)
class UserV1:
"""Original version with direct attribute access."""
def __init__(self, username, email):
self.username = username # Direct attribute
self.email = email # Direct attribute
# Version 2: Using properties (new way - backward compatible!)
class UserV2:
"""Refactored with properties - same interface!"""
def __init__(self, username, email):
self.username = username # Uses setter
self.email = email # Uses setter
@property
def username(self):
"""Get username."""
return self._username
@username.setter
def username(self, value):
"""Set username with validation (NEW!)."""
if not isinstance(value, str) or len(value) < 3:
raise ValueError("Username must be string with 3+ chars")
self._username = value
@property
def email(self):
"""Get email."""
return self._email
@email.setter
def email(self, value):
"""Set email with validation (NEW!)."""
if "@" not in value:
raise ValueError("Invalid email format")
self._email = value.lower() # Normalize to lowercase (NEW!)
# Old API - direct access
class LegacyCart:
"""Old shopping cart with direct access."""
def __init__(self):
self.items = []
self.total = 0
def add_item(self, price):
"""Add item (user must update total manually - error-prone!)."""
self.items.append(price)
# User responsible for updating total!
# New API - using properties
class ModernCart:
"""Modernized cart with computed total."""
def __init__(self):
self._items = []
@property
def items(self):
"""Get items (read-only copy)."""
return self._items.copy()
@property
def total(self):
"""Computed total (always accurate!)."""
return sum(self._items)
def add_item(self, price):
"""Add item (total auto-updates!)."""
if price < 0:
raise ValueError("Price cannot be negative")
self._items.append(price)
# No manual total update needed!
def main():
print("=== Migrating to Properties ===\n")
# Version 1: Direct attribute access
print("--- Version 1: Direct Attributes ---")
user_v1 = UserV1("alice", "ALICE@EXAMPLE.COM")
print(f"Username: {user_v1.username}")
print(f"Email: {user_v1.email}")
# Problems with V1:
user_v1.username = "ab" # No validation - BAD DATA!
user_v1.email = "not-an-email" # No validation - BAD DATA!
print(f"Invalid username accepted: {user_v1.username}")
print(f"Invalid email accepted: {user_v1.email}")
# Version 2: Properties (backward compatible interface!)
print("\n--- Version 2: Properties ---")
user_v2 = UserV2("alice", "ALICE@EXAMPLE.COM")
print(f"Username: {user_v2.username}") # Same interface!
print(f"Email: {user_v2.email}") # But normalized!
# Now with validation:
try:
user_v2.username = "ab" # Now validated!
except ValueError as e:
print(f"Validation error: {e}")
# Legacy cart problems
print("\n--- Legacy Cart (Manual Total) ---")
legacy = LegacyCart()
legacy.add_item(10.00)
legacy.add_item(20.00)
print(f"Items: {legacy.items}")
print(f"Total: ${legacy.total}") # Still 0 - forgot to update!
# Forgot to update total!
legacy.total = 30.00 # Manual and error-prone
print(f"After manual update: ${legacy.total}")
# Modern cart with computed property
print("\n--- Modern Cart (Computed Total) ---")
modern = ModernCart()
modern.add_item(10.00)
modern.add_item(20.00)
print(f"Items: {modern.items}")
print(f"Total: ${modern.total}") # Always correct!
modern.add_item(15.00)
print(f"After adding item: ${modern.total}") # Auto-updated!
# Can't accidentally set wrong total
try:
modern.total = 9999
except AttributeError as e:
print(f"Error: {e}")
print("\n=== Migration Benefits ===")
print("""
1. Backward compatible - same interface
2. Add validation without breaking existing code
3. Transform data (e.g., normalize email)
4. Computed values always accurate
5. Prevent incorrect manual updates
6. Read-only computed properties
""")
if __name__ == "__main__":
main()
# version1
# Version 1: Direct attribute access (old way)
#
# Original implementation.
#
# init_v1
# def __init__(self, username, email):
#
# V1 constructor.
#
# direct_username
# self.username = username
#
# Public attribute (no validation).
#
# direct_email
# self.email = email
#
# Public attribute (no normalization).
#
# version2
# Version 2: Using properties (new way - backward compatible!)
#
# Refactored with properties.
# Same interface as V1!
#
# init_v2
# def __init__(self, username, email):
#
# V2 constructor.
#
# prop_username_init
# self.username = username
#
# Triggers username setter (validates!).
#
# prop_email_init
# self.email = email
#
# Triggers email setter (validates & normalizes!).
#
# username_property_v2
# @property
#
# Username property.
#
# username_getter_v2
# def username(self):
#
# Get username.
#
# return_username_v2
# return self._username
#
# Return stored value.
#
# username_setter_v2
# @username.setter
#
# Username setter with validation.
#
# username_setter_method_v2
# def username(self, value):
#
# Set username.
#
# validate_username_v2
# if not isinstance(value, str) or len(value) < 3:
#
# NEW: Validate input.
#
# raise_username_v2
# raise ValueError("Username must be string with 3+ chars")
#
# NEW: Reject invalid.
#
# store_username_v2
# self._username = value
#
# Store valid value.
#
# email_property_v2
# @property
#
# Email property.
#
# email_getter_v2
# def email(self):
#
# Get email.
#
# return_email_v2
# return self._email
#
# Return stored email.
#
# email_setter_v2
# @email.setter
#
# Email setter.
#
# email_setter_method_v2
# def email(self, value):
#
# Set email.
#
# validate_email_v2
# if "@" not in value:
#
# NEW: Validate format.
#
# raise_email_v2
# raise ValueError("Invalid email format")
#
# NEW: Reject invalid.
#
# store_email_v2
# self._email = value.lower()
#
# NEW: Normalize to lowercase!
#
# old_api
# Old API - direct access
#
# Legacy cart with manual total.
#
# init_legacy_cart
# def __init__(self):
#
# Legacy constructor.
#
# direct_items
# self.items = []
#
# Public items list.
#
# direct_total
# self.total = 0
#
# Public total (manual).
#
# add_item_legacy
# def add_item(self, price):
#
# Add item method.
#
# append_item
# self.items.append(price)
#
# Add to items.
#
# manual_total
# User responsible for updating total!
#
# Error-prone!
#
# new_api
# New API - using properties
#
# Modern cart with computed total.
#
# init_modern_cart
# def __init__(self):
#
# Modern constructor.
#
# private_items
# self._items = []
#
# Private items list.
#
# items_property
# @property
#
# Items property (read-only).
#
# items_getter
# def items(self):
#
# Get items.
#
# return_items_copy
# return self._items.copy()
#
# Return copy (prevent modification).
#
# total_property
# @property
#
# Computed total.
#
# total_getter
# def total(self):
#
# Calculate total.
#
# calc_total
# return sum(self._items)
#
# Always accurate!
#
# add_item_modern
# def add_item(self, price):
#
# Add item (modern).
#
# validate_price
# if price < 0:
#
# Validate price.
#
# raise_price
# raise ValueError("Price cannot be negative")
#
# Reject negative.
#
# append_modern_item
# self._items.append(price)
#
# Add to items.
#
# auto_total
# No manual total update needed!
#
# Total auto-computed.
#
# demo_v1
# Version 1: Direct attribute access
#
# Show V1 problems.
#
# create_v1
# user_v1 = UserV1("alice", "ALICE@EXAMPLE.COM")
#
# Create V1 user.
#
# print_v1_username
# print(f"Username: {user_v1.username}")
#
# "Username: alice"
#
# print_v1_email
# print(f"Email: {user_v1.email}")
#
# "Email: ALICE@EXAMPLE.COM" (not normalized)
#
# v1_problems
# Problems with V1:
#
# No validation or normalization.
#
# set_invalid_v1
# user_v1.username = "ab"
#
# Too short but accepted!
#
# set_invalid_email_v1
# user_v1.email = "not-an-email"
#
# Invalid but accepted!
#
# print_invalid_v1
# print(f"Invalid username accepted: {user_v1.username}")
#
# "Invalid username accepted: ab"
#
# print_invalid_email_v1
# print(f"Invalid email accepted: {user_v1.email}")
#
# "Invalid email accepted: not-an-email"
#
# demo_v2
# Version 2: Properties (backward compatible interface!)
#
# Show V2 improvements.
#
# create_v2
# user_v2 = UserV2("alice", "ALICE@EXAMPLE.COM")
#
# Create V2 user.
#
# print_v2_username
# print(f"Username: {user_v2.username}")
#
# Same interface as V1!
# "Username: alice"
#
# print_v2_email
# print(f"Email: {user_v2.email}")
#
# "Email: alice@example.com" (normalized!)
#
# v2_validation
# Now with validation:
#
# Properties add validation.
#
# try_invalid_v2
# try:
#
# Try invalid data.
#
# set_invalid_v2
# user_v2.username = "ab"
#
# Now rejected!
#
# catch_v2_error
# except ValueError as e:
#
# Validation error.
#
# print_v2_error
# print(f"Validation error: {e}")
#
# "Validation error: Username must be string with 3+ chars"
#
# legacy_cart_demo
# Legacy cart problems
#
# Manual total is error-prone.
#
# create_legacy
# legacy = LegacyCart()
#
# Create legacy cart.
#
# add_legacy_1
# legacy.add_item(10.00)
#
# Add $10 item.
#
# add_legacy_2
# legacy.add_item(20.00)
#
# Add $20 item.
#
# print_legacy_items
# print(f"Items: {legacy.items}")
#
# "Items: [10.0, 20.0]"
#
# print_legacy_total
# print(f"Total: ${legacy.total}")
#
# "Total: $0" - WRONG! Forgot to update.
#
# forgot_total
# Forgot to update total!
#
# Common bug with manual total.
#
# manual_total_update
# legacy.total = 30.00
#
# Manual update (error-prone).
#
# print_manual_total
# print(f"After manual update: ${legacy.total}")
#
# "After manual update: $30.0"
#
# modern_cart_demo
# Modern cart with computed property
#
# Always accurate.
#
# create_modern
# modern = ModernCart()
#
# Create modern cart.
#
# add_modern_1
# modern.add_item(10.00)
#
# Add $10.
#
# add_modern_2
# modern.add_item(20.00)
#
# Add $20.
#
# print_modern_items
# print(f"Items: {modern.items}")
#
# "Items: [10.0, 20.0]"
#
# print_modern_total
# print(f"Total: ${modern.total}")
#
# "Total: $30.0" - Always correct!
#
# add_modern_3
# modern.add_item(15.00)
#
# Add $15.
#
# print_updated_total
# print(f"After adding item: ${modern.total}")
#
# "After adding item: $45.0" - Auto-updated!
#
# cant_set_total
# Can't accidentally set wrong total
#
# Read-only property.
#
# try_set_total
# try:
#
# Try to cheat.
#
# attempt_set_total
# modern.total = 9999
#
# No setter!
#
# catch_total_error
# except AttributeError as e:
#
# Can't set.
#
# print_total_error
# print(f"Error: {e}")
#
# "Error: can't set attribute"
#
Start with plain attribute. Add property later. Callers don't change.
Exercise: practical.py
Build a temperature class with Celsius/Fahrenheit properties