Struggling with Class Inheritance & super().__init__ - python

Very new to Python, trying to create a game in which any number of armies can be created, but each army will pre-render the names of the soldiers.
I think I need to be using super init to really cut down on duplicate code, but I cannot for the life of me figure out how to make it work. From what I understand my class Army should be the Parent class, with RedArmy and Scout as subclasses. I'm just struggling to understand where the super().__init__() should come in?
class Army:
def __init__(self):
self.color = None
self.scoutname = None
self.demomanname = None
self.medicname = None
def train_scout(self, weapon):
return Scout(self.color, self.scoutname, weapon)
class RedArmy(Army):
def __init__(self):
self.color = "Red"
self.scoutname = "Yankee"
self.demomanname = "Irish"
self.medicname = "Dutch"
class BlueArmy(Army):
pass
class Scout:
specialization = "fast captures"
def __init__(self, color, scoutname, weapon):
self.color = color
self.scoutname = scoutname
self.weapon = weapon
def introduce(self):
return (f'Hi I\'m {self.scoutname}, I do {self.specialization} and I wield a {self.weapon}')
my_army = RedArmy()
soldier_1 = my_army.train_scout("baseball bat")
print(soldier_1.introduce())

Where to put the super().__init__ (if anywhere) depends on the specifics of your class hierarchy.
Most commonly, if it matters where you put it, you want it at the very start of the subclass's __init__ method. This makes sure that all of the base-class variables are set up, all of its invariants are met, and all of its methods can be called by the rest of the subclass __init__ code.
In your case, it matters because you're setting the same attributes in the base class and the subclass. You obviously want the subclass version to be the one that takes, so it has to come after the default assignments, not before:
class RedArmy(Army):
def __init__(self):
super().__init__()
self.color = "Red"
self.scoutname = "Yankee"
self.demomanname = "Irish"
self.medicname = "Dutch"
However, it's worth considering whether you really want the base class to set these variables to None in the first place.
I assume that in your real code, BlueArmy isn't just going to pass, but is instead going to do the same thing as RedArmy, replacing all of those values with some strings.
Also, the rest of your code is presumably going to assume that there are valid strings there, not None. An exception like TypeError: '<' not supported between instances of 'NoneType' and 'str' is harder to debug than AttributeError: 'GreenArmy' object has no attribute 'scoutname', not easier, so why not just leave the defaults out? Then you can eliminate Army.__init__ entirely, and you don't have to worry about calling super in the subclass initializers in the first place.
Or, alternatively, you could have Army.__init__ take parameters that are used to assign the values, and have the subclasses call super().__init__("Red", "Yankee", "Irish", "Dutch"). Then, Army() will raise a TypeError instead of creating an invalid Army instance. Or you could make an #abstractmethod named self._setup() that Army.__init__ calls and expects each subclass to provide, so Army() will raise an even more meaningful TypeError about instantiating an abstract class. These refinements make it easier to debug your Army subclasses—if you just have two of them, they may just be a waste of time, but if you have a bunch of them, which will be developed by a variety of people or over a long period of time, it might be worth it.

Passing values as arguments makes the most sense if you want your classes to take advantage of super and init:
class Army:
def __init__(self, color=None, scoutname=None, demomanname=None,
medicname=None):
self.color = color
self.scoutname = scoutname
self.demomanname = demomanname
self.medicname = medicname
def train_scout(self, weapon):
return Scout(self.color, self.scoutname, weapon)
class RedArmy(Army):
def __init__(self, color="Red", scoutname="Yankee", demomanname="Irish",
medicname="Dutch"):
super().__init__(color, scoutname, demomanname, medicname)
class Scout:
specialization = "fast captures"
def __init__(self, color, scoutname, weapon):
self.color = color
self.scoutname = scoutname
self.weapon = weapon
def introduce(self):
return (
f'Hi I\'m {self.scoutname}, I do {self.specialization} and I wield '
f'a {self.weapon}')
my_army = RedArmy()
soldier_1 = my_army.train_scout("baseball bat")
print(soldier_1.introduce())

Related

How does python3 diamond inheritance works for data fields? How to initialize the inherited fields with super()._init__?

