Python Forum

Full Version: Class inheritance
You're currently viewing a stripped down version of our content. View the full version with proper formatting.
Hello,
I am just learning programming with Python as my first language.

I want to build a small application to manage electronic parts.
All parts share some global attributes: ipn, mpn, dpn, manufacturer...

But then there are additional attributes that only parts from a specific category have.
For example a capacitor will have these additional attributes: dielectric, volts...

I've done it this way:

# Define parent class
class Part():
    def __init__(self, ipn, mpn, dpn, man):
        self.ipn = ipn
        self.mpn = mpn
        self.dpn = dpn
        self.manufacturer = man


# Define child class
class PartCapacitor(Part):
    def __init__(self, ipn, mpn, dpn, man, dielectric):
        super().__init__(ipn, mpn, dpn, man)
        self.dielectric = dielectric
A programming rule I've already learned is: "Dont repeat yourself".
However in the child class I have to repeat all arguments in line 12 and 13.

If I want to add an attribute to the parent class, I'll have to write it twice(!) in every single child class too?
If I have 10 child classes I'll need to add it in 20 places...

That sounds insane and absolutely wrong to me.
Am I doing it wrong? Is there an easier way?

And another question:
I want to add a method to print out the object's attributes.
But since the parent and sub classes will have different attributes, I am not sure where to create this method.
Should I create it in the parent and control which attributes to print with if statements?
Or should I create one print method in every child class? (Which means I have to repeat some parts over and over)
What's best practice here?

Thank you in advance Blush
Use named arguments.
class Part():
    """Abstract base class for all parts"""
    
    def __init__(self, ipn=None, mpn=None, dpn=None, man=None):
        self.ipn = ipn
        self.mpn = mpn
        self.dpn = dpn
        self.manufacturer = man

    def __repr__(self):
        return f'{type(self).__name__}: {self.ipn}, {self.mpn}, {self.dpn}, {self.manufacturer}'

class Capacitor(Part):
    """Capacitor part"""
    def __init__(self, dielectric=None, **kvargs):
        super().__init__(**kvargs)
        self.dielectric = dielectric

    def __repr__(self):
        return f'{super().__repr__()}, {self.dielectric}'


cap = Capacitor(ipn='a', mpn='b', dpn='c', man='d', dielectric='e')
print(cap)
Using a bunch of position arguments is bad for many reasons. It makes refactoring code difficult. It makes it easy to mix up the order of arguments and pass the wrong value to an argument. It is hard to remember the order so you have to keep referencing the function. It makes the code difficult to read. Named arguments fix all of these problems and they make it so your subclasses only have to declare additional arguments that are not handled by the superclass.

Another way to do this is initialize all your class attributes to None and set them outside the __init__.
class Part():
    """Abstract base class for all parts"""
    
    def __init__(self,):
        self.ipn = None
        self.mpn = None
        self.dpn = None
        self.manufacturer = None

    def __repr__(self):
        return f'{type(self).__name__}: {self.ipn}, {self.mpn}, {self.dpn}, {self.manufacturer}'

class Capacitor(Part):
    """Capacitor part"""
    def __init__(self, **kvargs):
        super().__init__(**kvargs)
        self.dielectric = None

    def __repr__(self):
        return f'{super().__repr__()}, {self.dielectric}'


cap = Capacitor()
cap.ipn = 'a'
cap.mpn = 'b'
cap.dpn = 'c'
cap.manufacturer = 'd'
cap.dielectric = 'e'
@deanhystad Thank you! **kwargs and __repr__ was exactly what I was looking for.
Couldn't imagine a better answer. Wink

I already intended to use named arguments. I haven't noticed that I actually didn't...
One problem with relying on named arguments is that you cannot use position arguments. You could not do this:
cap = Capacitor(ipn, mpn, dpn, manufacturer, dielectric)
Output:
TypeError: Capacitor() takes 1 positional argument but 5 were given
I've noticed some functions in the Python standard libraries that have this same problem/limitation.
Quote:random.choices(population, weights=None, *, cum_weights=None, k=1)
I cannot call random.choices like this:
c = random.choices(values, weights, None, 10)
"cum_weights" and "k" are name only arguments. This is enforced by "*" in the signature. If you want to use name-only arguments you could do the same.