I think it may be still not always knowing when my code is (or should be) dealing with the class itself
Answer: Usually never.
vs when it is acting on an instance of that class.
Answer: 99.99% of the time.
One of the primary uses of a class is
encapsulation: you separate code off into its own little thing, and
only the class itself ever needs to care about the internals. So when you say that you're losing track of all the "self" calls and cannot follow what's actually happening, that's a red flag to me that something iffy is going on. Objects are
literally designed to solve exactly this problem of code complexity.
e.g. Let's use your card game idea as an example (which I'll imagine works like Yugioh or something, just to make this post extra gay). So you'll probably have a
Card
class or something:
Python:
class Card:
def __init__(self, attack):
self.attack = attack
def battle(self, other_card):
# Insert complicated battle logic here
return (self.attack - other_card.attack)
In the constructor
__init__
you take in the attack points as a parameter and assign them to the attribute
self.attack
so that they're stored in the class. Then you have a method
battle
that lets you battle another card with this card.
Now watch this:
Python:
card1 = Card(3000)
card2 = Card(2400)
card1.battle(card2)
That's what the 'main' code that uses our class looks like: we make two instances of the
Card
class and make them battle. Notice some things:
1. No
self
appears anywhere in this main code:
self
only ever appears in the class.
2. We only ever reference the
Card
class itself when we make the instances: after that point, we're strictly dealing with the instance objects
card1
and
card2
. (Now you
can reference the class itself for other things, and as you get more familiar with OOP you'll come across class methods/attributes where you do exactly that. But for now, we only reference the class itself when using it to instantiate the instance objects.)
3.
All the main code needs to know is that these Card
objects have a battle
method that can be used to make them fight.
Point 3 is the crux.
All of our code for implementing the battle logic can now sit comfortably inside our
Card
class: the main program doesn't need to know about
any of it or how it works. You can work on the battle code completely separately from the main logic of your game, because it's all inside the
Card
class and your main program only ever accesses that
Card
class through its
interface, i.e. its constructor and the
battle
method.
This means that you're free to change the battle logic all you want without breaking anything: all the main code knows is that
Card
objects have a
battle
method that it calls by passing in another
Card
object and (in this case) that method returns a number. Your main code doesn't know or
care about any of the internals, so you're free to modify them however much you want at any point.
This also means you only need to worry about the battle logic
inside that
Card
class. When you're working on the main code, you (or, for example, another programmer on your team) only need to worry about the
interface. "Yeah if you want these two cards to battle each other just do
first.card.battle(second_card)
and I'll take care of it all". That's the promise that your class is making to your main code (or less abstractly, the promise you'd be making to the programmer writing the main code if you were the programmer in charge of writing the
Card
class and implementing the battle logic).
This idea is awesome and very powerful, and its why Object-Oriented Programming dominates the software development landscape. It empowers you to have programmers working on different parts of a program without having to know every little bit of it themselves: they just need to know the interfaces. Or even if it's just you, it allows you to
separate concerns: if ever you wanted to change the battle logic in your card game, you know that
all you need to do is modify the
Card
class: none of the other code would ever need to change.
Hopefully that was useful.