I was checking this problem to understand multiple inheritance and I got stuck.
How can I set the fields of the inherited objects from the last class?
class Vehicle():
def __init__(self, name:str, seats:int):
self.name = name
self.seats = seats
def print_vehicle(self):
print(f'Vehicle {self.name} has {self.seats} seats')
class Boat(Vehicle):
def __init__(self, name:str, seats:int, engine_type:str):
super().__init__(name, seats)
self.engine_type = engine_type
def print_vehicle(self):
print(f'Boat {self.name} has {self.seats} seats and engine {self.engine_type}')
class Car(Vehicle):
def __init__(self, name:str, seats:int, fuel:str):
super().__init__(name, seats)
self.fuel = fuel
def print_vehicle(self):
print(f'Car {self.name} has fuel {self.fuel}')
class AnphibiousCar(Boat, Car):
def __init__(self, name, seats, engine_type, fuel):
super(AnphibiousCar, self).__init__(name, seats, engine_type) # ???
def print_vehicle(self):
print(f'Anphibious car {self.name} has {self.seats} seats and {self.engine_type} - {self.fuel} engine')
ac = AnphibiousCar('name', 4, 'piston', 'gas')
ac.print_vehicle()
The point is that each class should focus only on the stuff which is its direct responsibility; the rest should be delegated to superclasses (and note that, when you deal with such a cooperative inheritance with super(), your methods that call super() should not need to know what exactly are the actual superclasses, in particular the nearest one - as this can change, depending on the actual class of self).
So let's reimplement your classes (with a bunch of explanations in the comments):
class Vehicle:
# Added the `*,` marker to make `name` and `seats` *keyword-only*
# arguments (i.e., arguments that are identified only by their
# *names*, never by their positions in a call's arguments list).
def __init__(self, *, name: str, seats: int):
self.name = name
self.seats = seats
# We abstract out class-specific features into separate methods,
# keeping in the `print_vehicle()` method only the common stuff,
# so that in subclasses we'll need to customize only those methods
# (`list_features()`, `get_type_label()`), *not* `print_vehicle()`.
def print_vehicle(self):
vehicle_type_label = self.get_type_label()
features = ', '.join(self.list_features())
print(f'{vehicle_type_label} {self.name}: {features}.')
# Side note: the `list[str]` type annotation requires Python 3.9
# or newer (for compatibility with older versions you need to
# replace it with `List[str]`, using `from typing import List`).
def list_features(self) -> list[str]:
return [f'has {self.seats} seats']
# This implementation is, in fact, quite generic (so that
# in most subclasses we will *not* need to customize it).
def get_type_label(self) -> str:
return self.__class__.__name__
class Boat(Vehicle):
# Only `Boat`-specific arguments (as keyword-only ones, as above...)
# are declared here explicitly. Any other are treated as a "black
# box", just being passed into superclasses...
def __init__(self, *, engine_type: str, **kwargs):
super().__init__(**kwargs)
self.engine_type = engine_type
# Also here we focus only on this-class-specific stuff, handling
# other stuff as "agnostically" as possible...
def list_features(self) -> list[str]:
return super().list_features() + [f'has {self.engine_type} engine']
class Car(Vehicle):
# And analogously...
def __init__(self, *, fuel: str, **kwargs):
super().__init__(**kwargs)
self.fuel = fuel
def list_features(self) -> list[str]:
return super().list_features() + [f'needs {self.fuel} fuel']
class AmphibiousCar(Boat, Car):
# Note: here we get our `__init__()` and `list_features()`
# for free (!), as the superclasses provide all we need
# when it comes to those two methods.
# The only thing we may want to customize is:
def get_type_label(self) -> str:
return 'Amphibious car'
ac = AmphibiousCar(
name='Julia-III',
seats=4,
engine_type='piston',
fuel='gas')
# "Amphibious car Julia-III: has 4 seats, needs gas fuel, has piston engine."
ac.print_vehicle()
As a further reading, I'd recommend: https://rhettinger.wordpress.com/2011/05/26/super-considered-super/
You have some errors:
super(AnphibiousCar, self).__init__(name, seats, engine_type) could become
Boat.__init__(self, name, seats, engine_type) so calling the class you could
give information about how to initialize it.
there is a missing parameter in Boat where you should give a fuel argument
to the superclass Vehicle, like super().__init__(name, seats, "oil")
As you can note if you use super you don't need to pass self, if you use
the class name you are using it.
My point of view is that, yes, is good to understand, but don't loose to much
time as this kind of multiple inheritance is only theoretical and practically
not used in real coding. This in fact can cause a lot of confusion and add
boilerplate... "new" languages like, for example, Rust do not even provide
inheritance.
Just to say: "Yes, study it, but keep it simple" ^_^

Better way to pass default arguments to subclasses

