Search code examples
pythonclassinstanceinit

Python: difference class with __init__ and class without it


I'm learning how classes works and I got stuck on a thing which I can't explain, I also didn't find a solution on the Internet so here am I with my first question on StackOverflow.

class Swords:
    damage = 5

Here is a class with only one line, with attribute damage. Let's make an instance of the class:

sharp_sword = Swords()

And here is the moment of truth. My class doesn't have an init function, so right now my sharp_sword.__dict__ is empty - my object of the class has an empty namespace, it doesn't have any attributes or methods. Well, we can create an attribute since we already defined it in the class itself:

sharp_sword.damage = 10

Now if we print(sharp_sword.damage) the return will be 10, if we print(Swords.damage), the return will be 5. That means that our instance of the class has its own attribute - and it is without any init inside my class. I can create another object, set its damage to 15, and I will have two objects with different damage. Since we didn't change damage in class, if we print(Swords.damage), the return will be 5.

The question coming by itself - why then should I use __init__ inside my class to set properties for objects if I can do it without it with even fewer lines of code in my class?

class SwordsWithoutInit:
    damage = 5

class SwordsWithInit:
    def __init__(self):
        self.damage = 5

big_sword = SwordsWithoutInit()
big_sword.damage = 10

sharp_sword = SwordsWithInit()
sharp_sword.damage = 20

print(f'big sword (class {type(big_sword)}) damage equals {big_sword.damage}')
print(f'sharp sword (class {type(sharp_sword)}) damage equals {sharp_sword.damage}')

# it both works the same way
# note that even if you create another object of class without init, it still will work the same as with init, there won't be any errors

another_sword = SwordsWithoutInit()
another_sword.damage = 33

print(f'big sword (class {type(big_sword)}) damage equals {big_sword.damage}')
print(f'another sword (class {type(another_sword)}) damage equals {another_sword.damage}')

print(f'Class without init still will have damage attribute equals to {SwordsWithoutInit.damage} in itself - we didnt change it')

'''
output:
big sword (class <class '__main__.SwordsWithoutInit'>) damage equals 10
#sharp sword (class <class '__main__.SwordsWithInit'>) damage equals 20
big sword (class <class '__main__.SwordsWithoutInit'>) damage equals 10
another sword (class <class '__main__.SwordsWithoutInit'>) damage equals 33
Class without init still will have damage attribute equals to 5 in itself - we didnt change it
'''

Solution

  • The way you've set this up, via some odd sequence you'll sort of get the result you expect either way, but it's mostly due to int being immutable.

    So, let's look at a mutable type instead:

    class Sword:
      pass
    
    class Spear:
      pass
    
    # All players get a sword by default
    class Items:
      weapons = [Sword()]
      
    player1_items = Items()
    player2_items = Items()
    
    # give player2 a spear
    player2_items.weapons.append(Spear())
    
    # player1 should still just have a sword right?
    # whoops! they have a spear too!
    print(player1_items.weapons)
    

    live example

    You might now ask "well then I'll just use __init__ if I have mutable types." and I suppose that would work for the cases you're describing. It's just a strange way of structuring the code and will make it harder to read over time. To answer the question why you should favor assigning to self even "if I can do it without it with even less lines of code"..

    1. Minimizing the number of lines isn't a good motivation to do something.
    2. Keeping your code readable, maintainable, and fairly consistent is a good motivation.