A summary of the class-based assignments and advanced concepts in this deck.
Create a class called Book. The __init__ method should take title, author, and pages as arguments and store them as attributes.
Then, instantiate two Book objects with the following details:
Finally, print the title of the first book and the author of the second book.
Write your code below and click Run:
Loading Python…Modify the Book class from Assignment 1.
Add a method called get_summary() that returns a string in the following format:
"Title by Author, X pages"
Create a Book object for "The Hobbit" and call the get_summary() method on it. Print the returned string.
Write your code below and click Run:
Loading Python…Model a simple light switch by creating a class called LightSwitch.
__init__, create an attribute is_on and set its default to False.turn_on() that sets is_on to True and prints a confirmation.turn_off() that sets is_on to False and prints a confirmation.get_status() that prints the current state ("ON" or "OFF").Instantiate a LightSwitch object and test all its methods.
Write your code below and click Run:
Loading Python…Model a product for an e-commerce store by creating a class called Product.
__init__ should accept name, price, and quantity_in_stock.calculate_total_value() that returns the price * quantity.sell(amount) that reduces the quantity. Ensure you can't sell more than you have in stock.Create a "Laptop" product (price $1200, quantity 10) and test your methods.
Write your code below and click Run:
Loading Python…Create a class Circle that is initialized with a radius.
__init__ method should store the radius.calculate_area() that returns the area (π * r²).calculate_circumference() that returns the circumference (2 * π * r).You can use 3.14159 for π. Create a Circle with a radius of 5 and print its area and circumference.
Write your code below and click Run:
Loading Python…Add a new method to the Student class from our lecture.
Student class that has name, age, and grade.set_grade(self, new_grade) to update a student's grade.Create a student, update their grade with a valid value, then try to update it with an invalid value to test your logic.
Write your code below and click Run:
Loading Python…Model the relationship between a pet and its owner using two classes.
Pet Class:
__init__ takes name and species.get_info() returns a string like "Fido is a Dog".Owner Class:
__init__ takes owner_name and creates an empty list called pets.add_pet(pet_object) adds a Pet to the list.show_pets() prints info for all pets.Write your code below and click Run:
Loading Python…Imagine you are designing a simple role-playing game. Describe how you would create a Player class. This is a design question; no code is required.
Player object need? (List at least four).Player object be able to perform? (List at least three).Player class demonstrate the concept of Encapsulation?Write your code below and click Run:
Loading Python…How do we prevent users from setting invalid values, like a negative resistance?
Python doesn't have true "private" variables, but uses naming conventions:
class PowerSupply:
def __init__(self, voltage, current_limit):
self._max_voltage = 24.0 # Protected
self.__internal_temp = 25.0 # "Private"
self.set_voltage(voltage) # Use a method for validation
self.current_limit = current_limit
def set_voltage(self, new_voltage):
if 0 <= new_voltage <= self._max_voltage:
self.voltage = new_voltage
print(f"Voltage set to {self.voltage}V")
else:
print(f"Error: Voltage must be 0-{self._max_voltage}V")
psu = PowerSupply(5.0, 1.0)
psu.set_voltage(30.0) # Error: Voltage must be 0-24.0V
# You can still do this, but the underscore warns you not to.
# psu._max_voltage = 50.0
# This will cause an AttributeError due to name mangling.
# print(psu.__internal_temp)
Think of it like a security guard at a door. Without `@property`, anyone can walk in and set any value they want — even bad ones. With `@property`, you put a guard at the door: when someone reads the value, the @property method (the getter) hands it over. When someone writes a value, the @name.setter method checks it first — and rejects it if it's invalid.
The beauty is that from the outside, it still looks like normal attribute access (resistor.resistance = 1000). The user of your class doesn't need to know there's a guard — it just works. But behind the scenes, your validation code runs every time.
@property — turns a method into a "getter": controls what happens when you read the attribute.@name.setter — turns a method into a "setter": controls what happens when you assign to the attribute (e.g., reject negative values).@property without a setter, the attribute becomes read-only.class Resistor:
def __init__(self, resistance_ohms):
# Use the setter property during initialization
self.resistance = resistance_ohms
@property
def resistance(self):
"""The 'getter' for resistance."""
return self._resistance
@resistance.setter
def resistance(self, value):
"""The 'setter' with validation."""
if value < 0:
raise ValueError("Resistance cannot be negative.")
self._resistance = value
r1 = Resistor(1000)
print(f"Resistance: {r1.resistance} Ω") # Looks like simple access
r1.resistance = 2200 # The setter method is called automatically
print(f"New Resistance: {r1.resistance} Ω")
# This will raise a ValueError thanks to our setter.
# r1.resistance = -500
### Attrebuite-like methods
class Circle:
def __init__(self, radius):
self._radius = radius
@property
def area(self):
return 3.14159 * self._radius ** 2
# Usage
circle = Circle(5)
print(circle.area) # No parentheses needed! Prints: 78.53975
### Read-only Property
class Person:
def __init__(self, first_name, last_name):
self.first_name = first_name
self.last_name = last_name
@property
def full_name(self):
return f"{self.first_name} {self.last_name}"
person = Person("John", "Doe")
print(person.full_name) # "John Doe"
# person.full_name = "Jane Smith" # This would raise an error!
Attributes can belong to an individual object (instance) or to the entire class.
class Capacitor:
# Class attribute: shared by all Capacitor objects
DIELECTRIC_MATERIAL = "Ceramic"
def __init__(self, capacitance_farads):
# Instance attribute: unique to each capacitor
self.capacitance = capacitance_farads
print(Capacitor.DIELECTRIC_MATERIAL)
c1 = Capacitor(1e-6) # 1 microfarad
c2 = Capacitor(10e-6) # 10 microfarads
# Both instances share the same class attribute
print(f"C1 Material: {c1.DIELECTRIC_MATERIAL}") # Ceramic
print(f"C2 Material: {c2.DIELECTRIC_MATERIAL}") # Ceramic
# But they have unique instance attributes
print(f"C1 Capacitance: {c1.capacitance}F") # 1e-06F
print(f"C2 Capacitance: {c2.capacitance}F") # 1e-05F
A static method doesn't receive the instance (`self`) or the class (`cls`) as an argument. It's essentially a regular function that is namespaced within the class.
Use them for utility functions that are logically related to the class but don't need to access any class or instance data.
class CircuitUtils:
@staticmethod
def parallel_resistance(resistors):
"""Calculate total resistance for resistors in parallel."""
# Note: no 'self' or 'cls' needed
if not resistors: return 0
inverse_sum = sum(1 / r for r in resistors)
return 1 / inverse_sum
@staticmethod
def ohms_law_voltage(i, r):
"""Calculate voltage using Ohm's Law (V=IR)."""
return i * r
# Call the static method directly on the class
r_parallel = [1000, 2000, 1000]
total_r = CircuitUtils.parallel_resistance(r_parallel)
print(f"Total Parallel Resistance: {total_r:.2f} Ω") # 400.00 Ω
voltage = CircuitUtils.ohms_law_voltage(i=0.05, r=100) # 5A * 100Ω
print(f"Voltage: {voltage}V") # 5.0V
Special methods, or "dunder" (double underscore) methods, let your objects integrate with Python's built-in behavior.
class Resistor:
def __init__(self, resistance_ohms):
self.resistance = resistance_ohms
def __str__(self):
# User-friendly output
if self.resistance >= 1e6:
return f"{self.resistance/1e6:.1f} MΩ Resistor"
if self.resistance >= 1e3:
return f"{self.resistance/1e3:.1f} kΩ Resistor"
return f"{self.resistance} Ω Resistor"
r_kilo = Resistor(2200)
r_mega = Resistor(4700000)
print(r_kilo) # Calls __str__: 2.2 kΩ Resistor
print(str(r_mega)) # Calls __str__: 4.7 MΩ Resistor
You can define how standard operators like `+`, `-`, `*` work on your objects by implementing their corresponding dunder methods.
This can make your code incredibly intuitive. For example, what should `resistor1 + resistor2` mean? It could represent connecting them in series!
class Resistor:
def __init__(self, resistance):
self.resistance = resistance
def __add__(self, other):
"""Overloads the + operator for series connection."""
if not isinstance(other, Resistor):
return NotImplemented
# Resistors in series: R_total = R1 + R2
new_resistance = self.resistance + other.resistance
return Resistor(new_resistance)
r1 = Resistor(1000)
r2 = Resistor(2200)
# This now calls the __add__ method we defined!
series_r = r1 + r2
print(f"R1: {r1}") # Resistor(1000)
print(f"R2: {r2}") # Resistor(2200)
print(f"R1 and R2 in series: {series_r}") # Resistor(3200)
You can make your objects behave like lists or dictionaries.
Let's model a digital signal as a collection of samples.
class DigitalSignal:
def __init__(self, samples):
# samples is a list of 0s and 1s
self._samples = list(samples)
def __len__(self):
"""Return the number of samples in the signal."""
return len(self._samples)
def __getitem__(self, index):
"""Allow accessing samples by index."""
return self._samples[index]
# A simple clock signal
clk_signal = DigitalSignal([0, 1, 0, 1, 0, 1, 0, 1])
print(f"Signal has {len(clk_signal)} samples.") # Calls __len__
print(f"Sample at index 3 is: {clk_signal[3]}") # Calls __getitem__
# We can even iterate over it because it has __getitem__!
for sample in clk_signal:
print(sample, end=' ')