Suppose I have some class which I subclass, that has some default (perhaps a flag-like) argument. What's the best way to handle passing such an argument around? I can think of doing
class Dog():
def __init__(self, noisy = False):
self.noisy = noisy
def bark(self):
if self.noisy:
print('YAP')
else:
print('yap')
class Beagle(Dog):
def __init__(self, noisy = False):
super().__init__(noisy)
dave = Beagle(noisy = True)
dave.bark()
But this uses noisy seven times, and I feel there must be a better way.
First of all, you can drop the noisy = in the instantiation of Beagle(), it's unneeded:
dave = Beagle(True)
Secondly, given your implementation, your Beagle class has no reason to exist. It does not add any functionality and does not specialize Dog in any way. If anything, possible subclasses of Dog that make sense would be:
class NoisyDog(Dog):
def __init__(self):
super().__init__(True)
class QuietDog(Dog):
def __init__(self):
super().__init__(False)
You could also keep the noisy= in the calls to super().__init__() for better readability, but again that's unneeded.
Other than that, there isn't really much else you can do. If you need a class property, you'll have to assign it to the class (self.foo = bar) and then reference it using its name...

Trouble with specific class inheritance behaviour

So I am trying to get my data structure set up for an automated generator I am writing for a roleplaying game and I am having trouble with some specific inheritance quirks. Here is an excerpt of the data structure.
class data():
def __init__(self):
self.races = Races()
class Races(data):
def __init__(self):
self.humans = Humans()
class Humans(Races):
def __init__(self):
self.Characteristics = {
'Brawn':2,
'Agility':2,
'Intellect':2,
'Cunning':2,
'Willpower':2,
'Presence':2
}
There is a lot more in the structure but this is just a bottom to top overview. I also know it is indented weirdly but that is strictly stack overflow.
Now I wish to have two behaviors from this object.
The ability to call any characteristic with
data.races.humans.Characteristic['brawn']
as the calling format.
And too also be able to iterate through subclasses with a generator like:
(subclass for subclass in data.races.__subclasses__())
obviously after I have instantiated the object.
Now I have tried changing the structure several times and I can get it so EITHER I can call it with dot notation, but it returns AttributeError: 'Races' object has no attribute '__subclasses__'
Or vice versa by completely separating it into a more traditional structure but then I cannot call in dot notation and this makes it very hard to keep everything organized and readable.
Can anyone suggest what I am doing wrong or a more Pythonic way to approach the problem?
Let's start in the middle. Presumably, a character of any race has the same attributes, just different values for those attributes.
class Race:
def __init__(self):
self.life = 100 # 100% healthy
class Humanoid(Race):
def __init__(self):
super().__init__()
self.legs = 2
class Insectoid(Race):
def __init__(self):
super().__init__()
self.legs = 8
class Human(Humanoid):
def __init__(self):
super().__init__()
self.brawn = 2
self.agility = 2
self.intellect = 2
self.cunning = 2,
self.willpower = 2
self.presence = 2
class Elf(Humanoid):
def __init__(self):
super.__init__()
self.brawn = 1
self.agility = 3
self.intellect = 3
self.cunning = 2
self.willpower = 3
self.presence = 1
Now, any particular character would be instantiated as the correct class:
some_elf_1 = Elf()
some_human_1 = Human()
some_human_2 = Human()
for character in [some_elf_1, some_human_1, some_human_2]:
print("Brawn: ", character.brawn)
In the preceding, it doesn't matter what the actual type of each character is; as long as you know that it is some subclass of Race (or an instance of Race itself), it will have a brawn attribute that you can access.
You data class doesn't really seem necessary without more detail.
So, While the answer given put me on the right track I realized what I needed and am just throwing in my lot for any poor souls.
Firstly - I realized what was wrong with my generator, I was calling on the initialized object instead of the class object. Objects do not have a subclasses attrib and I was mis-informed by most of the guides I read!
Secondly, I considered using a metaclass to get the iterating behavior I wanted from my objects can simply be achieved with a registry attribute that is a dict of all the initialized subclasses.
lass Races(data):
def __init__(self):
self.humans = Humans()
self.droids = Droids()
self.twileks = Twileks()
self.registry = {
'humans':self.humans,
'droids':self.droids,
'twileks':self.twileks
}
This allows me to iterate through certain values for different races after they have been initialized.
Thanks for all the great answers!

Python super constructors -- which arguments do you put in the child method?

