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 = Person("Alice", 30)
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 = Person("Dana", 42)
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 = Person("Sam", 19)
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()
"""Demonstrates basic property usage."""
3class Person:4 """Demonstrates basic property usage."""def main():
35def main():36 print("=== Basic @property Usage ===\n")3738 # Create person #?create_person39 print("--- Creating Person ---")40 person = Person("Alice", 30) #?create_alice41 #@person=Person("Dana", 42), Person("Sam", 19)output=== Basic @property Usage === --- Creating Person ---self._name ← Alice, self._age ← 30
6def __init__(self⟨Person A⟩, nameAlice, age30): #?init7 self._name→ Alice = nameAlice #?set_name8 self._age→ 30 = age30 #?set_ageperson ← ⟨Person A⟩
39print("--- Creating Person ---")40person→ ⟨Person A⟩ = Person("Alice", 30) #?create_alice41#@person=Person("Dana", 42), Person("Sam", 19)42print(f"Created: {person._nameAlice}, {person._age30}\n") #?print_created4344# Access via property (calls getter) #?access_property45print("--- Getting via Property ---")46name = person.nameAlice #?get_name47print(f"Name is: {name}\n") #?print_nameoutputCreated: Alice, 30 --- Getting via Property ---def name(self): #?name_getter
pass 1 of 210@property #?property_decorator11def name(self⟨Person A⟩): #?name_getter12 """Get the person's name.""" #?getter_doc13 print(f" Getting name: {self._nameAlice}") #?log_get_name14 return self._nameAlice #?return_nameoutput Getting name: Alicename ← Alice
45print("--- Getting via Property ---")46name→ Alice = person.nameAlice #?get_name47print(f"Name is: {nameAlice}\n") #?print_name4849# Set via property (calls setter) #?set_property50print("--- Setting via Property ---")51person.name = "Bob" #?set_bob52print(f"Name changed to: {person.name}\n") #?print_changedoutputName is: Alice --- Setting via Property ---self._name ← Bob
16@name.setter #?name_setter_decorator17def name(self⟨Person A⟩, valueBob): #?name_setter18 """Set the person's name.""" #?setter_doc19 print(f" Setting name: {valueBob}") #?log_set_name20 self._name→ Bob = valueBob #?store_nameoutput Setting name: Bobperson.name ← Bob
50print("--- Setting via Property ---")51person.name→ Bob = "Bob" #?set_bob52print(f"Name changed to: {person.nameBob}\n") #?print_changeddef name(self): #?name_getter
pass 2 of 210@property #?property_decorator11def name(self⟨Person A⟩): #?name_getter12 """Get the person's name.""" #?getter_doc13 print(f" Getting name: {self._nameBob}") #?log_get_name14 return self._nameBob #?return_nameoutput Getting name: Bobprint(f"Name changed to: {person.name} ") #?print_changed
51person.name = "Bob" #?set_bob52print(f"Name changed to: {person.nameBob}\n") #?print_changed5354# Age with validation #?age_validation55print("--- Age with Validation ---")56print(f"Current age: {person.age30}") #?print_current_age57person.age = 31 #?set_valid_ageoutputName changed to: Bob --- Age with Validation ---def age(self): #?age_getter
pass 1 of 222@property #?age_property23def age(self⟨Person A⟩): #?age_getter24 """Get the person's age."""25 return self._age30 #?return_ageprint(f"Current age: {person.age}") #?print_current_age
55print("--- Age with Validation ---")56print(f"Current age: {person.age30}") #?print_current_age57person.age = 31 #?set_valid_age58print(f"New age: {person.age}") #?print_new_ageoutputCurrent age: 30self._age ← 31
pass 1 of 227@age.setter #?age_setter_decorator28def age(self⟨Person A⟩, value31): #?age_setter29 """Set the person's age."""30 if value < 0: #?validate_age31 raise ValueError("Age cannot be negative") #?raise_negative32 self._age→ 31 = value31 #?store_ageperson.age ← 31
56print(f"Current age: {person.age}") #?print_current_age57person.age→ 31 = 31 #?set_valid_age58print(f"New age: {person.age31}") #?print_new_agedef age(self): #?age_getter
pass 2 of 222@property #?age_property23def age(self⟨Person A⟩): #?age_getter24 """Get the person's age."""25 return self._age31 #?return_ageprint(f"New age: {person.age}") #?print_new_age
57person.age = 31 #?set_valid_age58print(f"New age: {person.age31}") #?print_new_ageoutputNew age: 31def age(self, value): #?age_setter
pass 2 of 227@age.setter #?age_setter_decorator28def age(self⟨Person A⟩, value-5): #?age_setter29 """Set the person's age."""30 if value < 0: #?validate_ageif value < 0: #?validate_age
29"""Set the person's age."""30if value-5 < 0: #?validate_age31 raise ValueError("Age cannot be negative") #?raise_negative32self._age = value #?store_ageexcept ValueError as e: #?catch_invalid
61 person.age = -5 #?set_invalid_age62except ValueError as e: #?catch_invalid63 print(f"Error: {eAge cannot be negative}") #?print_error6465print("\n=== Key Points ===")outputError: Age cannot be negative Error: Age cannot be negativeprint(" === Key Points ===")
65 print("\n=== Key Points ===")66 print("""671. @property decorator converts method to getter682. @<property>.setter creates setter693. Access like regular attribute (no parentheses)704. Underlying value often stored with _ prefix715. Enables validation, logging, etc.72 """)output === Key Points === 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.main()
75if __name__ == "__main__":76 main()
"""Demonstrates basic property usage."""
3class Person:4 """Demonstrates basic property usage."""def main():
35def main():36 print("=== Basic @property Usage ===\n")3738 # Create person39 print("--- Creating Person ---")40 person = Person("Dana", 42)41 print(f"Created: {person._name}, {person._age}\n")output=== Basic @property Usage === --- Creating Person ---self._name ← Dana, self._age ← 42
6def __init__(self⟨Person A⟩, nameDana, age42):7 self._name→ Dana = nameDana8 self._age→ 42 = age42person ← ⟨Person A⟩
39print("--- Creating Person ---")40person→ ⟨Person A⟩ = Person("Dana", 42)41print(f"Created: {person._nameDana}, {person._age42}\n")4243# Access via property (calls getter)44print("--- Getting via Property ---")45name = person.nameDana46print(f"Name is: {name}\n")outputCreated: Dana, 42 --- Getting via Property ---def name(self):
pass 1 of 210@property11def name(self⟨Person A⟩):12 """Get the person's name."""13 print(f" Getting name: {self._nameDana}")14 return self._nameDanaoutput Getting name: Dananame ← Dana
44print("--- Getting via Property ---")45name→ Dana = person.nameDana46print(f"Name is: {nameDana}\n")4748# Set via property (calls setter)49print("--- Setting via Property ---")50person.name = "Bob"51print(f"Name changed to: {person.name}\n")outputName is: Dana --- Setting via Property ---self._name ← Bob
16@name.setter17def name(self⟨Person A⟩, valueBob):18 """Set the person's name."""19 print(f" Setting name: {valueBob}")20 self._name→ Bob = valueBoboutput Setting name: Bobperson.name ← Bob
49print("--- Setting via Property ---")50person.name→ Bob = "Bob"51print(f"Name changed to: {person.nameBob}\n")def name(self):
pass 2 of 210@property11def name(self⟨Person A⟩):12 """Get the person's name."""13 print(f" Getting name: {self._nameBob}")14 return self._nameBoboutput Getting name: Bobprint(f"Name changed to: {person.name} ")
50person.name = "Bob"51print(f"Name changed to: {person.nameBob}\n")5253# Age with validation54print("--- Age with Validation ---")55print(f"Current age: {person.age42}")56person.age = 31outputName changed to: Bob --- Age with Validation ---def age(self):
pass 1 of 222@property23def age(self⟨Person A⟩):24 """Get the person's age."""25 return self._age42print(f"Current age: {person.age}")
54print("--- Age with Validation ---")55print(f"Current age: {person.age42}")56person.age = 3157print(f"New age: {person.age}")outputCurrent age: 42self._age ← 31
pass 1 of 227@age.setter28def age(self⟨Person A⟩, value31):29 """Set the person's age."""30 if value < 0:31 raise ValueError("Age cannot be negative")32 self._age→ 31 = value31person.age ← 31
55print(f"Current age: {person.age}")56person.age→ 31 = 3157print(f"New age: {person.age31}")def age(self):
pass 2 of 222@property23def age(self⟨Person A⟩):24 """Get the person's age."""25 return self._age31print(f"New age: {person.age}")
56person.age = 3157print(f"New age: {person.age31}")outputNew age: 31def age(self, value):
pass 2 of 227@age.setter28def age(self⟨Person A⟩, value-5):29 """Set the person's age."""30 if value < 0:if value < 0:
29"""Set the person's age."""30if value-5 < 0:31 raise ValueError("Age cannot be negative")32self._age = valueexcept ValueError as e:
60 person.age = -561except ValueError as e:62 print(f"Error: {eAge cannot be negative}")6364print("\n=== Key Points ===")outputError: Age cannot be negative Error: Age cannot be negativeprint(" === Key Points ===")
64 print("\n=== Key Points ===")65 print("""661. @property decorator converts method to getter672. @<property>.setter creates setter683. Access like regular attribute (no parentheses)694. Underlying value often stored with _ prefix705. Enables validation, logging, etc.71 """)output === Key Points === 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.main()
74if __name__ == "__main__":75 main()
"""Demonstrates basic property usage."""
3class Person:4 """Demonstrates basic property usage."""def main():
35def main():36 print("=== Basic @property Usage ===\n")3738 # Create person39 print("--- Creating Person ---")40 person = Person("Sam", 19)41 print(f"Created: {person._name}, {person._age}\n")output=== Basic @property Usage === --- Creating Person ---self._name ← Sam, self._age ← 19
6def __init__(self⟨Person A⟩, nameSam, age19):7 self._name→ Sam = nameSam8 self._age→ 19 = age19person ← ⟨Person A⟩
39print("--- Creating Person ---")40person→ ⟨Person A⟩ = Person("Sam", 19)41print(f"Created: {person._nameSam}, {person._age19}\n")4243# Access via property (calls getter)44print("--- Getting via Property ---")45name = person.nameSam46print(f"Name is: {name}\n")outputCreated: Sam, 19 --- Getting via Property ---def name(self):
pass 1 of 210@property11def name(self⟨Person A⟩):12 """Get the person's name."""13 print(f" Getting name: {self._nameSam}")14 return self._nameSamoutput Getting name: Samname ← Sam
44print("--- Getting via Property ---")45name→ Sam = person.nameSam46print(f"Name is: {nameSam}\n")4748# Set via property (calls setter)49print("--- Setting via Property ---")50person.name = "Bob"51print(f"Name changed to: {person.name}\n")outputName is: Sam --- Setting via Property ---self._name ← Bob
16@name.setter17def name(self⟨Person A⟩, valueBob):18 """Set the person's name."""19 print(f" Setting name: {valueBob}")20 self._name→ Bob = valueBoboutput Setting name: Bobperson.name ← Bob
49print("--- Setting via Property ---")50person.name→ Bob = "Bob"51print(f"Name changed to: {person.nameBob}\n")def name(self):
pass 2 of 210@property11def name(self⟨Person A⟩):12 """Get the person's name."""13 print(f" Getting name: {self._nameBob}")14 return self._nameBoboutput Getting name: Bobprint(f"Name changed to: {person.name} ")
50person.name = "Bob"51print(f"Name changed to: {person.nameBob}\n")5253# Age with validation54print("--- Age with Validation ---")55print(f"Current age: {person.age19}")56person.age = 31outputName changed to: Bob --- Age with Validation ---def age(self):
pass 1 of 222@property23def age(self⟨Person A⟩):24 """Get the person's age."""25 return self._age19print(f"Current age: {person.age}")
54print("--- Age with Validation ---")55print(f"Current age: {person.age19}")56person.age = 3157print(f"New age: {person.age}")outputCurrent age: 19self._age ← 31
pass 1 of 227@age.setter28def age(self⟨Person A⟩, value31):29 """Set the person's age."""30 if value < 0:31 raise ValueError("Age cannot be negative")32 self._age→ 31 = value31person.age ← 31
55print(f"Current age: {person.age}")56person.age→ 31 = 3157print(f"New age: {person.age31}")def age(self):
pass 2 of 222@property23def age(self⟨Person A⟩):24 """Get the person's age."""25 return self._age31print(f"New age: {person.age}")
56person.age = 3157print(f"New age: {person.age31}")outputNew age: 31def age(self, value):
pass 2 of 227@age.setter28def age(self⟨Person A⟩, value-5):29 """Set the person's age."""30 if value < 0:if value < 0:
29"""Set the person's age."""30if value-5 < 0:31 raise ValueError("Age cannot be negative")32self._age = valueexcept ValueError as e:
60 person.age = -561except ValueError as e:62 print(f"Error: {eAge cannot be negative}")6364print("\n=== Key Points ===")outputError: Age cannot be negative Error: Age cannot be negativeprint(" === Key Points ===")
64 print("\n=== Key Points ===")65 print("""661. @property decorator converts method to getter672. @<property>.setter creates setter683. Access like regular attribute (no parentheses)694. Underlying value often stored with _ prefix705. Enables validation, logging, etc.71 """)output === Key Points === 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.main()
74if __name__ == "__main__":75 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()
"""Bank account with validation."""
3class BankAccount:4 """Bank account with validation."""56 def __init__(self, account_number, balance=0): #?init_account7 self._account_number = account_number #?store_account_number8 self._balance = balance #?store_balance910 @property #?balance_property11 def balance(self): #?balance_getter12 """Get current balance."""13 return self._balance #?return_balance1415 @balance.setter #?balance_setter16 def balance(self, value): #?balance_setter_method17 """Set balance with validation."""18 if value < 0: #?check_negative19 raise ValueError("Balance cannot be negative") #?raise_negative_balance20 self._balance = value #?store_valid_balance212223class User:24 """User with multiple validated properties."""def main():
72def main():73 print("=== Properties with Validation ===\n")7475 # Bank account validation #?bank_demo76 print("--- Bank Account ---")77 account = BankAccount("12345", 1000) #?create_account78 print(f"Initial balance: ${account.balance}") #?print_initialoutput=== Properties with Validation === --- Bank Account ---self._account_number ← 12345, self._balance ← 1000
6def __init__(self⟨BankAccount A⟩, account_number12345, balance1000=0): #?init_account7 self._account_number→ 12345 = account_number12345 #?store_account_number8 self._balance→ 1000 = balance1000 #?store_balanceaccount ← ⟨BankAccount A⟩
76print("--- Bank Account ---")77account→ ⟨BankAccount A⟩ = BankAccount("12345", 1000) #?create_account78print(f"Initial balance: ${account.balance1000}") #?print_initialdef balance(self): #?balance_getter
pass 1 of 210@property #?balance_property11def balance(self⟨BankAccount A⟩): #?balance_getter12 """Get current balance."""13 return self._balance1000 #?return_balanceprint(f"Initial balance: ${account.balance}") #?print_initial
77account = BankAccount("12345", 1000) #?create_account78print(f"Initial balance: ${account.balance1000}") #?print_initial7980account.balance = 1500 #?set_valid_balance81print(f"After deposit: ${account.balance}") #?print_after_depositoutputInitial balance: $1000self._balance ← 1500
pass 1 of 215@balance.setter #?balance_setter16def balance(self⟨BankAccount A⟩, value1500): #?balance_setter_method17 """Set balance with validation."""18 if value < 0: #?check_negative19 raise ValueError("Balance cannot be negative") #?raise_negative_balance20 self._balance→ 1500 = value1500 #?store_valid_balanceaccount.balance ← 1500
80account.balance→ 1500 = 1500 #?set_valid_balance81print(f"After deposit: ${account.balance1500}") #?print_after_depositdef balance(self): #?balance_getter
pass 2 of 210@property #?balance_property11def balance(self⟨BankAccount A⟩): #?balance_getter12 """Get current balance."""13 return self._balance1500 #?return_balanceprint(f"After deposit: ${account.balance}") #?print_after_deposit
80account.balance = 1500 #?set_valid_balance81print(f"After deposit: ${account.balance1500}") #?print_after_depositoutputAfter deposit: $1500def balance(self, value): #?balance_setter_method
pass 2 of 215@balance.setter #?balance_setter16def balance(self⟨BankAccount A⟩, value-100): #?balance_setter_method17 """Set balance with validation."""18 if value < 0: #?check_negativeif value < 0: #?check_negative
17"""Set balance with validation."""18if value-100 < 0: #?check_negative19 raise ValueError("Balance cannot be negative") #?raise_negative_balance20self._balance = value #?store_valid_balanceexcept ValueError as e: #?catch_balance_error
84 account.balance = -100 #?set_negative_balance85except ValueError as e: #?catch_balance_error86 print(f"Error: {eBalance cannot be negative}") #?print_balance_error8788# User validation #?user_demooutputError: Balance cannot be negative Error: Balance cannot be negativeprint(" --- User Validation ---")
88# User validation #?user_demo89print("\n--- User Validation ---")output --- User Validation ---def __init__(self, username, email, age): #?init_user
pass 1 of 226def __init__(self⟨User B⟩, usernamealice, emailalice@example.com, age25): #?init_user27 self.username = usernamealice # Uses setter28 self.email = email # Uses setterself._username ← alice, self.username ← alice
pass 1 of 326def __init__(self, username, email, age): #?init_user27 self.username→ alice = usernamealice # Uses setter28 self.email = emailalice@example.com # Uses setter29 self.age = age # Uses setter3031@property #?username_property32def username(self): #?username_getter33 """Get username."""34 return self._username #?return_username3536@username.setter #?username_setter37def username(self⟨User B⟩, valuealice): #?username_setter_method38 """Set username - must be 3-20 chars."""39 if not isinstance(value, str): #?check_string40 raise TypeError("Username must be a string") #?raise_type41 if not (3 <= len(value) <= 20): #?check_length42 raise ValueError("Username must be 3-20 characters") #?raise_length43 self._username→ alice = valuealice #?store_usernameAll 3 passes — pass 1 is the card above pass selfvalueusernameemailageeself._usernameself.usernameself._emailself.email1 ⟨User B⟩ alice alice alice@example.com 25 — alice alice alice@example.com alice@example.com 2 ⟨User B⟩ ab — — — Username must be 3-20 characters — — — — 3 ⟨User C⟩ a — — — Username must be 3-20 characters — — — — self._email ← alice@example.com, self.email ← alice@example.com
pass 1 of 227 self.username = username # Uses setter28 self.email→ alice@example.com = emailalice@example.com # Uses setter29 self.age = age25 # Uses setter3031@property #?username_property32def username(self): #?username_getter33 """Get username."""34 return self._username #?return_username3536@username.setter #?username_setter37def username(self, value): #?username_setter_method38 """Set username - must be 3-20 chars."""39 if not isinstance(value, str): #?check_string40 raise TypeError("Username must be a string") #?raise_type41 if not (3 <= len(value) <= 20): #?check_length42 raise ValueError("Username must be 3-20 characters") #?raise_length43 self._username = value #?store_username4445@property #?email_property46def email(self): #?email_getter47 """Get email."""48 return self._email #?return_email4950@email.setter #?email_setter51def email(self⟨User B⟩, valuealice@example.com): #?email_setter_method52 """Set email - must contain @."""53 if "@" not in value: #?check_at54 raise ValueError("Invalid email format") #?raise_email55 self._email→ alice@example.com = valuealice@example.com #?store_emailself._age ← 25, self.age ← 25
pass 1 of 328 self.email = email # Uses setter29 self.age→ 25 = age25 # Uses setter3031@property #?username_property32def username(self): #?username_getter33 """Get username."""34 return self._username #?return_username3536@username.setter #?username_setter37def username(self, value): #?username_setter_method38 """Set username - must be 3-20 chars."""39 if not isinstance(value, str): #?check_string40 raise TypeError("Username must be a string") #?raise_type41 if not (3 <= len(value) <= 20): #?check_length42 raise ValueError("Username must be 3-20 characters") #?raise_length43 self._username = value #?store_username4445@property #?email_property46def email(self): #?email_getter47 """Get email."""48 return self._email #?return_email4950@email.setter #?email_setter51def email(self, value): #?email_setter_method52 """Set email - must contain @."""53 if "@" not in value: #?check_at54 raise ValueError("Invalid email format") #?raise_email55 self._email = value #?store_email5657@property #?age_property58def age(self): #?age_getter59 """Get age."""60 return self._age #?return_age6162@age.setter #?age_setter63def age(self⟨User B⟩, value25): #?age_setter_method64 """Set age - must be 0-150."""65 if not isinstance(value, int): #?check_int66 raise TypeError("Age must be an integer") #?raise_age_type67 if not (0 <= value <= 150): #?check_age_range68 raise ValueError("Age must be between 0 and 150") #?raise_age_range69 self._age→ 25 = value25 #?store_ageAll 3 passes — pass 1 is the card above pass valueageeself._ageself.age1 25 25 — 25 25 2 25 — Age must be an integer — — 3 200 — Age must be between 0 and 150 — — user ← ⟨User B⟩
91try: #?try_create_user92 user→ ⟨User B⟩ = User("alice", "alice@example.com", 25) #?create_valid_user93 print(f"Created user: {user.usernamealice}, {user.emailalice@example.com}, {user.age25}") #?print_user94except (ValueError, TypeError) as e: #?catch_create_errordef username(self): #?username_getter
31@property #?username_property32def username(self⟨User B⟩): #?username_getter33 """Get username."""34 return self._usernamealice #?return_usernamedef email(self): #?email_getter
45@property #?email_property46def email(self⟨User B⟩): #?email_getter47 """Get email."""48 return self._emailalice@example.com #?return_emaildef age(self): #?age_getter
57@property #?age_property58def age(self⟨User B⟩): #?age_getter59 """Get age."""60 return self._age25 #?return_ageprint(f"Created user: {user.username}, {user.email}, {user.age}") #?pr…
92 user = User("alice", "alice@example.com", 25) #?create_valid_user93 print(f"Created user: {user.usernamealice}, {user.emailalice@example.com}, {user.age25}") #?print_user94except (ValueError, TypeError) as e: #?catch_create_erroroutputCreated user: alice, alice@example.com, 25print(" --- Invalid Username ---")
97# Invalid username #?invalid_username98print("\n--- Invalid Username ---")99try: #?try_invalid_usernameoutput --- Invalid Username ---if not (3 <= len(value) <= 20): #?check_length
pass 1 of 240 raise TypeError("Username must be a string") #?raise_type41if not (3 <= len(valueab) <= 20): #?check_length42 raise ValueError("Username must be 3-20 characters") #?raise_length43self._username = value #?store_usernameexcept ValueError as e: #?catch_username_error
100 user.username = "ab" # Too short101except ValueError as e: #?catch_username_error102 print(f"Error: {eUsername must be 3-20 characters}") #?print_username_error103104# Invalid email #?invalid_emailoutputError: Username must be 3-20 characters Error: Username must be 3-20 charactersprint(" --- Invalid Email ---")
104# Invalid email #?invalid_email105print("\n--- Invalid Email ---")106try: #?try_invalid_emailoutput --- Invalid Email ---def email(self, value): #?email_setter_method
pass 2 of 250@email.setter #?email_setter51def email(self⟨User B⟩, valuenot-an-email): #?email_setter_method52 """Set email - must contain @."""53 if "@" not in value: #?check_atif "@" not in value: #?check_at
52"""Set email - must contain @."""53if "@" not in valuenot-an-email: #?check_at54 raise ValueError("Invalid email format") #?raise_email55self._email = value #?store_emailexcept ValueError as e: #?catch_email_error
107 user.email = "not-an-email" #?set_invalid_email108except ValueError as e: #?catch_email_error109 print(f"Error: {eInvalid email format}") #?print_email_error110111# Invalid age type #?invalid_age_typeoutputError: Invalid email format Error: Invalid email formatprint(" --- Invalid Age Type ---")
111# Invalid age type #?invalid_age_type112print("\n--- Invalid Age Type ---")113try: #?try_invalid_age_typeoutput --- Invalid Age Type ---if not isinstance(value, int): #?check_int
64"""Set age - must be 0-150."""65if not isinstance(value25, int): #?check_int66 raise TypeError("Age must be an integer") #?raise_age_type67if not (0 <= value <= 150): #?check_age_rangeexcept TypeError as e: #?catch_age_type_error
114 user.age = "25" # String instead of int115except TypeError as e: #?catch_age_type_error116 print(f"Error: {eAge must be an integer}") #?print_age_type_error117118# Invalid age range #?invalid_age_rangeoutputError: Age must be an integer Error: Age must be an integerprint(" --- Invalid Age Range ---")
118# Invalid age range #?invalid_age_range119print("\n--- Invalid Age Range ---")120try: #?try_invalid_age_rangeoutput --- Invalid Age Range ---if not (0 <= value <= 150): #?check_age_range
66 raise TypeError("Age must be an integer") #?raise_age_type67if not (0 <= value200 <= 150): #?check_age_range68 raise ValueError("Age must be between 0 and 150") #?raise_age_range69self._age = value #?store_ageexcept ValueError as e: #?catch_age_range_error
121 user.age = 200 #?set_out_of_range_age122except ValueError as e: #?catch_age_range_error123 print(f"Error: {eAge must be between 0 and 150}") #?print_age_range_error124125# Validation during construction #?construction_validationoutputError: Age must be between 0 and 150 Error: Age must be between 0 and 150print(" --- Validation During Construction ---")
125# Validation during construction #?construction_validation126print("\n--- Validation During Construction ---")127try: #?try_invalid_constructionoutput --- Validation During Construction ---def __init__(self, username, email, age): #?init_user
pass 2 of 226def __init__(self⟨User C⟩, usernamea, emailbad, age300): #?init_user27 self.username = usernamea # Uses setter28 self.email = email # Uses setterif not (3 <= len(value) <= 20): #?check_length
pass 2 of 240 raise TypeError("Username must be a string") #?raise_type41if not (3 <= len(valuea) <= 20): #?check_length42 raise ValueError("Username must be 3-20 characters") #?raise_length43self._username = value #?store_usernameexcept (ValueError, TypeError) as e: #?catch_construction_error
128 invalid_user = User("a", "bad", 300) #?create_invalid_user129except (ValueError, TypeError) as e: #?catch_construction_error130 print(f"Construction failed: {eUsername must be 3-20 characters}") #?print_construction_error131132print("\n=== Key Points ===")outputConstruction failed: Username must be 3-20 characters Construction failed: Username must be 3-20 charactersprint(" === Key Points ===")
132 print("\n=== Key Points ===")133 print("""1341. Properties enable validation on assignment1352. Validation runs even during __init__1363. Can check type, range, format, etc.1374. Raises appropriate exceptions1385. Keeps object in valid state139 """)output === Key Points === 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 statemain()
142if __name__ == "__main__":143 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"
#
"""Circle with computed properties."""
6class Circle:7 """Circle with computed properties."""89 def __init__(self, radius): #?init_circle10 self._radius = radius #?store_radius1112 @property #?radius_property13 def radius(self): #?radius_getter14 """Get radius."""15 return self._radius #?return_radius1617 @radius.setter #?radius_setter18 def radius(self, value): #?radius_setter_method19 """Set radius."""20 if value <= 0: #?check_positive21 raise ValueError("Radius must be positive") #?raise_radius22 self._radius = value #?store_valid_radius2324 @property #?diameter_property25 def diameter(self): #?diameter_getter26 """Computed: diameter = 2 * radius."""27 return 2 * self._radius #?calc_diameter2829 @diameter.setter #?diameter_setter30 def diameter(self, value): #?diameter_setter_method31 """Set diameter (updates radius)."""32 self.radius = value / 2 # Uses radius setter3334 @property #?area_property35 def area(self): #?area_getter36 """Computed: area = π * r²."""37 return math.pi * self._radius ** 2 #?calc_area3839 @property #?circumference_property40 def circumference(self): #?circumference_getter41 """Computed: circumference = 2 * π * r."""42 return 2 * math.pi * self._radius #?calc_circumference434445class Temperature:46 """Temperature with Celsius/Fahrenheit conversion."""4748 def __init__(self, celsius=0): #?init_temp49 self._celsius = celsius #?store_celsius5051 @property #?celsius_property52 def celsius(self): #?celsius_getter53 """Get temperature in Celsius."""54 return self._celsius #?return_celsius5556 @celsius.setter #?celsius_setter57 def celsius(self, value): #?celsius_setter_method58 """Set temperature in Celsius."""59 if value < -273.15: #?check_absolute_zero60 raise ValueError("Below absolute zero!") #?raise_absolute_zero61 self._celsius = value #?store_valid_celsius6263 @property #?fahrenheit_property64 def fahrenheit(self): #?fahrenheit_getter65 """Computed: convert to Fahrenheit."""66 return self._celsius * 9/5 + 32 #?calc_fahrenheit6768 @fahrenheit.setter #?fahrenheit_setter69 def fahrenheit(self, value): #?fahrenheit_setter_method70 """Set temperature in Fahrenheit (updates celsius)."""71 self.celsius = (value - 32) * 5/9 #?set_celsius_from_fahrenheit7273 @property #?kelvin_property74 def kelvin(self): #?kelvin_getter75 """Computed: convert to Kelvin."""76 return self._celsius + 273.15 #?calc_kelvin7778 @kelvin.setter #?kelvin_setter79 def kelvin(self, value): #?kelvin_setter_method80 """Set temperature in Kelvin (updates celsius)."""81 self.celsius = value - 273.15 #?set_celsius_from_kelvin828384class Rectangle:85 """Rectangle with computed properties."""def main():
132def main():133 print("=== Computed Properties ===\n")134135 # Circle with computed properties #?circle_demo136 print("--- Circle ---")137 circle = Circle(5) #?create_circle138 print(f"Radius: {circle.radius}") #?print_radiusoutput=== Computed Properties === --- Circle ---self._radius ← 5
9def __init__(self⟨Circle A⟩, radius5): #?init_circle10 self._radius→ 5 = radius5 #?store_radiuscircle ← ⟨Circle A⟩
136print("--- Circle ---")137circle→ ⟨Circle A⟩ = Circle(5) #?create_circle138print(f"Radius: {circle.radius5}") #?print_radius139print(f"Diameter: {circle.diameter}") #?print_diameterdef radius(self): #?radius_getter
pass 1 of 212@property #?radius_property13def radius(self⟨Circle A⟩): #?radius_getter14 """Get radius."""15 return self._radius5 #?return_radiusprint(f"Radius: {circle.radius}") #?print_radius
137circle = Circle(5) #?create_circle138print(f"Radius: {circle.radius5}") #?print_radius139print(f"Diameter: {circle.diameter10}") #?print_diameter140print(f"Area: {circle.area:.2f}") #?print_areaoutputRadius: 5def diameter(self): #?diameter_getter
24@property #?diameter_property25def diameter(self⟨Circle A⟩): #?diameter_getter26 """Computed: diameter = 2 * radius."""27 return 2 * self._radius5 #?calc_diameterprint(f"Diameter: {circle.diameter}") #?print_diameter
138print(f"Radius: {circle.radius}") #?print_radius139print(f"Diameter: {circle.diameter10}") #?print_diameter140print(f"Area: {circle.area78.53981633974483:.2f}") #?print_area141print(f"Circumference: {circle.circumference:.2f}") #?print_circumferenceoutputDiameter: 10def area(self): #?area_getter
pass 1 of 234@property #?area_property35def area(self⟨Circle A⟩): #?area_getter36 """Computed: area = π * r²."""37 return math.pi3.141592653589793 * self._radius5 ** 2 #?calc_areaprint(f"Area: {circle.area:.2f}") #?print_area
139print(f"Diameter: {circle.diameter}") #?print_diameter140print(f"Area: {circle.area78.53981633974483:.2f}") #?print_area141print(f"Circumference: {circle.circumference31.41592653589793:.2f}") #?print_circumferenceoutputArea: 78.54def circumference(self): #?circumference_getter
39@property #?circumference_property40def circumference(self⟨Circle A⟩): #?circumference_getter41 """Computed: circumference = 2 * π * r."""42 return 2 * math.pi3.141592653589793 * self._radius5 #?calc_circumferenceprint(f"Circumference: {circle.circumference:.2f}") #?print_circumfere…
140print(f"Area: {circle.area:.2f}") #?print_area141print(f"Circumference: {circle.circumference31.41592653589793:.2f}") #?print_circumference142143# Update via diameter #?update_diameter144print("\n--- Update via Diameter ---")145circle.diameter = 20 #?set_diameter146print(f"New radius: {circle.radius}") #?print_new_radiusoutputCircumference: 31.42 --- Update via Diameter ---def diameter(self, value): #?diameter_setter_method
29@diameter.setter #?diameter_setter30def diameter(self⟨Circle A⟩, value20): #?diameter_setter_method31 """Set diameter (updates radius)."""32 self.radius = value20 / 2 # Uses radius setterself._radius ← 10.0
17@radius.setter #?radius_setter18def radius(self⟨Circle A⟩, value10.0): #?radius_setter_method19 """Set radius."""20 if value <= 0: #?check_positive21 raise ValueError("Radius must be positive") #?raise_radius22 self._radius→ 10.0 = value10.0 #?store_valid_radiusself.radius ← 10.0
31"""Set diameter (updates radius)."""32self.radius→ 10.0 = value20 / 2 # Uses radius settercircle.diameter ← 20.0
144print("\n--- Update via Diameter ---")145circle.diameter→ 20.0 = 20 #?set_diameter146print(f"New radius: {circle.radius10.0}") #?print_new_radius147print(f"New area: {circle.area:.2f}") #?print_new_areadef radius(self): #?radius_getter
pass 2 of 212@property #?radius_property13def radius(self⟨Circle A⟩): #?radius_getter14 """Get radius."""15 return self._radius10.0 #?return_radiusprint(f"New radius: {circle.radius}") #?print_new_radius
145circle.diameter = 20 #?set_diameter146print(f"New radius: {circle.radius10.0}") #?print_new_radius147print(f"New area: {circle.area314.1592653589793:.2f}") #?print_new_areaoutputNew radius: 10.0def area(self): #?area_getter
pass 2 of 234@property #?area_property35def area(self⟨Circle A⟩): #?area_getter36 """Computed: area = π * r²."""37 return math.pi3.141592653589793 * self._radius10.0 ** 2 #?calc_areaprint(f"New area: {circle.area:.2f}") #?print_new_area
146print(f"New radius: {circle.radius}") #?print_new_radius147print(f"New area: {circle.area314.1592653589793:.2f}") #?print_new_area148149# Temperature conversions #?temp_demo150print("\n--- Temperature Conversions ---")151temp = Temperature(25) #?create_temp152print(f"Celsius: {temp.celsius}°C") #?print_celsiusoutputNew area: 314.16 --- Temperature Conversions ---self._celsius ← 25
48def __init__(self⟨Temperature B⟩, celsius25=0): #?init_temp49 self._celsius→ 25 = celsius25 #?store_celsiustemp ← ⟨Temperature B⟩
150print("\n--- Temperature Conversions ---")151temp→ ⟨Temperature B⟩ = Temperature(25) #?create_temp152print(f"Celsius: {temp.celsius25}°C") #?print_celsius153print(f"Fahrenheit: {temp.fahrenheit}°F") #?print_fahrenheitdef celsius(self): #?celsius_getter
pass 1 of 251@property #?celsius_property52def celsius(self⟨Temperature B⟩): #?celsius_getter53 """Get temperature in Celsius."""54 return self._celsius25 #?return_celsiusprint(f"Celsius: {temp.celsius}°C") #?print_celsius
151temp = Temperature(25) #?create_temp152print(f"Celsius: {temp.celsius25}°C") #?print_celsius153print(f"Fahrenheit: {temp.fahrenheit77.0}°F") #?print_fahrenheit154print(f"Kelvin: {temp.kelvin}K") #?print_kelvinoutputCelsius: 25°Cdef fahrenheit(self): #?fahrenheit_getter
63@property #?fahrenheit_property64def fahrenheit(self⟨Temperature B⟩): #?fahrenheit_getter65 """Computed: convert to Fahrenheit."""66 return self._celsius25 * 9/5 + 32 #?calc_fahrenheitprint(f"Fahrenheit: {temp.fahrenheit}°F") #?print_fahrenheit
152print(f"Celsius: {temp.celsius}°C") #?print_celsius153print(f"Fahrenheit: {temp.fahrenheit77.0}°F") #?print_fahrenheit154print(f"Kelvin: {temp.kelvin298.15}K") #?print_kelvinoutputFahrenheit: 77.0°Fdef kelvin(self): #?kelvin_getter
pass 1 of 273@property #?kelvin_property74def kelvin(self⟨Temperature B⟩): #?kelvin_getter75 """Computed: convert to Kelvin."""76 return self._celsius25 + 273.15 #?calc_kelvinprint(f"Kelvin: {temp.kelvin}K") #?print_kelvin
153print(f"Fahrenheit: {temp.fahrenheit}°F") #?print_fahrenheit154print(f"Kelvin: {temp.kelvin298.15}K") #?print_kelvin155156# Set via Fahrenheit #?set_fahrenheit157print("\n--- Set via Fahrenheit ---")158temp.fahrenheit = 212 #?set_boiling159print(f"Celsius: {temp.celsius}°C") #?print_boiling_celsiusoutputKelvin: 298.15K --- Set via Fahrenheit ---def fahrenheit(self, value): #?fahrenheit_setter_method
68@fahrenheit.setter #?fahrenheit_setter69def fahrenheit(self⟨Temperature B⟩, value212): #?fahrenheit_setter_method70 """Set temperature in Fahrenheit (updates celsius)."""71 self.celsius = (value212 - 32) * 5/9 #?set_celsius_from_fahrenheitself._celsius ← 100.0
56@celsius.setter #?celsius_setter57def celsius(self⟨Temperature B⟩, value100.0): #?celsius_setter_method58 """Set temperature in Celsius."""59 if value < -273.15: #?check_absolute_zero60 raise ValueError("Below absolute zero!") #?raise_absolute_zero61 self._celsius→ 100.0 = value100.0 #?store_valid_celsiusself.celsius ← 100.0
70"""Set temperature in Fahrenheit (updates celsius)."""71self.celsius→ 100.0 = (value212 - 32) * 5/9 #?set_celsius_from_fahrenheittemp.fahrenheit ← 212.0
157print("\n--- Set via Fahrenheit ---")158temp.fahrenheit→ 212.0 = 212 #?set_boiling159print(f"Celsius: {temp.celsius100.0}°C") #?print_boiling_celsius160print(f"Kelvin: {temp.kelvin}K") #?print_boiling_kelvindef celsius(self): #?celsius_getter
pass 2 of 251@property #?celsius_property52def celsius(self⟨Temperature B⟩): #?celsius_getter53 """Get temperature in Celsius."""54 return self._celsius100.0 #?return_celsiusprint(f"Celsius: {temp.celsius}°C") #?print_boiling_celsius
158temp.fahrenheit = 212 #?set_boiling159print(f"Celsius: {temp.celsius100.0}°C") #?print_boiling_celsius160print(f"Kelvin: {temp.kelvin373.15}K") #?print_boiling_kelvinoutputCelsius: 100.0°Cdef kelvin(self): #?kelvin_getter
pass 2 of 273@property #?kelvin_property74def kelvin(self⟨Temperature B⟩): #?kelvin_getter75 """Computed: convert to Kelvin."""76 return self._celsius100.0 + 273.15 #?calc_kelvinprint(f"Kelvin: {temp.kelvin}K") #?print_boiling_kelvin
159print(f"Celsius: {temp.celsius}°C") #?print_boiling_celsius160print(f"Kelvin: {temp.kelvin373.15}K") #?print_boiling_kelvin161162# Rectangle computed properties #?rectangle_demo163print("\n--- Rectangle ---")164rect = Rectangle(10, 5) #?create_rect165print(f"Dimensions: {rect.width} x {rect.height}") #?print_dimensionsoutputKelvin: 373.15K --- Rectangle ---self._width ← 10, self._height ← 5
87def __init__(self⟨Rectangle C⟩, width10, height5): #?init_rectangle88 self._width→ 10 = width10 #?store_width89 self._height→ 5 = height5 #?store_heightrect ← ⟨Rectangle C⟩
163print("\n--- Rectangle ---")164rect→ ⟨Rectangle C⟩ = Rectangle(10, 5) #?create_rect165print(f"Dimensions: {rect.width10} x {rect.height5}") #?print_dimensions166print(f"Area: {rect.area}") #?print_rect_areadef width(self): #?width_getter
pass 1 of 291@property #?width_property92def width(self⟨Rectangle C⟩): #?width_getter93 return self._width10 #?return_widthdef height(self): #?height_getter
pass 1 of 2101@property #?height_property102def height(self⟨Rectangle C⟩): #?height_getter103 return self._height5 #?return_heightprint(f"Dimensions: {rect.width} x {rect.height}") #?print_dimensions
164rect = Rectangle(10, 5) #?create_rect165print(f"Dimensions: {rect.width10} x {rect.height5}") #?print_dimensions166print(f"Area: {rect.area50}") #?print_rect_area167print(f"Perimeter: {rect.perimeter}") #?print_perimeteroutputDimensions: 10 x 5def area(self): #?area_rect_getter
111@property #?area_rect_property112def area(self⟨Rectangle C⟩): #?area_rect_getter113 """Computed: area = width * height."""114 return self._width10 * self._height5 #?calc_rect_areaprint(f"Area: {rect.area}") #?print_rect_area
165print(f"Dimensions: {rect.width} x {rect.height}") #?print_dimensions166print(f"Area: {rect.area50}") #?print_rect_area167print(f"Perimeter: {rect.perimeter30}") #?print_perimeter168print(f"Aspect ratio: {rect.aspect_ratio}") #?print_aspect_ratiooutputArea: 50def perimeter(self): #?perimeter_getter
116@property #?perimeter_property117def perimeter(self⟨Rectangle C⟩): #?perimeter_getter118 """Computed: perimeter = 2 * (width + height)."""119 return 2 * (self._width10 + self._height5) #?calc_perimeterprint(f"Perimeter: {rect.perimeter}") #?print_perimeter
166print(f"Area: {rect.area}") #?print_rect_area167print(f"Perimeter: {rect.perimeter30}") #?print_perimeter168print(f"Aspect ratio: {rect.aspect_ratio2.0}") #?print_aspect_ratio169print(f"Is square: {rect.is_square}") #?print_is_squareoutputPerimeter: 30def aspect_ratio(self): #?aspect_ratio_getter
121@property #?aspect_ratio_property122def aspect_ratio(self⟨Rectangle C⟩): #?aspect_ratio_getter123 """Computed: aspect ratio = width / height."""124 return self._width10 / self._height5 #?calc_aspect_ratioprint(f"Aspect ratio: {rect.aspect_ratio}") #?print_aspect_ratio
167print(f"Perimeter: {rect.perimeter}") #?print_perimeter168print(f"Aspect ratio: {rect.aspect_ratio2.0}") #?print_aspect_ratio169print(f"Is square: {rect.is_squareFalse}") #?print_is_squareoutputAspect ratio: 2.0def is_square(self): #?is_square_getter
pass 1 of 2126@property #?is_square_property127def is_square(self⟨Rectangle C⟩): #?is_square_getter128 """Computed: check if it's a square."""129 return self._width10 == self._height5 #?check_squareprint(f"Is square: {rect.is_square}") #?print_is_square
168print(f"Aspect ratio: {rect.aspect_ratio}") #?print_aspect_ratio169print(f"Is square: {rect.is_squareFalse}") #?print_is_square170171# Make it a square #?make_square172print("\n--- Make it a Square ---")173rect.width = 5 #?set_width_5174print(f"Dimensions: {rect.width} x {rect.height}") #?print_square_dimensionsoutputIs square: False --- Make it a Square ---self._width ← 5
95@width.setter #?width_setter96def width(self⟨Rectangle C⟩, value5): #?width_setter_method97 if value <= 0: #?check_width98 raise ValueError("Width must be positive") #?raise_width99 self._width→ 5 = value5 #?store_valid_widthrect.width ← 5
172print("\n--- Make it a Square ---")173rect.width→ 5 = 5 #?set_width_5174print(f"Dimensions: {rect.width5} x {rect.height5}") #?print_square_dimensions175print(f"Is square: {rect.is_square}") #?print_is_now_squaredef width(self): #?width_getter
pass 2 of 291@property #?width_property92def width(self⟨Rectangle C⟩): #?width_getter93 return self._width5 #?return_widthdef height(self): #?height_getter
pass 2 of 2101@property #?height_property102def height(self⟨Rectangle C⟩): #?height_getter103 return self._height5 #?return_heightprint(f"Dimensions: {rect.width} x {rect.height}") #?print_square_dime…
173rect.width = 5 #?set_width_5174print(f"Dimensions: {rect.width5} x {rect.height5}") #?print_square_dimensions175print(f"Is square: {rect.is_squareTrue}") #?print_is_now_squareoutputDimensions: 5 x 5def is_square(self): #?is_square_getter
pass 2 of 2126@property #?is_square_property127def is_square(self⟨Rectangle C⟩): #?is_square_getter128 """Computed: check if it's a square."""129 return self._width5 == self._height5 #?check_squareprint(f"Is square: {rect.is_square}") #?print_is_now_square
174 print(f"Dimensions: {rect.width} x {rect.height}") #?print_square_dimensions175 print(f"Is square: {rect.is_squareTrue}") #?print_is_now_square176177 print("\n=== Key Points ===")178 print("""1791. Computed properties calculate values on-the-fly1802. No need to store derived values1813. Always consistent with source data1824. Can have setters that update source data1835. Useful for conversions, calculations, flags184 """)outputIs square: True === Key Points === 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, flagsmain()
187if __name__ == "__main__":188 main()
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"
#
"""Person with read-only birth_year."""
6class Person:7 """Person with read-only birth_year."""89 def __init__(self, name, birth_year): #?init_person10 self._name = name #?store_name11 self._birth_year = birth_year #?store_birth_year1213 @property #?birth_year_property14 def birth_year(self): #?birth_year_getter15 """Read-only birth year."""16 return self._birth_year #?return_birth_year1718 # No @birth_year.setter - read-only! #?no_setter1920 @property #?age_property21 def age(self): #?age_getter22 """Computed age based on birth year."""23 current_year = datetime.now().year #?get_current_year24 return current_year - self._birth_year #?calc_age252627class ImmutablePoint:28 """Point with read-only coordinates."""2930 def __init__(self, x, y): #?init_point31 self._x = x #?store_x32 self._y = y #?store_y3334 @property #?x_property35 def x(self): #?x_getter36 """Read-only x coordinate."""37 return self._x #?return_x3839 @property #?y_property40 def y(self): #?y_getter41 """Read-only y coordinate."""42 return self._y #?return_y4344 @property #?distance_property45 def distance_from_origin(self): #?distance_getter46 """Computed distance from origin."""47 return (self._x ** 2 + self._y ** 2) ** 0.5 #?calc_distance484950class BankAccount:51 """Bank account with read-only account number."""5253 def __init__(self, account_number, balance=0): #?init_account54 self._account_number = account_number #?store_account_number55 self._balance = balance #?store_balance56 self._transactions = [] #?init_transactions5758 @property #?account_number_property59 def account_number(self): #?account_number_getter60 """Read-only account number."""61 return self._account_number #?return_account_number6263 @property #?balance_property64 def balance(self): #?balance_getter65 """Read-only balance (use deposit/withdraw)."""66 return self._balance #?return_balance6768 @property #?transactions_property69 def transactions(self): #?transactions_getter70 """Read-only copy of transactions."""71 return self._transactions.copy() # Return copy, not original!7273 def deposit(self, amount): #?deposit_method74 """Deposit money."""75 if amount <= 0: #?check_deposit_amount76 raise ValueError("Deposit must be positive") #?raise_deposit77 self._balance += amount #?add_to_balance78 self._transactions.append(f"Deposit: +${amount}") #?log_deposit7980 def withdraw(self, amount): #?withdraw_method81 """Withdraw money."""82 if amount <= 0: #?check_withdraw_amount83 raise ValueError("Withdrawal must be positive") #?raise_withdraw_amount84 if amount > self._balance: #?check_sufficient_funds85 raise ValueError("Insufficient funds") #?raise_insufficient86 self._balance -= amount #?subtract_from_balance87 self._transactions.append(f"Withdrawal: -${amount}") #?log_withdrawal888990class Counter:91 """Counter with read-only count."""def main():
110def main():111 print("=== Read-only Properties ===\n")112113 # Person with read-only birth year #?person_demo114 print("--- Person with Read-only Birth Year ---")115 person = Person("Alice", 1990) #?create_person116 print(f"Name: {person._name}") #?print_nameoutput=== Read-only Properties === --- Person with Read-only Birth Year ---self._name ← Alice, self._birth_year ← 1990
9def __init__(self⟨Person A⟩, nameAlice, birth_year1990): #?init_person10 self._name→ Alice = nameAlice #?store_name11 self._birth_year→ 1990 = birth_year1990 #?store_birth_yearperson ← ⟨Person A⟩
114print("--- Person with Read-only Birth Year ---")115person→ ⟨Person A⟩ = Person("Alice", 1990) #?create_person116print(f"Name: {person._nameAlice}") #?print_name117print(f"Birth year: {person.birth_year1990}") #?print_birth_year118print(f"Age: {person.age}") #?print_ageoutputName: Alicedef birth_year(self): #?birth_year_getter
13@property #?birth_year_property14def birth_year(self⟨Person A⟩): #?birth_year_getter15 """Read-only birth year."""16 return self._birth_year1990 #?return_birth_yearprint(f"Birth year: {person.birth_year}") #?print_birth_year
116print(f"Name: {person._name}") #?print_name117print(f"Birth year: {person.birth_year1990}") #?print_birth_year118print(f"Age: {person.age36}") #?print_ageoutputBirth year: 1990current_year ← 2026
20@property #?age_property21def age(self⟨Person A⟩): #?age_getter22 """Computed age based on birth year."""23 current_year→ 2026 = datetime<class 'datetime.datetime'>.now().year #?get_current_year24 return current_year2026 - self._birth_year1990 #?calc_ageprint(f"Age: {person.age}") #?print_age
117print(f"Birth year: {person.birth_year}") #?print_birth_year118print(f"Age: {person.age36}") #?print_ageoutputAge: 36except AttributeError as e: #?catch_attribute_error
121 person.birth_year = 1995 # Attempt to modify122except AttributeError as e: #?catch_attribute_error123 print(f"Error: {eproperty 'birth_year' of 'Person' object has no setter}") #?print_readonly_error124125# Immutable point #?point_demooutputError: property 'birth_year' of 'Person' object has no setter Error: property 'birth_year' of 'Person' object has no setterprint(" --- Immutable Point ---")
125# Immutable point #?point_demo126print("\n--- Immutable Point ---")127point = ImmutablePoint(3, 4) #?create_point128print(f"Point: ({point.x}, {point.y})") #?print_pointoutput --- Immutable Point ---self._x ← 3, self._y ← 4
30def __init__(self⟨ImmutablePoint B⟩, x3, y4): #?init_point31 self._x→ 3 = x3 #?store_x32 self._y→ 4 = y4 #?store_ypoint ← ⟨ImmutablePoint B⟩
126print("\n--- Immutable Point ---")127point→ ⟨ImmutablePoint B⟩ = ImmutablePoint(3, 4) #?create_point128print(f"Point: ({point.x3}, {point.y4})") #?print_point129print(f"Distance from origin: {point.distance_from_origin}") #?print_distancedef x(self): #?x_getter
34@property #?x_property35def x(self⟨ImmutablePoint B⟩): #?x_getter36 """Read-only x coordinate."""37 return self._x3 #?return_xdef y(self): #?y_getter
39@property #?y_property40def y(self⟨ImmutablePoint B⟩): #?y_getter41 """Read-only y coordinate."""42 return self._y4 #?return_yprint(f"Point: ({point.x}, {point.y})") #?print_point
127point = ImmutablePoint(3, 4) #?create_point128print(f"Point: ({point.x3}, {point.y4})") #?print_point129print(f"Distance from origin: {point.distance_from_origin5.0}") #?print_distanceoutputPoint: (3, 4)def distance_from_origin(self): #?distance_getter
44@property #?distance_property45def distance_from_origin(self⟨ImmutablePoint B⟩): #?distance_getter46 """Computed distance from origin."""47 return (self._x3 ** 2 + self._y4 ** 2) ** 0.5 #?calc_distanceprint(f"Distance from origin: {point.distance_from_origin}") #?print_d…
128print(f"Point: ({point.x}, {point.y})") #?print_point129print(f"Distance from origin: {point.distance_from_origin5.0}") #?print_distanceoutputDistance from origin: 5.0except AttributeError as e: #?catch_point_error
132 point.x = 10 #?attempt_modify_x133except AttributeError as e: #?catch_point_error134 print(f"Error: {eproperty 'x' of 'ImmutablePoint' object has no setter}") #?print_point_error135136# Bank account #?account_demooutputError: property 'x' of 'ImmutablePoint' object has no setter Error: property 'x' of 'ImmutablePoint' object has no setterprint(" --- Bank Account ---")
136# Bank account #?account_demo137print("\n--- Bank Account ---")138account = BankAccount("12345", 1000) #?create_account139print(f"Account: {account.account_number}") #?print_account_numberoutput --- Bank Account ---self._account_number ← 12345, self._balance ← 1000, self._transactions ← []
53def __init__(self⟨BankAccount C⟩, account_number12345, balance1000=0): #?init_account54 self._account_number→ 12345 = account_number12345 #?store_account_number55 self._balance→ 1000 = balance1000 #?store_balance56 self._transactions→ [] = [] #?init_transactionsaccount ← ⟨BankAccount C⟩
137print("\n--- Bank Account ---")138account→ ⟨BankAccount C⟩ = BankAccount("12345", 1000) #?create_account139print(f"Account: {account.account_number12345}") #?print_account_number140print(f"Balance: ${account.balance}") #?print_balancedef account_number(self): #?account_number_getter
58@property #?account_number_property59def account_number(self⟨BankAccount C⟩): #?account_number_getter60 """Read-only account number."""61 return self._account_number12345 #?return_account_numberprint(f"Account: {account.account_number}") #?print_account_number
138account = BankAccount("12345", 1000) #?create_account139print(f"Account: {account.account_number12345}") #?print_account_number140print(f"Balance: ${account.balance1000}") #?print_balanceoutputAccount: 12345def balance(self): #?balance_getter
pass 1 of 263@property #?balance_property64def balance(self⟨BankAccount C⟩): #?balance_getter65 """Read-only balance (use deposit/withdraw)."""66 return self._balance1000 #?return_balanceprint(f"Balance: ${account.balance}") #?print_balance
139print(f"Account: {account.account_number}") #?print_account_number140print(f"Balance: ${account.balance1000}") #?print_balance141142account⟨BankAccount C⟩.deposit(500) #?do_deposit143account.withdraw(200) #?do_withdrawoutputBalance: $1000self._balance ← 1500, self._transactions ← ['Deposit: +$500']
73def deposit(self⟨BankAccount C⟩, amount500): #?deposit_method74 """Deposit money."""75 if amount <= 0: #?check_deposit_amount76 raise ValueError("Deposit must be positive") #?raise_deposit77 self._balance→ 1500 += amount500 #?add_to_balance78 self._transactions→ ['Deposit: +$500'].append(f"Deposit: +${amount500}") #?log_depositaccount.deposit(500) #?do_deposit
142account⟨BankAccount C⟩.deposit(500) #?do_deposit143account⟨BankAccount C⟩.withdraw(200) #?do_withdraw144print(f"New balance: ${account.balance}") #?print_new_balanceself._balance ← 1300, self._transactions ← ['Deposit: +$500', 'Withdrawal: -$200']
80def withdraw(self⟨BankAccount C⟩, amount200): #?withdraw_method81 """Withdraw money."""82 if amount <= 0: #?check_withdraw_amount83 raise ValueError("Withdrawal must be positive") #?raise_withdraw_amount84 if amount > self._balance: #?check_sufficient_funds85 raise ValueError("Insufficient funds") #?raise_insufficient86 self._balance→ 1300 -= amount200 #?subtract_from_balance87 self._transactions→ ['Deposit: +$500', 'Withdrawal: -$200'].append(f"Withdrawal: -${amount200}") #?log_withdrawalaccount.withdraw(200) #?do_withdraw
142account.deposit(500) #?do_deposit143account⟨BankAccount C⟩.withdraw(200) #?do_withdraw144print(f"New balance: ${account.balance1300}") #?print_new_balancedef balance(self): #?balance_getter
pass 2 of 263@property #?balance_property64def balance(self⟨BankAccount C⟩): #?balance_getter65 """Read-only balance (use deposit/withdraw)."""66 return self._balance1300 #?return_balanceprint(f"New balance: ${account.balance}") #?print_new_balance
143account.withdraw(200) #?do_withdraw144print(f"New balance: ${account.balance1300}") #?print_new_balance145146print("Transactions:") #?print_transactions_header147for transaction in account.transactions: #?loop_transactionsoutputNew balance: $1300 Transactions:def transactions(self): #?transactions_getter
68@property #?transactions_property69def transactions(self⟨BankAccount C⟩): #?transactions_getter70 """Read-only copy of transactions."""71 return self._transactions['Deposit: +$500', 'Withdrawal: -$200'].copy() # Return copy, not original!for transaction in account.transactions: #?loop_transactions
pass 1 of 2146print("Transactions:") #?print_transactions_header147for transactionDeposit: +$500 in account.transactions['Deposit: +$500', 'Withdrawal: -$200']: #?loop_transactions148 print(f" - {transactionDeposit: +$500}") #?print_transactionoutput - Deposit: +$500for transaction in account.transactions: #?loop_transactions
pass 2 of 2146print("Transactions:") #?print_transactions_header147for transactionWithdrawal: -$200 in account.transactions['Deposit: +$500', 'Withdrawal: -$200']: #?loop_transactions148 print(f" - {transactionWithdrawal: -$200}") #?print_transactionoutput - Withdrawal: -$200except AttributeError as e: #?catch_balance_error
151 account.balance = 999999 # Attempt to cheat152except AttributeError as e: #?catch_balance_error153 print(f"Error: {eproperty 'balance' of 'BankAccount' object has no setter}") #?print_balance_error154155# Counter #?counter_demooutputError: property 'balance' of 'BankAccount' object has no setter Error: property 'balance' of 'BankAccount' object has no setterprint(" --- Counter ---")
155# Counter #?counter_demo156print("\n--- Counter ---")157counter = Counter() #?create_counter158print(f"Initial count: {counter.count}") #?print_initial_countoutput --- Counter ---self._count ← 0
93def __init__(self⟨Counter D⟩): #?init_counter94 self._count→ 0 = 0 #?init_countcounter ← ⟨Counter D⟩
156print("\n--- Counter ---")157counter→ ⟨Counter D⟩ = Counter() #?create_counter158print(f"Initial count: {counter.count0}") #?print_initial_countdef count(self): #?count_getter
pass 1 of 396@property #?count_property97def count(self⟨Counter D⟩): #?count_getter98 """Read-only count."""99 return self._count0 #?return_countAll 3 passes — pass 1 is the card above pass self._count1 0 2 3 3 0 print(f"Initial count: {counter.count}") #?print_initial_count
157counter = Counter() #?create_counter158print(f"Initial count: {counter.count0}") #?print_initial_count159160counter⟨Counter D⟩.increment() #?inc1161counter.increment() #?inc2outputInitial count: 0self._count ← 1
pass 1 of 3101def increment(self⟨Counter D⟩): #?increment_method102 """Increment counter."""103 self._count→ 1 += 1 #?do_incrementAll 3 passes — pass 1 is the card above pass self._count1 0 → 1 2 1 → 2 3 2 → 3 counter.increment() #?inc1
160counter⟨Counter D⟩.increment() #?inc1161counter⟨Counter D⟩.increment() #?inc2162counter.increment() #?inc3counter.increment() #?inc2
160counter.increment() #?inc1161counter⟨Counter D⟩.increment() #?inc2162counter⟨Counter D⟩.increment() #?inc3163print(f"After increments: {counter.count}") #?print_after_incrementscounter.increment() #?inc3
161counter.increment() #?inc2162counter⟨Counter D⟩.increment() #?inc3163print(f"After increments: {counter.count3}") #?print_after_incrementsprint(f"After increments: {counter.count}") #?print_after_increments
162counter.increment() #?inc3163print(f"After increments: {counter.count3}") #?print_after_incrementsoutputAfter increments: 3except AttributeError as e: #?catch_count_error
166 counter.count = 100 #?attempt_modify_count167except AttributeError as e: #?catch_count_error168 print(f"Error: {eproperty 'count' of 'Counter' object has no setter}") #?print_count_error169170counter.reset() #?do_resetoutputError: property 'count' of 'Counter' object has no setter Error: property 'count' of 'Counter' object has no settercounter.reset() #?do_reset
170counter⟨Counter D⟩.reset() #?do_reset171print(f"After reset: {counter.count}") #?print_after_resetself._count ← 0
105def reset(self⟨Counter D⟩): #?reset_method106 """Reset counter."""107 self._count→ 0 = 0 #?do_resetcounter.reset() #?do_reset
170counter⟨Counter D⟩.reset() #?do_reset171print(f"After reset: {counter.count0}") #?print_after_resetprint(f"After reset: {counter.count}") #?print_after_reset
170 counter.reset() #?do_reset171 print(f"After reset: {counter.count0}") #?print_after_reset172173 print("\n=== Key Points ===")174 print("""1751. Property without setter is read-only1762. Attempting to set raises AttributeError1773. Useful for IDs, computed values, immutable data1784. Provide methods (deposit/withdraw) instead of direct access1795. Return copies of mutable internal data180 """)outputAfter reset: 0 === Key Points === 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 datamain()
183if __name__ == "__main__":184 main()
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"
#
"""Original version with direct attribute access."""
4class UserV1:5 """Original version with direct attribute access."""67 def __init__(self, username, email): #?init_v18 self.username = username # Direct attribute9 self.email = email # Direct attribute101112# Version 2: Using properties (new way - backward compatible!) #?version213class UserV2:14 """Refactored with properties - same interface!"""1516 def __init__(self, username, email): #?init_v217 self.username = username # Uses setter18 self.email = email # Uses setter1920 @property #?username_property_v221 def username(self): #?username_getter_v222 """Get username."""23 return self._username #?return_username_v22425 @username.setter #?username_setter_v226 def username(self, value): #?username_setter_method_v227 """Set username with validation (NEW!)."""28 if not isinstance(value, str) or len(value) < 3: #?validate_username_v229 raise ValueError("Username must be string with 3+ chars") #?raise_username_v230 self._username = value #?store_username_v23132 @property #?email_property_v233 def email(self): #?email_getter_v234 """Get email."""35 return self._email #?return_email_v23637 @email.setter #?email_setter_v238 def email(self, value): #?email_setter_method_v239 """Set email with validation (NEW!)."""40 if "@" not in value: #?validate_email_v241 raise ValueError("Invalid email format") #?raise_email_v242 self._email = value.lower() # Normalize to lowercase (NEW!)434445# Old API - direct access #?old_api46class LegacyCart:47 """Old shopping cart with direct access."""4849 def __init__(self): #?init_legacy_cart50 self.items = [] #?direct_items51 self.total = 0 #?direct_total5253 def add_item(self, price): #?add_item_legacy54 """Add item (user must update total manually - error-prone!)."""55 self.items.append(price) #?append_item56 # User responsible for updating total! #?manual_total575859# New API - using properties #?new_api60class ModernCart:61 """Modernized cart with computed total."""def main():
84def main():85 print("=== Migrating to Properties ===\n")8687 # Version 1: Direct attribute access #?demo_v188 print("--- Version 1: Direct Attributes ---")89 user_v1 = UserV1("alice", "ALICE@EXAMPLE.COM") #?create_v190 print(f"Username: {user_v1.username}") #?print_v1_usernameoutput=== Migrating to Properties === --- Version 1: Direct Attributes ---self.username ← alice, self.email ← ALICE@EXAMPLE.COM
7def __init__(self⟨UserV1 A⟩, usernamealice, emailALICE@EXAMPLE.COM): #?init_v18 self.username→ alice = usernamealice # Direct attribute9 self.email→ ALICE@EXAMPLE.COM = emailALICE@EXAMPLE.COM # Direct attributeuser_v1 ← ⟨UserV1 A⟩, user_v1.username ← ab, user_v1.email ← not-an-email
88print("--- Version 1: Direct Attributes ---")89user_v1→ ⟨UserV1 A⟩ = UserV1("alice", "ALICE@EXAMPLE.COM") #?create_v190print(f"Username: {user_v1.usernamealice}") #?print_v1_username91print(f"Email: {user_v1.emailALICE@EXAMPLE.COM}") #?print_v1_email9293# Problems with V1: #?v1_problems94user_v1.username→ ab = "ab" # No validation - BAD DATA!95user_v1.email→ not-an-email = "not-an-email" # No validation - BAD DATA!96print(f"Invalid username accepted: {user_v1.usernameab}") #?print_invalid_v197print(f"Invalid email accepted: {user_v1.emailnot-an-email}") #?print_invalid_email_v19899# Version 2: Properties (backward compatible interface!) #?demo_v2100print("\n--- Version 2: Properties ---")101user_v2 = UserV2("alice", "ALICE@EXAMPLE.COM") #?create_v2102print(f"Username: {user_v2.username}") # Same interface!outputUsername: alice Email: ALICE@EXAMPLE.COM Invalid username accepted: ab Invalid email accepted: not-an-email --- Version 2: Properties ---def __init__(self, username, email): #?init_v2
16def __init__(self⟨UserV2 B⟩, usernamealice, emailALICE@EXAMPLE.COM): #?init_v217 self.username = usernamealice # Uses setter18 self.email = email # Uses setterself._username ← alice, self.username ← alice
pass 1 of 216def __init__(self, username, email): #?init_v217 self.username→ alice = usernamealice # Uses setter18 self.email = emailALICE@EXAMPLE.COM # Uses setter1920@property #?username_property_v221def username(self): #?username_getter_v222 """Get username."""23 return self._username #?return_username_v22425@username.setter #?username_setter_v226def username(self⟨UserV2 B⟩, valuealice): #?username_setter_method_v227 """Set username with validation (NEW!)."""28 if not isinstance(value, str) or len(value) < 3: #?validate_username_v229 raise ValueError("Username must be string with 3+ chars") #?raise_username_v230 self._username→ alice = valuealice #?store_username_v2self._email ← alice@example.com, self.email ← alice@example.com
17 self.username = username # Uses setter18 self.email→ alice@example.com = emailALICE@EXAMPLE.COM # Uses setter1920@property #?username_property_v221def username(self): #?username_getter_v222 """Get username."""23 return self._username #?return_username_v22425@username.setter #?username_setter_v226def username(self, value): #?username_setter_method_v227 """Set username with validation (NEW!)."""28 if not isinstance(value, str) or len(value) < 3: #?validate_username_v229 raise ValueError("Username must be string with 3+ chars") #?raise_username_v230 self._username = value #?store_username_v23132@property #?email_property_v233def email(self): #?email_getter_v234 """Get email."""35 return self._email #?return_email_v23637@email.setter #?email_setter_v238def email(self⟨UserV2 B⟩, valueALICE@EXAMPLE.COM): #?email_setter_method_v239 """Set email with validation (NEW!)."""40 if "@" not in value: #?validate_email_v241 raise ValueError("Invalid email format") #?raise_email_v242 self._email→ alice@example.com = valueALICE@EXAMPLE.COM.lower() # Normalize to lowercase (NEW!)user_v2 ← ⟨UserV2 B⟩
100print("\n--- Version 2: Properties ---")101user_v2→ ⟨UserV2 B⟩ = UserV2("alice", "ALICE@EXAMPLE.COM") #?create_v2102print(f"Username: {user_v2.usernamealice}") # Same interface!103print(f"Email: {user_v2.email}") # But normalized!def username(self): #?username_getter_v2
20@property #?username_property_v221def username(self⟨UserV2 B⟩): #?username_getter_v222 """Get username."""23 return self._usernamealice #?return_username_v2print(f"Username: {user_v2.username}") # Same interface!
101user_v2 = UserV2("alice", "ALICE@EXAMPLE.COM") #?create_v2102print(f"Username: {user_v2.usernamealice}") # Same interface!103print(f"Email: {user_v2.emailalice@example.com}") # But normalized!outputUsername: alicedef email(self): #?email_getter_v2
32@property #?email_property_v233def email(self⟨UserV2 B⟩): #?email_getter_v234 """Get email."""35 return self._emailalice@example.com #?return_email_v2print(f"Email: {user_v2.email}") # But normalized!
102print(f"Username: {user_v2.username}") # Same interface!103print(f"Email: {user_v2.emailalice@example.com}") # But normalized!outputEmail: alice@example.comdef username(self, value): #?username_setter_method_v2
pass 2 of 225@username.setter #?username_setter_v226def username(self⟨UserV2 B⟩, valueab): #?username_setter_method_v227 """Set username with validation (NEW!)."""28 if not isinstance(value, str) or len(value) < 3: #?validate_username_v2if not isinstance(value, str) or len(value) < 3: #?validate_username_v…
27"""Set username with validation (NEW!)."""28if not isinstance(valueab, str) or len(value) < 3: #?validate_username_v229 raise ValueError("Username must be string with 3+ chars") #?raise_username_v230self._username = value #?store_username_v2except ValueError as e: #?catch_v2_error
107 user_v2.username = "ab" # Now validated!108except ValueError as e: #?catch_v2_error109 print(f"Validation error: {eUsername must be string with 3+ chars}") #?print_v2_error110111# Legacy cart problems #?legacy_cart_demooutputValidation error: Username must be string with 3+ chars Validation error: Username must be string with 3+ charsprint(" --- Legacy Cart (Manual Total) ---")
111# Legacy cart problems #?legacy_cart_demo112print("\n--- Legacy Cart (Manual Total) ---")113legacy = LegacyCart() #?create_legacy114legacy.add_item(10.00) #?add_legacy_1output --- Legacy Cart (Manual Total) ---self.items ← [], self.total ← 0
49def __init__(self⟨LegacyCart C⟩): #?init_legacy_cart50 self.items→ [] = [] #?direct_items51 self.total→ 0 = 0 #?direct_totallegacy ← ⟨LegacyCart C⟩
112print("\n--- Legacy Cart (Manual Total) ---")113legacy→ ⟨LegacyCart C⟩ = LegacyCart() #?create_legacy114legacy⟨LegacyCart C⟩.add_item(10.00) #?add_legacy_1115legacy.add_item(20.00) #?add_legacy_2self.items ← [10.0]
pass 1 of 253def add_item(self⟨LegacyCart C⟩, price10.0): #?add_item_legacy54 """Add item (user must update total manually - error-prone!)."""55 self.items→ [10.0].append(price10.0) #?append_item56 # User responsible for updating total! #?manual_totallegacy.add_item(10.00) #?add_legacy_1
113legacy = LegacyCart() #?create_legacy114legacy⟨LegacyCart C⟩.add_item(10.00) #?add_legacy_1115legacy⟨LegacyCart C⟩.add_item(20.00) #?add_legacy_2116print(f"Items: {legacy.items}") #?print_legacy_itemsself.items ← [10.0, 20.0]
pass 2 of 253def add_item(self⟨LegacyCart C⟩, price20.0): #?add_item_legacy54 """Add item (user must update total manually - error-prone!)."""55 self.items→ [10.0, 20.0].append(price20.0) #?append_item56 # User responsible for updating total! #?manual_totallegacy.total ← 30.0
114legacy.add_item(10.00) #?add_legacy_1115legacy⟨LegacyCart C⟩.add_item(20.00) #?add_legacy_2116print(f"Items: {legacy.items[10.0, 20.0]}") #?print_legacy_items117print(f"Total: ${legacy.total0}") # Still 0 - forgot to update!118119# Forgot to update total! #?forgot_total120legacy.total→ 30.0 = 30.00 # Manual and error-prone121print(f"After manual update: ${legacy.total30.0}") #?print_manual_total122123# Modern cart with computed property #?modern_cart_demo124print("\n--- Modern Cart (Computed Total) ---")125modern = ModernCart() #?create_modern126modern.add_item(10.00) #?add_modern_1outputItems: [10.0, 20.0] Total: $0 After manual update: $30.0 --- Modern Cart (Computed Total) ---self._items ← []
63def __init__(self⟨ModernCart D⟩): #?init_modern_cart64 self._items→ [] = [] #?private_itemsmodern ← ⟨ModernCart D⟩
124print("\n--- Modern Cart (Computed Total) ---")125modern→ ⟨ModernCart D⟩ = ModernCart() #?create_modern126modern⟨ModernCart D⟩.add_item(10.00) #?add_modern_1127modern.add_item(20.00) #?add_modern_2self._items ← [10.0]
pass 1 of 376def add_item(self⟨ModernCart D⟩, price10.0): #?add_item_modern77 """Add item (total auto-updates!)."""78 if price < 0: #?validate_price79 raise ValueError("Price cannot be negative") #?raise_price80 self._items→ [10.0].append(price10.0) #?append_modern_item81 # No manual total update needed! #?auto_totalAll 3 passes — pass 1 is the card above pass priceself._items1 10.0 [] → [10.0] 2 20.0 [10.0] → [10.0, 20.0] 3 15.0 [10.0, 20.0] → [10.0, 20.0, 15.0] modern.add_item(10.00) #?add_modern_1
125modern = ModernCart() #?create_modern126modern⟨ModernCart D⟩.add_item(10.00) #?add_modern_1127modern⟨ModernCart D⟩.add_item(20.00) #?add_modern_2128print(f"Items: {modern.items}") #?print_modern_itemsmodern.add_item(20.00) #?add_modern_2
126modern.add_item(10.00) #?add_modern_1127modern⟨ModernCart D⟩.add_item(20.00) #?add_modern_2128print(f"Items: {modern.items[10.0, 20.0]}") #?print_modern_items129print(f"Total: ${modern.total}") # Always correct!def items(self): #?items_getter
66@property #?items_property67def items(self⟨ModernCart D⟩): #?items_getter68 """Get items (read-only copy)."""69 return self._items[10.0, 20.0].copy() #?return_items_copyprint(f"Items: {modern.items}") #?print_modern_items
127modern.add_item(20.00) #?add_modern_2128print(f"Items: {modern.items[10.0, 20.0]}") #?print_modern_items129print(f"Total: ${modern.total30.0}") # Always correct!outputItems: [10.0, 20.0]def total(self): #?total_getter
pass 1 of 271@property #?total_property72def total(self⟨ModernCart D⟩): #?total_getter73 """Computed total (always accurate!)."""74 return sum(self._items[10.0, 20.0]) #?calc_totalprint(f"Total: ${modern.total}") # Always correct!
128print(f"Items: {modern.items}") #?print_modern_items129print(f"Total: ${modern.total30.0}") # Always correct!130131modern⟨ModernCart D⟩.add_item(15.00) #?add_modern_3132print(f"After adding item: ${modern.total}") # Auto-updated!outputTotal: $30.0modern.add_item(15.00) #?add_modern_3
131modern⟨ModernCart D⟩.add_item(15.00) #?add_modern_3132print(f"After adding item: ${modern.total45.0}") # Auto-updated!def total(self): #?total_getter
pass 2 of 271@property #?total_property72def total(self⟨ModernCart D⟩): #?total_getter73 """Computed total (always accurate!)."""74 return sum(self._items[10.0, 20.0, 15.0]) #?calc_totalprint(f"After adding item: ${modern.total}") # Auto-updated!
131modern.add_item(15.00) #?add_modern_3132print(f"After adding item: ${modern.total45.0}") # Auto-updated!outputAfter adding item: $45.0except AttributeError as e: #?catch_total_error
136 modern.total = 9999 #?attempt_set_total137except AttributeError as e: #?catch_total_error138 print(f"Error: {eproperty 'total' of 'ModernCart' object has no setter}") #?print_total_error139140print("\n=== Migration Benefits ===")outputError: property 'total' of 'ModernCart' object has no setter Error: property 'total' of 'ModernCart' object has no setterprint(" === Migration Benefits ===")
140 print("\n=== Migration Benefits ===")141 print("""1421. Backward compatible - same interface1432. Add validation without breaking existing code1443. Transform data (e.g., normalize email)1454. Computed values always accurate1465. Prevent incorrect manual updates1476. Read-only computed properties148 """)output === Migration Benefits === 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 propertiesmain()
151if __name__ == "__main__":152 main()
Start with plain attribute. Add property later. Callers don't change.
Exercise: practical.py
Build a temperature class with Celsius/Fahrenheit properties