Python: Classes: Methods 2

LEARN PYTHON: CLASSES

Hi - I have pasted a whole chapter here. Sorry!

Methods

Methods are functions that are defined as part of a class. The first argument in a method is always the object that is calling the method. Convention recommends that we name this first argument self . Methods always have at least this one argument.

We define methods similarly to functions, except that they are indented to be part of the class.

class Dog(): dog_time_dilation = 7 
   def time_explanation(self): 
         print("Dogs experience {} years for every 1 human year.".format(self.dog_time_dilation)) 

pipi_pitbull = Dog() 
pipi_pitbull.time_explanation()

Prints “Dogs experience 7 years for every 1 human year.”

Above we created a Dog class with a time_explanation method that takes one argument, self , which refers to the object calling the function. We created a Dog named pipi_pitbull and called the .time_explanation() method on our new object for Pipi.

Notice we didn’t pass any arguments when we called .time_explanation() , but were able to refer to self in the function body. When you call a method it automatically passes the object calling the method as the first argument.

In the above chapter When you call a method it automatically passes the object calling the method as the first argument. - need more explanation to understand this line.

We called dog_time_dilation using self - so i understood that we used the argument to call a variable and not the method. And then this variable was automatically passed to the method call as an argument.

Please advise if what i think is right. Any input to understand this will be great help.

Thank you.

Blockquote

When you ask a question, don’t forget to include a link to the exercise or project you’re dealing with!

If you want to have the best chances of getting a useful answer quickly, make sure you follow our guidelines about how to ask a good question. That way you’ll be helping everyone – helping people to answer your question and helping others who are stuck to find the question and answer! :slight_smile:

Hey @rdb20

If we define the class, like so:

class Dog():
    dog_time_dilation = 7
    def time_explanation(self):
        print("Dogs experience {} years for every 1 human year".format(self.dog_time_dilation))

when we do pipi_pitbull = Dog(), we’re creating a new instance of the Dog class and storing it in the variable pipi_pitbull.

When we call pipi_pitbull.time_explanation(), what we’re doing is the same as calling Dog.time_explanation(pipi_pitbull).

The method, time_explanation(), knows that it belongs to an instance called pipi_pitbull - self in this case is pipi_pitbull, and Python knows that already so we don’t need to tell it.

For example:

# We want a cat!
class Cat:
    lives = 9

    def lose_a_life(self):
        self.lives -= 1

    def lives_left(self):
        print("How many lives do I have? Why, %d of course!" % self.lives)

# We have a new cat! Sir Pounce-a-lot!
sir_pouncealot = Cat()

# How many lives does he have?
# Notice that we're using the Cat class directly, and passing in the instance of Sir Pouncealot.
Cat.lives_left(sir_pouncealot)  # output: "How many lives do I have? Why, 9 of course!"

# Invoke the lose_a_life method which is part of the Sir Pouncealot instance.
sir_pouncealot.lose_a_life()

# Check again how many lives. This time we're going direct to Sir Pouncealot!
sir_pouncealot.lives_left() # output: "How many lives do I have? Why, 8 of course!"

# Now, we'll use the Cat class directly to make Sir Pouncealot lose a life.
# Again, we pass him in as an argument.
Cat.lose_a_life(sir_pouncealot)

# How's our cat doing?
sir_pouncealot.lives_left() # output: "How many lives do I have? Why, 7 of course!"

# Actually, we'd also quite like a dog...
class Dog:
    lives = 1

    def lives_left(self):
        if self.lives <= 0:
            print("I think I need a nap... *bork!*")
        else:
            print("I have %d life! *woof!*" % self.lives)

# We have a new dog!
woofenstein = Dog()

# Let's see how he is...
woofenstein.lives_left() # output: "I have 1 life! *woof!*"

# ... and see what happens if we introduce him to the cat...
Cat.lose_a_life(woofenstein)

# ....doggo?
woofenstein.lives_left() # output: "I think I need a nap... *bork!*"

In each of the instances where we’re calling methods of the form instance.method(), like woofenstein.lives_left(), Python already knows which instance it’s tied to so we don’t need to tell it.

