Object-Oriented Basics
Encapsulation
Controlled Access
Your BankAccount class has a balance. If code can set account.balance = -1000
directly, bugs happen. Encapsulation hides internal data and provides controlled
access through methods and properties - protecting your data from misuse.
Public vs private conventions
Python uses naming conventions for access control.
# Public vs Private Attributes
print("=== Public Attributes ===\n")
class Dog:
def __init__(self, name, age):
self.name = name
self.age = age
buddy = Dog("Buddy", 3)
# Direct access - anyone can read/write
print(f"Name: {buddy.name}")
buddy.age = 4 # Changed directly
print(f"Age: {buddy.age}")
# Problem: No validation!
buddy.age = -5 # Oops! Invalid value
print(f"Invalid age: {buddy.age}")
print("\n=== Protected Attributes (Convention) ===")
class Cat:
def __init__(self, name, age):
self._name = name
self._age = age
def get_info(self):
return f"{self._name} is {self._age} years old"
whiskers = Cat("Whiskers", 5)
print(whiskers.get_info())
# Still accessible, but indicates "internal use"
print(f"Direct access: {whiskers._name}") # Works, but discouraged
whiskers._age = 6 # Works, but breaks convention
print("\n=== Private Attributes (Name Mangling) ===")
class BankAccount:
def __init__(self, balance):
self.__balance = balance
def get_balance(self):
return self.__balance
def deposit(self, amount):
if amount > 0:
self.__balance += amount
return True
return False
account = BankAccount(1000)
print(f"Balance: {account.get_balance()}")
# Can't access directly
# print(account.__balance) # AttributeError!
# Name mangling - still accessible but harder
print(f"Mangled name: {account._BankAccount__balance}") # Works!
print("\n=== Why Use Privacy? ===")
class Counter:
def __init__(self):
self.__count = 0
self.__min = 0
self.__max = 100
def increment(self):
if self.__count < self.__max:
self.__count += 1
def decrement(self):
if self.__count > self.__min:
self.__count -= 1
def value(self):
return self.__count
counter = Counter()
counter.increment()
counter.increment()
counter.increment()
print(f"Count: {counter.value()}")
# Can't bypass validation
# counter.__count = 999 # Creates new attribute, not the real one!
counter.increment()
print(f"Count: {counter.value()}") # Still 4, not 999
print("\n=== Access Summary ===")
class Example:
def __init__(self):
self.public = "Anyone can access"
self._protected = "Convention: internal"
self.__private = "Name mangled"
obj = Example()
print(f"public: {obj.public}")
print(f"_protected: {obj._protected}") # Accessible
# print(f"__private: {obj.__private}") # Error!
print(f"_Example__private: {obj._Example__private}") # Name mangled
_name means "internal". __name means "strongly private" (name mangling).
Traditional getters and setters
Methods to read and write private data.
# Traditional Getters and Setters
print("=== Basic Getter and Setter ===\n")
class Person:
def __init__(self, name, age):
self._name = name
self._age = age
def get_name(self):
return self._name
def set_name(self, name):
if len(name) >= 2:
self._name = name
else:
print("Name too short!")
def get_age(self):
return self._age
def set_age(self, age):
if 0 <= age <= 150:
self._age = age
else:
print(f"Invalid age: {age}")
person = Person("Alice", 30)
# Using getters
print(f"Name: {person.get_name()}")
print(f"Age: {person.get_age()}")
# Using setters
person.set_name("Bob")
person.set_age(25)
print(f"Updated: {person.get_name()}, {person.get_age()}")
# Validation prevents bad values
person.set_age(-5) # Rejected
person.set_name("X") # Rejected
print(f"Still: {person.get_name()}, {person.get_age()}")
print("\n=== Getter with Logic ===")
class Temperature:
def __init__(self, celsius):
self._celsius = celsius
def get_celsius(self):
return self._celsius
def get_fahrenheit(self):
return self._celsius * 9/5 + 32
def get_kelvin(self):
return self._celsius + 273.15
def set_celsius(self, value):
if value >= -273.15: # Absolute zero
self._celsius = value
else:
print("Below absolute zero!")
temp = Temperature(25)
print(f"Celsius: {temp.get_celsius()}")
print(f"Fahrenheit: {temp.get_fahrenheit()}")
print(f"Kelvin: {temp.get_kelvin()}")
print("\n=== Setter with Side Effects ===")
class Logger:
def __init__(self):
self._log_level = "INFO"
self._changes = []
def get_log_level(self):
return self._log_level
def set_log_level(self, level):
valid_levels = ["DEBUG", "INFO", "WARNING", "ERROR"]
if level.upper() in valid_levels:
old = self._log_level
self._log_level = level.upper()
self._changes.append(f"{old} -> {self._log_level}")
print(f"Log level changed: {old} -> {self._log_level}")
else:
print(f"Invalid level: {level}")
def get_history(self):
return self._changes.copy()
logger = Logger()
logger.set_log_level("DEBUG")
logger.set_log_level("ERROR")
logger.set_log_level("invalid") # Rejected
print(f"History: {logger.get_history()}")
print("\n=== Why This Style is Verbose ===")
class Point:
def __init__(self, x, y):
self._x = x
self._y = y
def get_x(self): return self._x
def set_x(self, x): self._x = x
def get_y(self): return self._y
def set_y(self, y): self._y = y
p = Point(3, 4)
# Verbose usage:
x = p.get_x()
y = p.get_y()
p.set_x(x + 1)
print(f"Point: ({p.get_x()}, {p.get_y()})")
print("""
Traditional getters/setters are verbose:
p.get_x() instead of p.x
p.set_x(5) instead of p.x = 5
Python offers @property for cleaner syntax!
""")
get_balance() and set_balance() control access. Java-style.
Python properties
The Pythonic way to control attribute access.
# Python Properties
print("=== Basic @property ===\n")
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self):
"""Get the radius."""
return self._radius
@radius.setter
def radius(self, value):
"""Set the radius with validation."""
if value > 0:
self._radius = value
else:
print(f"Invalid radius: {value}")
circle =
# Access like attribute
print(f"Radius: {circle.radius}") # Calls getter
# Assign like attribute
circle.radius = 10 # Calls setter
print(f"New radius: {circle.radius}")
# Validation still works
circle.radius = -5 # Rejected
print(f"Still: {circle.radius}")
print("\n=== Properties vs Direct Access ===")
# Compare syntax:
print("""
Traditional:
value = obj.get_attribute()
obj.set_attribute(value)
With @property:
value = obj.attribute
obj.attribute = value
Same control, cleaner syntax!
""")
print("=== Multiple Properties ===")
class Rectangle:
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:
self._width = value
@property
def height(self):
return self._height
@height.setter
def height(self, value):
if value > 0:
self._height = value
@property
def area(self):
"""Computed property - no setter."""
return self._width * self._height
@property
def perimeter(self):
return 2 * (self._width + self._height)
rect =
print(f"Size: {rect.width} x {rect.height}")
print(f"Area: {rect.area}")
print(f"Perimeter: {rect.perimeter}")
# Change dimensions
rect.width = 10
print(f"\nAfter resize:")
print(f"Size: {rect.width} x {rect.height}")
print(f"Area: {rect.area}") # Automatically updated!
print("\n=== Property with Deleter ===")
class User:
def __init__(self, email):
self._email = email
@property
def email(self):
return self._email
@email.setter
def email(self, value):
if "@" in value:
self._email = value
@email.deleter
def email(self):
print("Removing email...")
self._email = None
user = User("alice@example.com")
print(f"Email: {user.email}")
del user.email
print(f"After delete: {user.email}")
print("\n=== Property Naming Patterns ===")
class Temperature:
def __init__(self, celsius):
self._celsius = celsius
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value >= -273.15:
self._celsius = value
@property
def fahrenheit(self):
return self._celsius * 9/5 + 32
@fahrenheit.setter
def fahrenheit(self, value):
self._celsius = (value - 32) * 5/9
temp = Temperature(0)
print(f"0°C = {temp.fahrenheit}°F")
temp.fahrenheit = 212 # Set via Fahrenheit
print(f"{temp.fahrenheit}°F = {temp.celsius}°C")
# Python Properties
print("=== Basic @property ===\n")
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self):
"""Get the radius."""
return self._radius
@radius.setter
def radius(self, value):
"""Set the radius with validation."""
if value > 0:
self._radius = value
else:
print(f"Invalid radius: {value}")
circle =
# Access like attribute
print(f"Radius: {circle.radius}") # Calls getter
# Assign like attribute
circle.radius = 10 # Calls setter
print(f"New radius: {circle.radius}")
# Validation still works
circle.radius = -5 # Rejected
print(f"Still: {circle.radius}")
print("\n=== Properties vs Direct Access ===")
# Compare syntax:
print("""
Traditional:
value = obj.get_attribute()
obj.set_attribute(value)
With @property:
value = obj.attribute
obj.attribute = value
Same control, cleaner syntax!
""")
print("=== Multiple Properties ===")
class Rectangle:
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:
self._width = value
@property
def height(self):
return self._height
@height.setter
def height(self, value):
if value > 0:
self._height = value
@property
def area(self):
"""Computed property - no setter."""
return self._width * self._height
@property
def perimeter(self):
return 2 * (self._width + self._height)
rect =
print(f"Size: {rect.width} x {rect.height}")
print(f"Area: {rect.area}")
print(f"Perimeter: {rect.perimeter}")
# Change dimensions
rect.width = 10
print(f"\nAfter resize:")
print(f"Size: {rect.width} x {rect.height}")
print(f"Area: {rect.area}") # Automatically updated!
print("\n=== Property with Deleter ===")
class User:
def __init__(self, email):
self._email = email
@property
def email(self):
return self._email
@email.setter
def email(self, value):
if "@" in value:
self._email = value
@email.deleter
def email(self):
print("Removing email...")
self._email = None
user = User("alice@example.com")
print(f"Email: {user.email}")
del user.email
print(f"After delete: {user.email}")
print("\n=== Property Naming Patterns ===")
class Temperature:
def __init__(self, celsius):
self._celsius = celsius
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value >= -273.15:
self._celsius = value
@property
def fahrenheit(self):
return self._celsius * 9/5 + 32
@fahrenheit.setter
def fahrenheit(self, value):
self._celsius = (value - 32) * 5/9
temp = Temperature(0)
print(f"0°C = {temp.fahrenheit}°F")
temp.fahrenheit = 212 # Set via Fahrenheit
print(f"{temp.fahrenheit}°F = {temp.celsius}°C")
# Python Properties
print("=== Basic @property ===\n")
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self):
"""Get the radius."""
return self._radius
@radius.setter
def radius(self, value):
"""Set the radius with validation."""
if value > 0:
self._radius = value
else:
print(f"Invalid radius: {value}")
circle =
# Access like attribute
print(f"Radius: {circle.radius}") # Calls getter
# Assign like attribute
circle.radius = 10 # Calls setter
print(f"New radius: {circle.radius}")
# Validation still works
circle.radius = -5 # Rejected
print(f"Still: {circle.radius}")
print("\n=== Properties vs Direct Access ===")
# Compare syntax:
print("""
Traditional:
value = obj.get_attribute()
obj.set_attribute(value)
With @property:
value = obj.attribute
obj.attribute = value
Same control, cleaner syntax!
""")
print("=== Multiple Properties ===")
class Rectangle:
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:
self._width = value
@property
def height(self):
return self._height
@height.setter
def height(self, value):
if value > 0:
self._height = value
@property
def area(self):
"""Computed property - no setter."""
return self._width * self._height
@property
def perimeter(self):
return 2 * (self._width + self._height)
rect =
print(f"Size: {rect.width} x {rect.height}")
print(f"Area: {rect.area}")
print(f"Perimeter: {rect.perimeter}")
# Change dimensions
rect.width = 10
print(f"\nAfter resize:")
print(f"Size: {rect.width} x {rect.height}")
print(f"Area: {rect.area}") # Automatically updated!
print("\n=== Property with Deleter ===")
class User:
def __init__(self, email):
self._email = email
@property
def email(self):
return self._email
@email.setter
def email(self, value):
if "@" in value:
self._email = value
@email.deleter
def email(self):
print("Removing email...")
self._email = None
user = User("alice@example.com")
print(f"Email: {user.email}")
del user.email
print(f"After delete: {user.email}")
print("\n=== Property Naming Patterns ===")
class Temperature:
def __init__(self, celsius):
self._celsius = celsius
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value >= -273.15:
self._celsius = value
@property
def fahrenheit(self):
return self._celsius * 9/5 + 32
@fahrenheit.setter
def fahrenheit(self, value):
self._celsius = (value - 32) * 5/9
temp = Temperature(0)
print(f"0°C = {temp.fahrenheit}°F")
temp.fahrenheit = 212 # Set via Fahrenheit
print(f"{temp.fahrenheit}°F = {temp.celsius}°C")
# Python Properties
print("=== Basic @property ===\n")
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self):
"""Get the radius."""
return self._radius
@radius.setter
def radius(self, value):
"""Set the radius with validation."""
if value > 0:
self._radius = value
else:
print(f"Invalid radius: {value}")
circle =
# Access like attribute
print(f"Radius: {circle.radius}") # Calls getter
# Assign like attribute
circle.radius = 10 # Calls setter
print(f"New radius: {circle.radius}")
# Validation still works
circle.radius = -5 # Rejected
print(f"Still: {circle.radius}")
print("\n=== Properties vs Direct Access ===")
# Compare syntax:
print("""
Traditional:
value = obj.get_attribute()
obj.set_attribute(value)
With @property:
value = obj.attribute
obj.attribute = value
Same control, cleaner syntax!
""")
print("=== Multiple Properties ===")
class Rectangle:
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:
self._width = value
@property
def height(self):
return self._height
@height.setter
def height(self, value):
if value > 0:
self._height = value
@property
def area(self):
"""Computed property - no setter."""
return self._width * self._height
@property
def perimeter(self):
return 2 * (self._width + self._height)
rect =
print(f"Size: {rect.width} x {rect.height}")
print(f"Area: {rect.area}")
print(f"Perimeter: {rect.perimeter}")
# Change dimensions
rect.width = 10
print(f"\nAfter resize:")
print(f"Size: {rect.width} x {rect.height}")
print(f"Area: {rect.area}") # Automatically updated!
print("\n=== Property with Deleter ===")
class User:
def __init__(self, email):
self._email = email
@property
def email(self):
return self._email
@email.setter
def email(self, value):
if "@" in value:
self._email = value
@email.deleter
def email(self):
print("Removing email...")
self._email = None
user = User("alice@example.com")
print(f"Email: {user.email}")
del user.email
print(f"After delete: {user.email}")
print("\n=== Property Naming Patterns ===")
class Temperature:
def __init__(self, celsius):
self._celsius = celsius
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value >= -273.15:
self._celsius = value
@property
def fahrenheit(self):
return self._celsius * 9/5 + 32
@fahrenheit.setter
def fahrenheit(self, value):
self._celsius = (value - 32) * 5/9
temp = Temperature(0)
print(f"0°C = {temp.fahrenheit}°F")
temp.fahrenheit = 212 # Set via Fahrenheit
print(f"{temp.fahrenheit}°F = {temp.celsius}°C")
# Python Properties
print("=== Basic @property ===\n")
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self):
"""Get the radius."""
return self._radius
@radius.setter
def radius(self, value):
"""Set the radius with validation."""
if value > 0:
self._radius = value
else:
print(f"Invalid radius: {value}")
circle =
# Access like attribute
print(f"Radius: {circle.radius}") # Calls getter
# Assign like attribute
circle.radius = 10 # Calls setter
print(f"New radius: {circle.radius}")
# Validation still works
circle.radius = -5 # Rejected
print(f"Still: {circle.radius}")
print("\n=== Properties vs Direct Access ===")
# Compare syntax:
print("""
Traditional:
value = obj.get_attribute()
obj.set_attribute(value)
With @property:
value = obj.attribute
obj.attribute = value
Same control, cleaner syntax!
""")
print("=== Multiple Properties ===")
class Rectangle:
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:
self._width = value
@property
def height(self):
return self._height
@height.setter
def height(self, value):
if value > 0:
self._height = value
@property
def area(self):
"""Computed property - no setter."""
return self._width * self._height
@property
def perimeter(self):
return 2 * (self._width + self._height)
rect =
print(f"Size: {rect.width} x {rect.height}")
print(f"Area: {rect.area}")
print(f"Perimeter: {rect.perimeter}")
# Change dimensions
rect.width = 10
print(f"\nAfter resize:")
print(f"Size: {rect.width} x {rect.height}")
print(f"Area: {rect.area}") # Automatically updated!
print("\n=== Property with Deleter ===")
class User:
def __init__(self, email):
self._email = email
@property
def email(self):
return self._email
@email.setter
def email(self, value):
if "@" in value:
self._email = value
@email.deleter
def email(self):
print("Removing email...")
self._email = None
user = User("alice@example.com")
print(f"Email: {user.email}")
del user.email
print(f"After delete: {user.email}")
print("\n=== Property Naming Patterns ===")
class Temperature:
def __init__(self, celsius):
self._celsius = celsius
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value >= -273.15:
self._celsius = value
@property
def fahrenheit(self):
return self._celsius * 9/5 + 32
@fahrenheit.setter
def fahrenheit(self, value):
self._celsius = (value - 32) * 5/9
temp = Temperature(0)
print(f"0°C = {temp.fahrenheit}°F")
temp.fahrenheit = 212 # Set via Fahrenheit
print(f"{temp.fahrenheit}°F = {temp.celsius}°C")
# Python Properties
print("=== Basic @property ===\n")
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self):
"""Get the radius."""
return self._radius
@radius.setter
def radius(self, value):
"""Set the radius with validation."""
if value > 0:
self._radius = value
else:
print(f"Invalid radius: {value}")
circle =
# Access like attribute
print(f"Radius: {circle.radius}") # Calls getter
# Assign like attribute
circle.radius = 10 # Calls setter
print(f"New radius: {circle.radius}")
# Validation still works
circle.radius = -5 # Rejected
print(f"Still: {circle.radius}")
print("\n=== Properties vs Direct Access ===")
# Compare syntax:
print("""
Traditional:
value = obj.get_attribute()
obj.set_attribute(value)
With @property:
value = obj.attribute
obj.attribute = value
Same control, cleaner syntax!
""")
print("=== Multiple Properties ===")
class Rectangle:
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:
self._width = value
@property
def height(self):
return self._height
@height.setter
def height(self, value):
if value > 0:
self._height = value
@property
def area(self):
"""Computed property - no setter."""
return self._width * self._height
@property
def perimeter(self):
return 2 * (self._width + self._height)
rect =
print(f"Size: {rect.width} x {rect.height}")
print(f"Area: {rect.area}")
print(f"Perimeter: {rect.perimeter}")
# Change dimensions
rect.width = 10
print(f"\nAfter resize:")
print(f"Size: {rect.width} x {rect.height}")
print(f"Area: {rect.area}") # Automatically updated!
print("\n=== Property with Deleter ===")
class User:
def __init__(self, email):
self._email = email
@property
def email(self):
return self._email
@email.setter
def email(self, value):
if "@" in value:
self._email = value
@email.deleter
def email(self):
print("Removing email...")
self._email = None
user = User("alice@example.com")
print(f"Email: {user.email}")
del user.email
print(f"After delete: {user.email}")
print("\n=== Property Naming Patterns ===")
class Temperature:
def __init__(self, celsius):
self._celsius = celsius
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value >= -273.15:
self._celsius = value
@property
def fahrenheit(self):
return self._celsius * 9/5 + 32
@fahrenheit.setter
def fahrenheit(self, value):
self._celsius = (value - 32) * 5/9
temp = Temperature(0)
print(f"0°C = {temp.fahrenheit}°F")
temp.fahrenheit = 212 # Set via Fahrenheit
print(f"{temp.fahrenheit}°F = {temp.celsius}°C")
# Python Properties
print("=== Basic @property ===\n")
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self):
"""Get the radius."""
return self._radius
@radius.setter
def radius(self, value):
"""Set the radius with validation."""
if value > 0:
self._radius = value
else:
print(f"Invalid radius: {value}")
circle =
# Access like attribute
print(f"Radius: {circle.radius}") # Calls getter
# Assign like attribute
circle.radius = 10 # Calls setter
print(f"New radius: {circle.radius}")
# Validation still works
circle.radius = -5 # Rejected
print(f"Still: {circle.radius}")
print("\n=== Properties vs Direct Access ===")
# Compare syntax:
print("""
Traditional:
value = obj.get_attribute()
obj.set_attribute(value)
With @property:
value = obj.attribute
obj.attribute = value
Same control, cleaner syntax!
""")
print("=== Multiple Properties ===")
class Rectangle:
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:
self._width = value
@property
def height(self):
return self._height
@height.setter
def height(self, value):
if value > 0:
self._height = value
@property
def area(self):
"""Computed property - no setter."""
return self._width * self._height
@property
def perimeter(self):
return 2 * (self._width + self._height)
rect =
print(f"Size: {rect.width} x {rect.height}")
print(f"Area: {rect.area}")
print(f"Perimeter: {rect.perimeter}")
# Change dimensions
rect.width = 10
print(f"\nAfter resize:")
print(f"Size: {rect.width} x {rect.height}")
print(f"Area: {rect.area}") # Automatically updated!
print("\n=== Property with Deleter ===")
class User:
def __init__(self, email):
self._email = email
@property
def email(self):
return self._email
@email.setter
def email(self, value):
if "@" in value:
self._email = value
@email.deleter
def email(self):
print("Removing email...")
self._email = None
user = User("alice@example.com")
print(f"Email: {user.email}")
del user.email
print(f"After delete: {user.email}")
print("\n=== Property Naming Patterns ===")
class Temperature:
def __init__(self, celsius):
self._celsius = celsius
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value >= -273.15:
self._celsius = value
@property
def fahrenheit(self):
return self._celsius * 9/5 + 32
@fahrenheit.setter
def fahrenheit(self, value):
self._celsius = (value - 32) * 5/9
temp = Temperature(0)
print(f"0°C = {temp.fahrenheit}°F")
temp.fahrenheit = 212 # Set via Fahrenheit
print(f"{temp.fahrenheit}°F = {temp.celsius}°C")
# Python Properties
print("=== Basic @property ===\n")
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self):
"""Get the radius."""
return self._radius
@radius.setter
def radius(self, value):
"""Set the radius with validation."""
if value > 0:
self._radius = value
else:
print(f"Invalid radius: {value}")
circle =
# Access like attribute
print(f"Radius: {circle.radius}") # Calls getter
# Assign like attribute
circle.radius = 10 # Calls setter
print(f"New radius: {circle.radius}")
# Validation still works
circle.radius = -5 # Rejected
print(f"Still: {circle.radius}")
print("\n=== Properties vs Direct Access ===")
# Compare syntax:
print("""
Traditional:
value = obj.get_attribute()
obj.set_attribute(value)
With @property:
value = obj.attribute
obj.attribute = value
Same control, cleaner syntax!
""")
print("=== Multiple Properties ===")
class Rectangle:
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:
self._width = value
@property
def height(self):
return self._height
@height.setter
def height(self, value):
if value > 0:
self._height = value
@property
def area(self):
"""Computed property - no setter."""
return self._width * self._height
@property
def perimeter(self):
return 2 * (self._width + self._height)
rect =
print(f"Size: {rect.width} x {rect.height}")
print(f"Area: {rect.area}")
print(f"Perimeter: {rect.perimeter}")
# Change dimensions
rect.width = 10
print(f"\nAfter resize:")
print(f"Size: {rect.width} x {rect.height}")
print(f"Area: {rect.area}") # Automatically updated!
print("\n=== Property with Deleter ===")
class User:
def __init__(self, email):
self._email = email
@property
def email(self):
return self._email
@email.setter
def email(self, value):
if "@" in value:
self._email = value
@email.deleter
def email(self):
print("Removing email...")
self._email = None
user = User("alice@example.com")
print(f"Email: {user.email}")
del user.email
print(f"After delete: {user.email}")
print("\n=== Property Naming Patterns ===")
class Temperature:
def __init__(self, celsius):
self._celsius = celsius
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value >= -273.15:
self._celsius = value
@property
def fahrenheit(self):
return self._celsius * 9/5 + 32
@fahrenheit.setter
def fahrenheit(self, value):
self._celsius = (value - 32) * 5/9
temp = Temperature(0)
print(f"0°C = {temp.fahrenheit}°F")
temp.fahrenheit = 212 # Set via Fahrenheit
print(f"{temp.fahrenheit}°F = {temp.celsius}°C")
# Python Properties
print("=== Basic @property ===\n")
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self):
"""Get the radius."""
return self._radius
@radius.setter
def radius(self, value):
"""Set the radius with validation."""
if value > 0:
self._radius = value
else:
print(f"Invalid radius: {value}")
circle =
# Access like attribute
print(f"Radius: {circle.radius}") # Calls getter
# Assign like attribute
circle.radius = 10 # Calls setter
print(f"New radius: {circle.radius}")
# Validation still works
circle.radius = -5 # Rejected
print(f"Still: {circle.radius}")
print("\n=== Properties vs Direct Access ===")
# Compare syntax:
print("""
Traditional:
value = obj.get_attribute()
obj.set_attribute(value)
With @property:
value = obj.attribute
obj.attribute = value
Same control, cleaner syntax!
""")
print("=== Multiple Properties ===")
class Rectangle:
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:
self._width = value
@property
def height(self):
return self._height
@height.setter
def height(self, value):
if value > 0:
self._height = value
@property
def area(self):
"""Computed property - no setter."""
return self._width * self._height
@property
def perimeter(self):
return 2 * (self._width + self._height)
rect =
print(f"Size: {rect.width} x {rect.height}")
print(f"Area: {rect.area}")
print(f"Perimeter: {rect.perimeter}")
# Change dimensions
rect.width = 10
print(f"\nAfter resize:")
print(f"Size: {rect.width} x {rect.height}")
print(f"Area: {rect.area}") # Automatically updated!
print("\n=== Property with Deleter ===")
class User:
def __init__(self, email):
self._email = email
@property
def email(self):
return self._email
@email.setter
def email(self, value):
if "@" in value:
self._email = value
@email.deleter
def email(self):
print("Removing email...")
self._email = None
user = User("alice@example.com")
print(f"Email: {user.email}")
del user.email
print(f"After delete: {user.email}")
print("\n=== Property Naming Patterns ===")
class Temperature:
def __init__(self, celsius):
self._celsius = celsius
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value >= -273.15:
self._celsius = value
@property
def fahrenheit(self):
return self._celsius * 9/5 + 32
@fahrenheit.setter
def fahrenheit(self, value):
self._celsius = (value - 32) * 5/9
temp = Temperature(0)
print(f"0°C = {temp.fahrenheit}°F")
temp.fahrenheit = 212 # Set via Fahrenheit
print(f"{temp.fahrenheit}°F = {temp.celsius}°C")
@property creates a getter. @name.setter creates a setter. Clean syntax.
Read-only properties
Attributes that can't be changed after creation.
# Read-Only and Computed Properties
print("=== Read-Only Properties ===\n")
class ImmutablePoint:
def __init__(self, x, y):
self._x = x
self._y = y
@property
def x(self):
return self._x
@property
def y(self):
return self._y
# No setters! Can't modify x or y
point = ImmutablePoint(3, 4)
print(f"Point: ({point.x}, {point.y})")
# Reading works
print(f"X coordinate: {point.x}")
# Writing fails
try:
point.x = 10 # AttributeError!
except AttributeError as e:
print(f"Error: can't set x - {e}")
print("\n=== Computed Read-Only Properties ===")
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def radius(self):
return self._radius
@radius.setter
def radius(self, value):
if value > 0:
self._radius = value
@property
def diameter(self):
return self._radius * 2
@property
def circumference(self):
import math
return 2 * math.pi * self._radius
@property
def area(self):
import math
return math.pi * self._radius ** 2
circle = Circle(5)
print(f"Radius: {circle.radius}")
print(f"Diameter: {circle.diameter}")
print(f"Circumference: {circle.circumference:.2f}")
print(f"Area: {circle.area:.2f}")
# Change radius - computed properties update
circle.radius = 10
print(f"\nAfter radius = 10:")
print(f"Diameter: {circle.diameter}")
print(f"Area: {circle.area:.2f}")
print("\n=== ID and Timestamp Properties ===")
import time
class Document:
_next_id = 1
def __init__(self, title, content):
self._id = Document._next_id
Document._next_id += 1
self._created = time.time()
self._title = title
self._content = content
@property
def id(self):
return self._id
@property
def created(self):
return self._created
@property
def title(self):
return self._title
@title.setter
def title(self, value):
self._title = value
@property
def content(self):
return self._content
@content.setter
def content(self, value):
self._content = value
doc = Document("Report", "Initial content")
print(f"ID: {doc.id}")
print(f"Created: {doc.created}")
print(f"Title: {doc.title}")
# Can modify title and content
doc.title = "Updated Report"
print(f"New title: {doc.title}")
# Can't modify id or created
try:
doc.id = 999
except AttributeError:
print("Can't change ID!")
print("\n=== Derived Properties ===")
class Person:
def __init__(self, first_name, last_name, birth_year):
self._first = first_name
self._last = last_name
self._birth_year = birth_year
@property
def first_name(self):
return self._first
@property
def last_name(self):
return self._last
@property
def full_name(self):
return f"{self._first} {self._last}"
@property
def initials(self):
return f"{self._first[0]}.{self._last[0]}."
@property
def age(self):
return 2024 - self._birth_year
person = Person("John", "Doe", 1990)
print(f"Name: {person.full_name}")
print(f"Initials: {person.initials}")
print(f"Age: {person.age}")
print("\n=== Conditional Properties ===")
class Order:
def __init__(self, items, discount=0):
self._items = items
self._discount = discount
@property
def subtotal(self):
return sum(item["price"] * item["qty"] for item in self._items)
@property
def discount_amount(self):
if self.subtotal > 100: # Only if over $100
return self.subtotal * (self._discount / 100)
return 0
@property
def total(self):
return self.subtotal - self.discount_amount
@property
def is_eligible_for_discount(self):
return self.subtotal > 100
items = [
{"name": "Book", "price": 25, "qty": 2},
{"name": "Pen", "price": 5, "qty": 10}
]
order = Order(items, discount=10)
print(f"Subtotal: ${order.subtotal}")
print(f"Eligible for discount: {order.is_eligible_for_discount}")
print(f"Discount (10%): ${order.discount_amount}")
print(f"Total: ${order.total}")
Provide getter without setter. Attempts to set raise AttributeError.
Properties with validation
Validate data before setting.
# Properties with Validation
print("=== Type Validation ===\n")
class Product:
def __init__(self, name, price):
self._name = None
self._price = None
# Use setters for validation
self.name = name
self.price = price
@property
def name(self):
return self._name
@name.setter
def name(self, value):
if not isinstance(value, str):
raise TypeError("Name must be a string")
if len(value) < 1:
raise ValueError("Name cannot be empty")
self._name = value
@property
def price(self):
return self._price
@price.setter
def price(self, value):
if not isinstance(value, (int, float)):
raise TypeError("Price must be a number")
if value < 0:
raise ValueError("Price cannot be negative")
self._price = round(value, 2)
product = Product("Laptop", 999.999)
print(f"Product: {product.name}, ${product.price}")
# Validation in action
try:
product.price = "free"
except TypeError as e:
print(f"Type error: {e}")
try:
product.price = -50
except ValueError as e:
print(f"Value error: {e}")
print("\n=== Range Validation ===")
class Temperature:
MIN_CELSIUS = -273.15
MAX_CELSIUS = 1000
def __init__(self, celsius):
self._celsius = None
self.celsius = celsius
@property
def celsius(self):
return self._celsius
@celsius.setter
def celsius(self, value):
if value < self.MIN_CELSIUS:
raise ValueError(f"Below absolute zero: {value}")
if value > self.MAX_CELSIUS:
raise ValueError(f"Too hot: {value}")
self._celsius = value
temp = Temperature(25)
print(f"Temperature: {temp.celsius}°C")
try:
temp.celsius = -300 # Below absolute zero
except ValueError as e:
print(f"Rejected: {e}")
print("\n=== Format Validation ===")
class User:
def __init__(self, email, phone):
self._email = None
self._phone = None
self.email = email
self.phone = phone
@property
def email(self):
return self._email
@email.setter
def email(self, value):
value = value.strip().lower()
if "@" not in value:
raise ValueError("Invalid email: missing @")
parts = value.split("@")
if len(parts) != 2 or not parts[1]:
raise ValueError("Invalid email format")
self._email = value
@property
def phone(self):
return self._phone
@phone.setter
def phone(self, value):
# Remove non-digits
digits = ''.join(c for c in value if c.isdigit())
if len(digits) < 10:
raise ValueError("Phone must have at least 10 digits")
self._phone = digits
user = User(" Alice@Example.COM ", "(555) 123-4567")
print(f"Email: {user.email}")
print(f"Phone: {user.phone}")
print("\n=== Interdependent Validation ===")
class Rectangle:
def __init__(self, width, height, max_area=10000):
self._width = 0
self._height = 0
self._max_area = max_area
self.width = width # Validates
self.height = height # Validates
@property
def width(self):
return self._width
@width.setter
def width(self, value):
if value <= 0:
raise ValueError("Width must be positive")
if value * self._height > self._max_area:
raise ValueError(f"Area would exceed {self._max_area}")
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")
if self._width * value > self._max_area:
raise ValueError(f"Area would exceed {self._max_area}")
self._height = value
@property
def area(self):
return self._width * self._height
rect = Rectangle(50, 100)
print(f"Rectangle: {rect.width}x{rect.height}, area={rect.area}")
try:
rect.width = 200 # Would make area 20000
except ValueError as e:
print(f"Rejected: {e}")
print("\n=== Validation with Warnings ===")
class Score:
def __init__(self, value):
self._value = 0
self._warnings = []
self.value = value
@property
def value(self):
return self._value
@value.setter
def value(self, value):
self._warnings.clear()
if value < 0:
self._warnings.append(f"Adjusted {value} to 0")
value = 0
elif value > 100:
self._warnings.append(f"Adjusted {value} to 100")
value = 100
self._value = value
@property
def warnings(self):
return self._warnings.copy()
score = Score(150)
print(f"Score: {score.value}")
print(f"Warnings: {score.warnings}")
score.value = -20
print(f"Score: {score.value}")
print(f"Warnings: {score.warnings}")
Setter can check value and raise exception if invalid.
Exercise: practical.py
Build a class with encapsulated state and validation