Objects and Classes!

The learning continues. Right now I’m learning about objects and classes. In the example that I’m reading out of the book (python for everyone, 2nd edition) it’s using the example of a Counter Class. Right now I’m learning how to define methods within the class. The book says the following:

"An instance variable is a storage location that is present in each object of the class. In our example, each Counter object has a single instance variable named _value" In the method definition, it users the following:

def click(self) :
self._value = self._value + 1

I’m wondering:

  1. Is _value required for all methods, or is this just the instance variable used in this method?
  2. Can somebody explain the importance of .self as the book does a pretty terrible job

Thanks!

_value is just what they decided to use here. You can use all the same variable names you would for a regular variable (letters, numbers, underscore, etc). However by convention _names with one underscore at the start signify “private” attributes, classes, methods etc that aren’t intended to be used by outsiders.

The first parameter of methods is special, and conventionally called self (though again you don’t have to). When you call a method on an object, the object itself is passed in as the first argument. So that’s how you access attributes on the object you’re dealing with. Think of it like obj.method(1, 2, 3) being transformed into TheClass.method(obj, 1, 2, 3).

1 Like

That _value is simply an example they used. Normally you would also
initialize it with some default in a real-world situation, in case
it’s referenced before it gets set (or else you’d need additional
error handling at the very least).

As for self, it’s a handy reference to the object’s instance, so
self._value from within the class definition there refers to a
_value attribute associated with the specific instance of your
Counter class being called.

Let’s say you had two Counter objects like so:

foo = Counter()
bar = Counter()

foo.click()
foo.click()

bar.click()

After that, you should expect foo._value to have been incremented
twice while bar._value was only incremented once. The _value
attributes are distinct between each object even though they share
the same class and methods.

1 Like

On a similar topic, I’m also wondering about constructors. So the book I’m using says “Python allows you to define only one constructor per class.” What if I’m creating a method that can or cannot have an argument passed through it (for example, list() ).

Let’s say I want to create a class called Item that can have any of the following passed through it:

Item("Popcorn")
Item("Popcorn", 2.50)
Item()

Would it be

def __init__(self, description = "", price = 0.0)
self._price = price
self._description = description

If so, does this mean that only arguments in the order of item(), item(description), and item(description, price) will work? Could the argument have Item(2.50), for example?

You could rely on keyword arguments, and pass Item(price=2.50) in
that case.

There’s a lot to cover in regards to classes and instances. I remember
finding it really hard to wrap my head around the idea when I first
learned about it. So don’t stress if it seems a bit mysterious for a
while.

One way to think about a class and it’s instances is to think of them as
a convenient way to keep some data together with the functions that
operate on that data. For example, you might define a record that holds
data:

A Student record consists of three pieces of data: a name, a student ID,
and a year level (grade 1 to 6). You create a student record:

cartman = ("Eric Cartman", get_new_student_id(), 4)

You might have a bunch of functions that operate on Student records:

def find_classroom(student, time):
    # return the classroom the student is supposed to be in
    # at that time

def get_grade_average(student):
    # return the average grade for the student in each of
    # their classes

and more. To use them, you pass the student record to the function:

find_classroom(cartman, "Monday 2pm")
# returns "gym"

Those functions might be scattered around your program, and they might
clash with other functions:

def get_grade_average(teacher):
    # return the average grade the teacher gives to their students

Using a class gives a way to bring order to the chaos by bringing all
the relevant Student functions into one place, where they will not
interfere with Teacher functions.

class Student:
    def __init__(self, name, starting_year):
        # set up the student record
        self.name = name
        self.id = get_new_student_id()
        self.year_level = starting_year

    def find_classroom(self, time): ...

    def get_grade_average(self):

Notice that we change the parameter “student” to self. This is just a
convention, but a powerful and common one. Now we write:

cartman = Student("Eric Cartman", 4)
cartman.find_classroom("Mon 2pm")
# returns "gym"

and the instance cartman gets passed to the function as “self”. This
happens automatically and you don’t have to worry about how it happens.

We call those functions inside a class methods, and they are logically
equivalent to regular functions, the syntax is just moved around a bit:

find_classroom(cartman, time)

cartman.find_classroom(time)

Notice the special method __init__. We call that “the constructor”,
although if we want to be really precise, it’s actually an iniatialiser,
there’s another method that is the real constructor. But don’t worry
about that, most of the time you only care about __init__.

When you call the class as if it were a function:

cartman = Student("Eric Cartman", 4)

Python creates a new student record, an instance of the class, and calls
the constructor __init__ to populate its fields and do any other
preparation needed.

Your constructor is a method like any other method, so you can give it
multiple arguments, with default values, and do anything you like inside
it. There are no hard rules that every class has to do this or do that.
You will see that my Student class has no _value attribute, and your
Collection class has no name or id attributes.

The only rules are:

  • the __new__ constructor method must return an instance (for advanced
    usage only)

  • the __init__ so-called constructor (actually an initialiser) must
    populate the instance’s attributes correctly

  • by convention, your methods refer to the instance being worked on
    as “self”

(and even those rules are more like guidelines than unbreakable rules).

Remember that original Student record, that looked like three fields?

cartman = ("Eric Cartman", get_new_student_id(), 4)
# Name field, ID field, year level field.

The downside of that is that it is tricky (but not impossible) to refer
to those fields by name. We use numbered items instead:

cartman[0]  # returns "Eric Cartman"

By moving to a class, it becomes totally natural to refer to them using
named attributes:

cartman.name  # returns "Eric Cartman"

Those named attributes are sometimes called “instance variables”,
although that’s more common in the Java community than Python. In Python
we tend to call them “attributes”, or sometimes “instance attributes”.

There are also class attributes that live on the class, and are
shared by all instances. So we might have:

Student.school  # returns "South Park Elementary"

A cool feature of class attributes is that, because they are shared by
all instances, you can do this:

cartman.school  # returns "South Park Elementary"

but there’s a catch. If you try to change the name of the school:

cartman.school = "Mr Hat Memorial Elementary School"

it will only change for that one instance, not for the other students.

Which book are you reading?

Technically, your classes can have any number of constructors. For
example, the builtin dict class has the regular constructor that you
can call using either the special dict syntax:

{'a': 1, 'b': 2}

or using function call syntax:

dict(a=1, b=2)

but it also has an alternative constructor that you call from a
method:

dict.fromkeys(['a', 'b'])
# like {'a': None, 'b': None}

So you can do the same with your own classes. You can make as many
constructors as you want!

As far as your __init__ method goes, of course you can give the
parameters default values (like any other function or method):

class Item:
    def __init__(self, description="", price=0.0):

You can call Item in three ways using positional arguments:

  • instance = Item() # uses default for both description and price

  • instance = Item(“popcorn”) # uses default for price

  • instance = Item(“popcorn”, 2.50) # no defaults

What happens if you call Item(2.5)? Do you get an error? If not, what is
the result? (Hint: what do the instance._price and instance._description
get set to?)

You can call Item in three more ways using keyword arguments:

  • instance = Item(description=“popcorn”, price=2.50) # no defaults

  • instance = Item(description=“popcorn”) # use default for price

  • instance = Item(price=2.50) # use default for description

And we can combine the two:

  • instance = Item(“popcorn”, price=2.50) # no defaults

But this will not work:

  • instance = Item(2.50, description=“popcorn”) # an error

Can you see why? (Hint: try it and see if you can understand the error
message you get.)