I've created an "Animal" class that takes in parameters name and colour, and a subclass called "Pig" which should inherit name and colour from "Animal", but should also take a new parameter, TailType.
Here's what I've done so far:
class Animal(object):
def __init__(self, name, colour):
self.name = name
self.colour = colour
def get_name(self):
return self.name
def set_name(self, newName = ""):
self.name = newName
def set_colour(self, newColour = ""):
self.colour = newColour
def get_colour(self):
return self.colour
def __str__(self):
return self.get_name() + ' : ' + self.colour
class Pig(Animal):
def __init__(self, name, colour, tailType):
super().__init__()
self.tailType = tailType
When I'm initialising the "Pig" class, I'm not sure which parameters to put in the __init__ definition; should it be name and colour, or name + colour + tailType?
Also, does this subclass inherit the __str__ representation method of Animal, or do I have to write that again within the "Pig" subclass?
I'm really not sure about which parameters go where, when I initialise a subclass. I've looked at examples, and they all have very simple cases with one parameter (self).
If I try to do
john = Pig('John', 'pink', 'curly')
I get
TypeError: __init__() missing 2 required positional arguments: 'name' and 'colour'.
Superclasses and subclasses make sense conceptually, but when it comes to dealing with their syntax, I'm really struggling.
Note: please don't refer me to a general explanation of what superclass constructors are: I've read a lot of them and still don't really know how to apply them in this situation.
Just pass name and colour.
class Pig(Animal):
def __init__(self, name, colour, tailType):
super().__init__(name, colour)
self.tailType = tailType
Think of it as super() provides me the method from the parent, bound to my object. What does it take as inputs? name and colour? Let's pass it name and colour, then.
And yes, __str__ is inherited like any other method.

Is there a "pythonic" approach to required properties (OOP)?

I'm struggling to find a "pythonic" approach to the following class organization:
I have a base class with properties initialized in its constructor, for example:
class Animal(object):
def __init__(self, class_, species, is_domesticated):
self.class_ = class_
self.species = species
self.is_domesticated = is_domesticated
Then, when I subclass, I would like to "hard-code" one or more of these properties, like so:
class Mammal(Animal):
def __init__(self, species, is_domesticated):
Animal.__init__(self, 'Mammal', species, is_domesticated)
A Mammal is thus instantiated like so:
monkey = Mammal('Primate', false)
The problem is, I would like to use *args so as to leave any derived classes alone when altering the base class definition. Thus the definition of Mammal becomes:
class Mammal(Animal):
def __init__(self, *args):
Animal.__init(self, *(args + (class_='Mammal',)))
Which (needless to say) looks horrible. Some tips would be appreciated =)
If you only have a fixed set of arguments in the base class, there isn't much need to worry about variable arguments. Just do what you did in your first example and it's fine. If you want to be able to randomly add arguments to the base class, but add them as positional arguments and without defaults, there's no hope; you can't just change the base class willy-nilly and expect all derived classes to keep working.
However, there is a fairly common intermediate case where you might have a large set of attributes, various combinations of which may be passed to any class in the hierarchy. You might want to add new arguments to the base class, but they'll have defaults so that derived classes don't need to know about them explicitly; they'll just gracefully degrade to the base-class default. In such a case it's usually a better idea to use **kwargs rather than *args.
class Animal(object):
def __init__(self, **kwargs):
self.class_ = kwargs['class_']
self.species = kwargs['species']
# etc.
class Mammal(Animal):
def __init__(self, **kwargs):
Animal.__init__(self, class_="Mammal", **kwargs)
This requires the arguments to passed by keyword:
>>> Animal(class_='Fish', species='barracuda', is_domesticated=False)
4: <__main__.Animal object at 0x0177ABF0>
>>> Mammal(species="monkey", is_domesticated=False)
5: <__main__.Mammal object at 0x0177AFB0>
. . . but this is better if there are a lot of them, because no one will remember which order to pass them in if you have 10 different things getting passed in positionally. It also means that you can add new arguments easily; no one has to know where in the list to put the new ones, they can just add them anywhere by keyword.
In Python 2 you have to manually extract the kwargs as I did above. In Python 3 you can use keyword-only arguments to make this even easier.
Well, why don't you just do what you said you want? Make Mammal.__init__() take a *args argument, then use that. Here's the code:
class Animal(object):
def __init__(self, class_, species, is_domesticated):
self.class_ = class_
self.species = species
self.is_domesticated = is_domesticated
def __str__(self):
s_dom = "dom" if self.is_domesticated else "wild"
return ("Animal(%s, %s, %s)" % (self.class_, self.species, s_dom))
class Mammal(Animal):
def __init__(self, *args):
Animal.__init__(self, 'Mammal', *args)
cat = Mammal("cat", True)
print(cat)
lion = Mammal("lion", False)
print(lion)
The output:
Animal(Mammal, cat, dom)
Animal(Mammal, lion, wild)

Categories

Resources