Why do some variables need `self.` and others don't, in 1 function?

Question:

i am starting my studies on OPP and I am struggling a little to grasp one concept. I believe this is very basic for you guys here, but I was hoping to get some help with it.

I have this code:

class Player:
    MAX_POSITION = 10

    def __init__(self):
        self.position = 0

    # Add a move() method with steps parameter     
    def move(self, steps):
        if self.position + steps < Player.MAX_POSITION:
            self.position = self.position + steps 
        else:
            self.position = Player.MAX_POSITION

It is correct, it runs… but I am struggling to learn why I need the prefix self on the position argument and not on the steps within the function move for the class Player?

any explanations or reading material suggestions are more than welcome
thanks

Asked By: Ricardo de Castro

||

Answers:

In an example like this:

def class MyClass:
    def __init__():
        self.attribute = None

    def my_method(self, some_parameter):
        self.attribute = some_parameter

attribute is defined as an attribute (or instance variable) on MyClass. This means that every instance of MyClass (a MyClass object) will have its own copy of attribute.

Since every normal method (a function defined on the class) expects the first argument to be the object itself (which you don’t have to pass; Python will pass it automatically), you can use self to refer to the instance the method was called on. (I say ‘normal’, because there are also ‘static’ and ‘class’ methods, but forget about those for now.)

So, in this example:

an_object = MyClass()
an_object.my_method(10)
print(an_object.attribute)

This works, because in the body of .my_method, the passed value, which gets assigned to some_parameter is assigned to the attribute attribute of an_object, because an_object is assigned to self in that call. Mind you, self could have been called anything; naming the first parameter self is just a convention you should follow.

The reason some_parameter does not need self. is because it is just another parameter of a function. It’s not an attribute of self — of the object the method was called on.

So, when compared to your code, you should say: steps does not need self. because it is not an attribute of a Player instance. It is just a parameter of a method defined on Player, and the value is accessible like any parameter is in a function body. The instance of a Player object is passed as self, and you can change its attributes by accessing them on self inside the function body.

A clue why you didn’t grasp this is that you call position an ‘argument’, but an argument is something passed to a function, to a specific parameter; and a parameter is an internal variable of a function that is assigned the argument. position is an attribute of an object.

So, when calling player.move(2), 2 is the argument, steps is the parameter in the body of move() and position is the attribute of player, with player being the Player class instance (or ‘object‘) accessible through the self parameter in the body of move().

Answered By: Grismar

Recently I faced quite similar problem and when searching the answer I stumbled your question and comprehensive answer given above by Grismar.
I just want to bring here some practical examples that will show a bit more and reveal under-the-hood gears.

Let’s start with your code. If we call a few instances like below this will bring us a correct result.

p1 = Player()
p1.move(100)
print('p1.__dict__ ->', p1.__dict__)

p2 = Player()
p2.move(1)
print('p2.__dict__ ->', p2.__dict__)

# output
# p1.__dict__ -> {'position': 10}
# p2.__dict__ -> {'position': 1}

Notations p1.__dict__ and p2.__dict__ show what values are actually assigned to a position variable.

So, what if we would like to use variable steps somewhere in another method (or methods)? I modified your code just a bit by adding additional method jump() to demonstrate what will happen.

This code will not work correctly for the method jump(). But the method move() still correctly executed.

class Player:
    MAX_POSITION = 10

    def __init__(self):
        self.position = 0

    # Add a move() method with steps parameter     
    def move(self, steps):
        if self.position + steps < Player.MAX_POSITION:
            self.position = self.position + steps 
        else:
            self.position = Player.MAX_POSITION

    def jump(self):
        print(steps * 2)


p1 = Player()
p1.move(100)
print('p1.__dict__ ->', p1.__dict__)
p1.jump()

# output
# p1.__dict__ -> {'position': 10}
# ---------------------------------------------------------------------------
# NameError: name 'steps' is not defined

To fix the problem and remove exception (here it is NameError: name 'steps' is not defined) we have to «glue» self and steps.

class Player:
    MAX_POSITION = 10

    def __init__(self):
        self.position = 0

    # Add a move() method with steps parameter     
    def move(self, steps):
        self.steps = steps # added line
        if self.position + steps < Player.MAX_POSITION:
            self.position = self.position + steps 
        else:
            self.position = Player.MAX_POSITION

    def jump(self):
        print('jump() method ->', self.steps * 2) # added self


p1 = Player()
p1.move(100)
print('p1.__dict__ ->', p1.__dict__)
p1.jump()

# output
# p1.__dict__ -> {'position': 10, 'steps': 100}
# jump() method -> 200

Now we can see that the method move() executed correctly (variable position assigned to 10). The method jump() executed without any exceptions as well. Instance p1 has now two available variables, they are 'position': 10 and 'steps': 100.
In other words self.variable notation gives the option to use the variable beyond the «parent» method.

Answered By: Serzhev
Categories: questions Tags:
Answers are sorted by their score. The answer accepted by the question owner as the best is marked with
at the top-right corner.