If we use the generic Class method, like Cat.lives_left(), notice how we can also use that on the Dog instance because they both have a class attribute of lives. We simply point the method at the right instance, and it does the rest.

Does that help at all? :slight_smile:

1 Like

Yes. A lot. thank you. I didn’t know that same name class variables can be used to interact with objects and change their value!!

ooohhhh! so pipi_pitbull is the object calling the method time_explanation() and is passed as a argument … that is self = pipi_pitbull… Got it :star_struck::ok_hand::+1:

So we have two ways of getting the same output? Why is object.method() preferred?

Woofenstein is Dog’s object. How come its able to use Cat’s method? Is it because they have same class variable. If the class variable was not same then… we couldn’t have used different methods on the same object??
Can you please give me an example from in-built classes.

Thank you… the above explanation was really great - just need a little nudge again

1 Like

The class is the generalised form, whereas when you assign that class to a variable you’re creating a specific instance of that class and giving it a name. The base class and the named instance will share the same methods, because the named instance copies the base, so we can use them either way around.

Yes. You’re calling the .time_explanation() method which belongs to the pipi_pitbull instance of the Dog class.

Yes, Cat.lives_left(sir_pouncealot) is the same as sir_pouncealot.lives_left().

I think instance.method() is preferred because it’s more obvious what’s going on - i.e. you’re calling the method that belongs to that instance.

Yes, it’s precisely because both the Cat and Dog classes define a class attribute called lives.

When we do Cat.lose_a_life(woofenstein), we’re telling Python that self = woofenstein.

The code inside the method, self.lives -= 1, then becomes the same as doing woofenstein.lives -= 1.

Not off the top of my head… sadly.

1 Like

I have just completed the review of Python : Classes and done their exercise. can you help me we few more basic questions please.

