Defining attributes and methods for the child class
- let’s add an attribute specific to electric cars (like a battery) and a method to report on this attribute
class Car:
….–snip–
class ElectricCar(Car):
….”””Represent aspects of a car, specific to electric vehicles.”””
….def __init__(self, make, model, year):
……..”””
……..Initialize attributes of the parent class.
……..Then initialize attributes specific to an electric car.
……..”””
……..super().__init__(make, model, year)
……..self.battery_size = 40
….def describe_battery(self):
……..”””Print a statement describing the battery size.”””
……..print(f”This car has a {self.battery_size}-kWh battery.”)
my_leaf = ElectricCar(‘nissan’, ‘leag’, 2024)
print(my_leaf.get_descriptive_name())
my_leaf.describe_battery()
- added a new attribute self.battery_size and set its initial value to 40
- attribute will be associated with all instances created from the ElectricCar class but won’t be associated with any instances of Car
- added a method called describe_battery() that prints information about the battery
- when we call this method, we get a description specific to an electric car
2024 Nissan Leaf
This car has a 40-kWh battery
Overriding methods from the parent class
- you can override any method form the parent class that won’t work with the child class
- define a method in the child class with the same name as the method you want to override in the parent class
- for example, class Car has a method called fill_gas_tank(), this would be meaningless for an all-electric vehicle
class ElectricCar(Car):
….–snip–
….def fill_gas_tank(self):
……..”””Electric cars don’t have gas tanks.”””
……..print(“This car doesn’t have a gas tank!”)
- calling fill_gas_tank() with an electric car will run this code instead
- benefit of using inheritance is you can retain info you need and override what you don’t need from the parent class
Instances as attributes
- when modeling something from the real world in your code, you may find a growing list of attributes and methods, meaning lengthy files
- composition – breaking a large class into smaller classes that work together
- if we’re adding many attributes and methods specific to a car’s battery, we can create a separate class called Battery
- we can use a Battery instance as an attribute in the ElectricCar class
class Car:
….–snip–
class Battery:
….”””A simple attempt to model a battery for an electric car.”””
….def __init-_(self, battery_size=40):
……..”””Initialize the battery’s attributes.”””
……..self.battery_size = battery_size
….def describe_battery(self):
……..”””Print a statement describing the battery size.”””
……..print*f”This car has a {self.battery_size}-kWh battery.”)
class ElectricCar(Car):
….”””Represent aspects of a car, specific to electric vehicles.”””
….def __init__(self, make, model, year):
……..”””
……..Initialize attributes of the parent class.
……..Then initialize attributes specific to an electric car.
……..”””
………super().__init__(make, model, year)
……..self.battery = Battery()
my_leaf = ElectricCar(‘nissan’, ‘leaf’, 2024)
print(my_leaf.get_descriptive_name())
my_leaf.battery.describe_battery()
- defined a new class called Battery that doesn’t inherit from any other class
- __init__() method has one parameter, battery_size, in addition to self
- this is an optional parameter that sets the battery’s size to 40 if no value is provided
- method describe_battery() has been moved to this class as well
- in the ElectricCar class, we now add an attribute called self.battery
- creates a new instance of Battery and assigns that instance to the attribute self.battery
- this will happen every time the __init__() method is called, any ElectricCar instance will have a Battery instance
- create an electric car and assign it to the variable my_leaf
- when we want to describe the battery, we need to work through the car’s battery attribute
my_leaf.battery.describe_battery()
- this line tells Python to look at the instance my_leaf, find its battery attribute, and call the method describe_battery() associated with Battery instance assigned to the attribute
- identical output from earlier
2024 Nissan Leaf
This car has a 40-kWh battery
- looks like extra work, but we can describe the battery in more detail without cluttering the ElectricCar class
- let’s add another method to Battery that reports the range of the car based on the battery size
class Car:
….–snip–
class Battery:
….–snip–
….def get_range(self):
……..”””Print a statement about the range this battery provides.””””
……..if self.battery_size == 40:
…………range = 150
……..elif self.battery_size == 65:
…………range = 225
……..print(f”This car can go about {range} miles on a full charge.”)
class ElectricCar(Car):
….–snip–
my_leaf = ElectricCar(‘nissan’, ‘leaf’, 2024)
print(my_leaf.get_descriptive_name())
my_leaf.battery.describe_battery()
my_leaf.battery.get_range()
- method get_range() performs simple analysis
- if the battery’s capacity is 40 kWh, get_range() sets the range to 150 miles
- if the capacity is 65 kWh, it sets the range to 225 miles
- then reports this value
- call this method through the car’s battery attribute
- output tells us the range of the car based on its battery size:
2024 Nissan Leaf
This car has a 40-kWh battery.
This car can go about 150 miles on a full charge.
Modeling Real-World Objects
- you’ll start wrestling with interesting questions when modeling more complicated things like electric cars
- for example, is the range of an electric car a property of the battery or the car
- fine to maintain the association of the method get_range() within the Battery class for one car
- for an entire fleet, we’ll want to move get_range() to the ElectricCar class
- or we could maintain the association of the get_range() method with the battery but pass it a parameter such as car_model
- the get_range() method would then report a range based on the battery size and car model
- in your growth as a programmer, you’ll wrestle with questions like these since you’re thinking at a higher logical level rather than a syntax-focused level
- not focused on Python, but rather on how to represent the real world in code
- you’ll realize there are no right or wrong approaches to modeling real-world situations
- but some approaches are more efficient, it takes practice to find the most efficient representations
- as long as your code works as you want it, you’re doing well
- don’t be discouraged when you find you’re ripping apart your classes and rewriting them several times using different approaches
Importing Classes
- as you add functionality to classes, your files can get long, even if you use inheritance and composition properly
- keep your files as uncluttered as possible
- Python lets you store classes in modules and then import the classes you need into your main program
Importing a single class
- let’s create a module containing just the Car class
- naming issue: already have a file named car.py in this chapter, this module should be named car.py because it contains code representing a car
- solution: store the Car class in a module named car.py, replacing the car.py file previously used
- this point forward, any program using this module will need a more specific filename, such as my_car.py
car.py
“””A class that can be used to represent a car.”””
class Car:
….”””A simple attempt to represent a car.””””
….def __init__(self, make, model, year):
……..”””Initialize attributes to describe a car.”””
……..self.make = make
……..self.model = model
……..self.year = year
……..self.odometer_reading = 0
….def get_descriptive_name(self):
……..”””Return a neatly formatted descriptive name.”””
……..long_name = f”{self.year} {self.make} {self.model}”
……..return long_name.title()
….def read_odometer(self):
……..”””Print a statement showing the car’s mileage.”””
……..print(f”This car has {self.odometer_reading} miles on it.”)
….def update_odometer(self, mileage):
……..”””
……..Set the odometer reading to the given value.
……..Reject the change if it attempts to roll the odometer back.
……..”””
……..if mileage >= self.odometer_reading:
…………self.odometer_reading = mileage
……..else:
…………print(“You can’t roll back an odometer!”)
……..def increment_odometer(self, miles):
…………”””Add the given amount to the odometer reading.”””
…………self.odometer_reading += miles
- include a module-level docstring that describes the contents of this module
- write a docstring for each module you create
- now we make a separate file called my_car.py
- this file will import the Car class and then create an instance from that class
my_car.py
from car import Car
my_new_car = Car(‘audi’, ‘a4’, 2024)
print(my_new_car.get_descriptive_name())
my_new_car.odometer_reading = 23
my_new_car.read_odometer()
- import statement tells Python to open the car module and import the class Car
2024 Audi A4
This car has 23 miles on it.
- importing classes is an effective way to program
- picture how long this program file would be if the entire Car class were included
- when you move the class to a module and import the module, same functionality, but cleaner code
Storing multiple classes in a module
- can store as many classes as needed in a single module, but they should be related
- classes Battery and ElectricCar both help represent cars, so let’s add them to the module car.py
car.py
“””A set of classes used to represent gas and electric cars.”””
class Car:
….–snip–
class Battery:
….”””A simple attempt to model a battery for an electric car.”””
….def __init__(self, battery_size=40):
……..”””Initialize the battery’s attributes.”””
……..self.battery_size = battery_size
….def describe_battery(self):
……..”””Print a statement describing the battery size.”””
……..print(f”This car has a {self.battery_size}-kWh battery.”)
….def get_range(self):
……..”””Print a statement about the range this battery provides.”””
……..if self.battery_size == 40:
…………range = 150
……..elif self.battery_size == 65:
…………range = 225
……..print(f”This car can go about {range} miles on a full charge.”)
class ElectricCar(Car):
….”””Models aspect of a car, specific to electric vehicles.”””
….def __init__(self, make, model, year):
……..”””
……..Initialize attributes of the parent class.
……..Then initialize attributes specific to an electric car.
……..”””
……..super().__init__(make, model, year)
……..self.battery = Battery()
- now we can make a new file called my_electric_car.py, import the ElectricCar class, and make an electric car
my_electric_car.py
from car import ElectricCar
my_leaf = ElectricCar(‘nissan’, ‘leaf’, 2024)
print(my_leaf.get_descriptive_name())
my_leaf.battery.describe_battery()
my_leaf.battery.get_range()
- same output, even though more logic is hidden in a module
2204 Nissan Leaf
This car has a 40-kWh battery.
This car can go about 150 miles on a full charge.
Importing multiple classes from a module
- can import as many classes as needed into a program file
my_cars.py
from car import Car, Electric Car
my_mustang = Car(‘ford’, ‘mustang’, 2024)
print(my_mustang.get_descriptive_name())
my_leaf = ElectricCar(‘nissan’, ‘leaf’, 2024)
print(my_leaf.get_descriptive_name())
- import multiple classes form a module by separating each class with a comma
- after importing classes, you can make as many instances of each class as you need
2024 Ford Mustang
2024 Nissan Leaf
Importing an entire module
- can import an entire module and then access the classes you need using dot notation
- since every call that creates an instance of a class includes the module name, you won’t have naming conflicts with any names in the current file
- let’s import the entire car module and then create a regular car and an electric car
my_cars.py
import car
my_mustang = car.Car(‘ford’, ‘mustang’, 2024)
print(my_mustang.get_descriptive_name())
my_leaf = car.ElectricCar(‘nissan’, ‘leaf’, 2024)
print(my_leaf.get_descriptive_name())
- first, we import the entire car module
- then access the classes through the module_name.ClassName syntax
- creates a Ford Mustang and Nissan Leaf
Importing all classes from a module
- can import every class from a module
from module_name import *
- method is not recommended for two reasons
- first, it’s helpful to read the import statements at the top of a file to understand which classes a program is using
- this approach can cause confusion with names in the file, if you import a class with the same name as something else in your program file, errors can be hard to diagnose
- if you need to import many classes from a module, you’re better off importing the entire module and using the module_name.ClassName syntax
- you’ll see clearly where the module is used in the program and avoid naming conflicts
Importing a module into a module
- can spread your classes across several modules and avoid storing unrelated classes together
- a class in one module may depend on a class in another
- can import required classes into the first module
- let’s store the Car class in one module and the ElectricCar and Battery classes in a separate module
electric_car.py
“””A set of classes that can be used to represent electric cars.”””
from care import Car
class Battery:
…–snip–
class ElectricCar(Car):
….–snip–
- ElectricCar needs access its parent class Car, so we import Car directly into the module
- need to update the Car module so it only contains the Car class
car.py
“””A class that can be used to represent a car.”””
class Car:
….–snip–
- now we can import from each module separately and create whatever kind of car we need
my_cars.py
from car import Car
from electric_car import ElectricCar
my_mustang = Car(‘ford’, ‘mustang’, 2024)
print(my_mustang.get_descriptive_name())
my_leaf = ElectricCar(‘nissan’, ‘leaf’, 2024)
print(my_leaf.get_descriptive_name())
- import Car from its module, and ElectricCar from its module
- then create one regular car and one electric car
2024 Ford Mustang
2024 Nissan Leaf
Using aliases
- aliases helpful when using modules to organize your projects’ code
- can use aliases when importing classes
- for example, a program to make a lot electric cars
- might get tedious to type (and read) ElectricCar repeatedly
- can give ElectricCar an alias in the import statement
from electric_car import ElectricCar as EC
- can use alias now
my_leaf = EC(‘nissan’, ‘leaf’, 2024)
- can give a module an alias
import electric_car as ec
- can use module alias with the full class name
my_leaf – ec.ElectricCar(‘nissan’, ‘leaf’, 2024)
Find your own workflow
- many options to structure code in a large project
- keep the structure simple in the beginning by doing everything in one file and moving your classes to separate models once everything is working
- try storing classes in modules when you start a project if you like how models and files interact
The Python standard library
- Python standard library – set of modules included with every Python installation
- now that you have a basic understanding of how functions and classes work, you can start to use models like these that other programmers have written
- can use any function or class in the library by including a simple import statement at the top of your file
- let’s look at one module, random, which can be useful for real-world situations
- randint() function takes two integer arguments and returns a randomly selected integer between and including those numbers
>>> from random import randint
>>> randint(1, 6)
3
- choice() function takes in a list or tuple and returns a randomly chosen element
>>> from random import choice
>>> players = [‘charles’, ‘martina’, ‘michael’, ‘florence’, ‘eli’]
>>> first_up choice(players)
>>> first_up
‘florence’
- random module shouldn’t be used for building security-related applications, but for fun and interesting projects
- explore the Python standard library on the site called Python module of the week (https://pymotw.com)
Styling classes
- CamelCase – capitalize the first letter of each word in the name, avoid underscores, used for classes
- instance and module names should be lowercase, with underscores between words
- docstring should follow every class definition to describe what the class does
- each module should have a docstring describing what the classes can be used for
- use blank lines to organize code, for example, one blank line between methods in a class or two blank lines to separate classes within a module
- when importing a module from the standard library and a module that you wrote, place the import statement for the standard library module first
- then add a blank line and the import statement for the module you wrote
Summary
- learned how to write classes, store info in a class using attributes, and how to write methods that give your classes the behavior they need
- learned to write __init__() methods to create instances from your classes with the exact attributes you want
- saw how to modify attributes of an instance directly and through methods
- learned that inheritance can simplify the creation of related classes and learned to use instances of one class as attributes for another class for simplicity
- saw how storing classes in modules and importing classes you need into files where they’ll be used can keep your projects organized
- started learning about the Python standard library
- saw an example, based on the random module
- learned to style your classes
End of study session.