Search code examples
pythonclassoopattributesinstance

Python Classes - Using Instances AS Attributes


I'm currently learning/working with classes in Python 3.10.2 as of writing this. What I am trying to achieve is to create a class instance which is an attribute within another class.

Here is some code I've been working on to help demonstrate my point.

class Vehicle():
    """To model a basic vehicle"""
    def __init__(self, name, max_speed, millage):
        """Initiate class variables"""
        self.name = name
        self.max_speed = max_speed
        self.millage = millage
        self.tyre = Tyre()

    def vehicle_details(self):
        """display vehicle details"""
        return f"\nName: {self.name.title()}, Max speed: {self.max_speed}MPH, Millage: {self.millage} "

class Tyre():
    """A class specific for vehicle tyres"""
    def __init__(self, size=16, pressure=36):
        self.size = size
        self.pressure = pressure

    def tyre_details(self):
        """Print tyre details."""
        print(f'Tyre size: {self.size} inches.')
        print(f"Tyre pressure {self.pressure} PSI.")

In the Vehicle class, I add Tyre() as an attribute.

Now this code DOES work and I can call the Tyre methods through my Vehicle instances, but only when I allocate pre-determined values to size and pressure within the Tyre class.

Is there either:

  1. A way I can achieve this without having to allocate the pre-determined values within the Tyre class?

    This is the Traceback I receive if I do not allocate the pre-determined values:

    TypeError: __init__() missing 2 required positional arguments: 'size' and 'pressure'
    
  2. An easy way for me to overwrite the pre-determined values when calling this class and its methods if I am unable to remove the pre-determined values?


Solution

  • There are almost infinite ways to "solve" this, but I'll present a few.

    None are "right", all involve tradeoffs and over time you'll learn which make sense where.

    Passthrough arguments

    class Vehicle():
        """To model a basic vehicle"""
        def __init__(self, name, max_speed, millage, tyre_size, tyre_pressure):
            """Initiate class variables"""
            self.name = name
            self.max_speed = max_speed
            self.millage = millage
            self.tyre = Tyre(tyre_size, tyre_pressure)
    
        def vehicle_details(self):
            """display vehicle details"""
            return f"\nName: {self.name.title()}, Max speed: {self.max_speed}MPH, Millage: {self.millage} "
    
    
    class Tyre():
        """A class specific for vehicle tyres"""
        def __init__(self, size, pressure):
            self.size = size
            self.pressure = pressure
    
        def tyre_details(self):
            """Print tyre details."""
            print(f'Tyre size: {self.size} inches.')
            print(f"Tyre pressure {self.pressure} PSI.")
    
    v1 = Vehicle("VehicleName", 120, 50_000, 16, 36)
    # Or, with named arguments
    v2 = Vehicle(
        name = "VehicleName", 
        max_speed = 120, 
        millage = 50_000, 
        tyre_size = 16, 
        typre_pressure = 36
    )
    

    Here, you're just passing the Tyre constructor's arguments through from the Vehicle constructor to your construction of Tyre. If every time you construct a Vehicle you'll know the Tyre specifications, this is fine. If not, you'll likely want an alternative.

    Construct Vehicle with Tyre instance as argument

    class Vehicle():
        """To model a basic vehicle"""
        def __init__(self, name, max_speed, millage, tyre):
            """Initiate class variables"""
            self.name = name
            self.max_speed = max_speed
            self.millage = millage
            self.tyre = tyre
    
        def vehicle_details(self):
            """display vehicle details"""
            return f"\nName: {self.name.title()}, Max speed: {self.max_speed}MPH, Millage: {self.millage} "
    
    
    class Tyre():
        """A class specific for vehicle tyres"""
        def __init__(self, size, pressure):
            self.size = size
            self.pressure = pressure
    
        def tyre_details(self):
            """Print tyre details."""
            print(f'Tyre size: {self.size} inches.')
            print(f"Tyre pressure {self.pressure} PSI.")
    
    
    v = Vehicle(
        name = "VehicleName", 
        max_speed = 120, 
        millage = 50_000, 
        tyre = Tyre(16, 36)
    )
    

    Similar to the previous option, but in the previous option each Vehicle had their own Tyre instance. Here you can "share" Tyre objects. Probably not a huge deal in this case, but something to keep in mind, especially when you work with larger or more complex objects and/or where sharing becomes a benefit.

    Sharing Tyre instances might look like:

    standard_tyre = Tyre(16, 36)
    
    v1 = Vehicle(
        name = "VehicleOne", 
        max_speed = 120, 
        millage = 50_000, 
        tyre = standard_tyre,
    )
    v2 = Vehicle(
        name = "VehicleTwo", 
        max_speed = 140, 
        millage = 20_000, 
        tyre = standard_tyre,
    )
    

    Setting tyre later, outside of the constructor

    class Vehicle():
        """To model a basic vehicle"""
        def __init__(self, name, max_speed, millage):
            """Initiate class variables"""
            self.name = name
            self.max_speed = max_speed
            self.millage = millage
            self.tyre = None
    
        def vehicle_details(self):
            """display vehicle details"""
            return f"\nName: {self.name.title()}, Max speed: {self.max_speed}MPH, Millage: {self.millage} "
    
    
    class Tyre():
        """A class specific for vehicle tyres"""
        def __init__(self, size, pressure):
            self.size = size
            self.pressure = pressure
    
        def tyre_details(self):
            """Print tyre details."""
            print(f'Tyre size: {self.size} inches.')
            print(f"Tyre pressure {self.pressure} PSI.")
    
    
    v = Vehicle(
        name = "VehicleName", 
        max_speed = 120, 
        millage = 50_000, 
    )
    v.tyre = Tyre(16,36)
    

    Maybe you don't know what the specifics of the Tyre are when you're creating the Vehicle, but you still want a Vehicle instance.