[https://www.codecademy.com/courses/learn-python-3/lessons/data-types/exercises/review?action=resume_content_item](http://Classes: review exercise)

My code:

class Student:
  def __init__(self, name, year):
    self.name = name
    self.year = year
    self.grades = []
    print(self.grades)
  
  def add_grade(self, grade):
    if type(grade) == Grade:
      return self.grades.append(grade) 
    
  #def get_average(self, score)
  
      
class Grade:
  minimum_passing = 65
  
  def __init__(self, score):
    self.score = score
  def is_passing(self, score):
    if self.score < minimum_passing:
      return 'You did not pass this subject'
    else:
      return ' YOu aced it!'
    
    
roger = Student("Roger van der Weyden", 10)
sandro = Student("Sandro Botticelli", 12)
pieter = Student("Pieter Bruegel the Elder", 8)
pieter.add_grade(Grade(100))

Point 7 and 8 - Grade does not have grade attribute so it won’t add 100 to pieter’s grade list.
Is that correct or am I missing something?

I am getting confused between the terms grade and score - will it help if i defined various grades corresponding to different scores?

Bottom line I did not understand the last 3 instructions properly.

Please help.

1 Like

I presume that the line that has you stumped is this one:
pieter.add_grade(Grade(100))

Let’s break it down into what’s happening, so we can step through the process.

  • Grade(100) is creating a new Grade object, with a score of 100.
  • .add_grade() is a class method belonging to the Student class. It takes a single argument, grade, which must be of the type Grade (i.e. it must be a Grade class instance).
  • pieter is a Student instance, with the following instance attributes:
pieter.name = "Pieter Bruegel the Elder"
pieter.year = 8
pieter.grades = []

So, what is our line of code doing? We’re calling pieter.add_grade(), which is the add_grade() method which belongs to pieter. This takes care of the value of self, as we already know, so the only argument we need to give to add_grade is the grade itself.

We’re creating a Grade object directly, and passing this in to Pieter’s method. The process is then like so: (I’ll write it in Python, with comments # to explain what Python is “thinking” as we go.)

# Here we go. Call the function!
pieter.add_grade(Grade(100))

# We're now in the `add_grade` method.
# What are the arguments we've been given?
grade = Grade(100) # this is the value we passed for the 'grade' parameter

# Right, let's run the code. 
if type(grade) == Grade: # Line 1 of add_grade - have we been given an instance of a Grade object.

# We have been given a Grade object, so on we go...
return self.grades.append(grade) # We're doing a couple of things here. 
# self.grades.append(grade) adds the Grade(100) object to pieter's list of grades. The return value of list.append() is 'None'.
# Ultimately, we return 'None'. We're not doing anything further with it, so this return value is forgotten.

If you then run pieter.grades, you’ll see the output [<__main__.Grade object at 0xmemory_reference>]. This is what we’d expect, as we’ve added a Grade object to the list.

Hopefully that’s not confused you further. :slight_smile: I can’t think of a better way to walk you through that, though.

1 Like

Nah! Actually the concept has become a lot clearer. Only few bits left -

pieter.add_grade(Grade(100)) - did we just do that in this line?

Here I wanted to go further and have the grade list have the value 100. That’s what i did not understand that why it returned the value None? I thought it would return 100 or may be A,B or C (if i had modified the scores to Grades)

How can i go ahead and add a passed value to the grade list and have it printed for 3 different students.

Thanks for all your replies. they have been great help!

1 Like

Grade(100) is an instance of the Grade class, where Grade.score is 100. That we didn’t bother to assign it to a variable before passing it into the method makes no difference, it’s still an instance of the Grade class and so is a valid input to the .add_grade() method. (I think that’s what was confusing you; your question was a little ambiguous.)

There’s a couple of things going on here.

You aren’t capturing the return value from pieter.add_grade() anywhere, so it doesn’t matter that it returns None. Even so, it would make no logical sense for this method to return the Grade score after execution - you already knew what that was, as you input it.

If you wanted a meaningful return, for example a confirmation string, you could amend as follows:

def add_grade(self, grade):
    if type(grade) == Grade:
      self.grades.append(grade)
      return "Added grade with score %s" % grade.score

This way, if we do print(pieter.add_grade(Grade(100))) we get back Added grade with score 100 on the console.

This doesn’t fix the “issue” that, if you were to print pieter.grades you’d still get a load of <__main.Grade object.... inside the list.

You could work around that by doing self.grades.append(grade.score), so you’d get a list of integers, but you then lose the ability to do something like this:

for grade in pieter.grades:
    print(grade.is_passing())

Just storing the score value, rather than the whole object, robs you of the ability to leverage the Grade methods later on if you wanted.

What would make more logical sense - and which would satisfy your other thought of being able to print a meaningful “grade” object - would be for you to add a __repr__ dunder to your Grade class, like this:

from random import randrange

class Student:
  def __init__(self, name, year):
    self.name = name
    self.year = year
    self.grades = []
    print(self.grades)
  
  def add_grade(self, grade):
    if type(grade) == Grade:
      self.grades.append(grade)
      return "Added grade with score %s" % grade.score
    
  #def get_average(self, score)
  
      
class Grade:
  minimum_passing = 65
  
  def __init__(self, score):
    self.score = score

  def __repr__(self):
    if self.score > 80:
      return "A"
    elif self.score > 75:
      return "B"
    elif self.score > 70:
      return "C"
    elif self.score > 65:
      return "D"
    else:
      return "F"

  def is_passing(self):
    if self.score < self.minimum_passing:
      return 'You did not pass this subject'
    else:
      return ' YOu aced it!'
    
    
roger = Student("Roger van der Weyden", 10)
sandro = Student("Sandro Botticelli", 12)
pieter = Student("Pieter Bruegel the Elder", 8)
pieter.add_grade(Grade(100))

for x in range(4):
  pieter.add_grade(Grade(randrange(0,100)))

for grade in pieter.grades:
  print(grade,end=', ')

Notice how I’ve defined the grade boundaries inside of the __repr__ dunder, and used randrange() with a for loop to put some random grades in for Pieter.

The result of this code (different every time, obviously!) is:

A, C, F, A, A,

You already know how to add a value to the grade list, we’ve done it several times now in this thread. To add them to a different student, call the method of a different student - e.g. roger.add_grade().

I’ve also given you the answer to making it print friendly. :slight_smile:

1 Like

thank you. that was great!

:+1:

1 Like