Home OOP: Class
Post
Cancel

OOP: Class

Class

  • Class ia a blueprint for creating an instance. Creating a new class creates a new type of object with which new instances of that type are made.
  • Class can hold attributes describing states and methods describing behaviors.
  • In Python class, all attributes are public by default (except private variables which we’ll see later)
  • Class are themselves objects too!
  • When a class definition is created, a new namespace is created which acts as a local scope. Also, the class object is created which is a wrapper around the class namespace.

Class Object

The class object has two operations.

  1. Attribute references $( including\ functions )$ by .name
  2. Instantiation $(creating\ an\ instance)$
1
2
3
4
5
class Person:
    planet = "earth"

    def greeting(self):
        print("hello world")

Let’s print the class object. You can see that Person is a class object which is located in the global namespace with the name Person.

1
2
3
4
print(Person)
print(globals()['Person']) # `Person` class obj is in the global namespace!
print(Person == globals()['Person'])
print(Person.__name__)
1
2
3
4
<class '__main__.Person'>
<class '__main__.Person'>
True
Person

Let’s now print the attributes of Person class object.

1
2
print(Person.planet) # string object
print(Person.greeting) # function object
1
2
earth
<function Person.greeting at 0x7f9cc796b310>

As you can see, you can directly access attributes through .name.

Bonus: When you read source codes, you probably have seen doc strings$(documentation\ strings)$. The doc strings are usually located at the top of the class namespace with ’'’docstring’’‘. What’s up with this? In fact, the strings declared at the top of the class namespace is assigned to __doc__ attribute of the class. Let’s check this out.

1
2
3
4
5
6
class Person:
    "This is the doc string"
    planet = "earth"

    def greeting(self):
        print("hello world")
1
Person.__doc__
1
'This is the doc string'

You can see the whole list of class attributes using __dict__ keyword which contains all the writable attributes of the object. Since class is also an object, it surely has one!

1
Person.__dict__
1
2
3
4
5
6
mappingproxy({'__module__': '__main__',
              '__doc__': 'This is the doc string',
              'planet': 'earth',
              'greeting': <function __main__.Person.greeting(self)>,
              '__dict__': <attribute '__dict__' of 'Person' objects>,
              '__weakref__': <attribute '__weakref__' of 'Person' objects>})

You can actually see all those __doc__, planet, and greeting attributes up there.
By the way, we’ll talk about class (private) attributes and methods in the later posts.

Cool! We’ve gone deeper into what class object is and how we can access the class attributes. Now, let’s dive into how to instantiate from the class object

Instantiation

Class instantiation is performed just like you call any other function by Person(). But wouldn’t it be great if we can assign some initial states? This is why we write a special methods __init__. Notice that there’s NO problem even if you don’t write __init__ and create an instance! See below,

1
2
p1 = Person()
print(p1)
1
<__main__.Person object at 0x7f9cc6d66a00>

But although this is acceptable, what the heck would you want that? Let’s try to see how __init__ works.

1
2
3
4
5
6
class Person:
    def __init__(self):
        print("Instantiation successful!")

p1 = Person()
print(p1)
1
2
Instantiation successful!
<__main__.Person object at 0x7f9cc6d663d0>

Great! You just created an instance of the Person class and assign to a variable p1. But wait.. what is that self? Why do we even need that every time we write __init__?

When a class defines an __init__ method, class instantiation automatically invokes __init__() for the class instance$(which\ is\ p1\ in\ this\ case)$ just created! That’s why we want to take advantage of this automation. Of course, just like any other functions with other parameters.

Back to our question: why self as a default parameter? When you instantiate using Person(), self is actually passed as an argument to the special __init__ method. Therefore, __init__ MUST have self parameter. The self itself refers to the initialized instance which will be again equal to what is stored in p1 in the example above. Let’ see what it means in code.

1
2
3
4
5
6
class Person:
    def __init__():
        print("Instantiation successful!")

p1 = Person()
print(p1)
1
2
3
4
5
6
7
8
9
10
11
12
---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

Input In [82], in <module>
      2     def __init__():
      3         print("Instantiation successful!")
----> 5 p1 = Person()
      6 print(p1)


TypeError: __init__() takes 0 positional arguments but 1 was given

Look at the exception message! We never put any argument in Person() which initialized an instance but it says but 1 was given!. To wrap up, even if you didn’t write any argument in Person(...), the initialized instance is automatically passed to the __init__ method so we definitely have to have a paremter that handles that argument. This is why we need __init__(self).

1
2
3
4
5
class Person:
    def __init__(self):
        print("Instantiation successful!")

p1 = Person()
1
Instantiation successful!

We’ve seen how __init__ method and self actually work behind the scene. With this magical functionality, we can do some cool stuff like setting initial states. Remember you can always have any number of parameters for __init__ method as long as you can self. In this way, you can manipulate initial states for instances. Remember! self in __init__ method refers to the newly created instance so if you want to set initial states for that particular instance, you must use self.xxx = xxx

1
2
3
4
5
6
7
8
9
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
        print(f"You just created an instance {self}\nwhich holds attributes name: {self.name}, age:{self.age}")
        return None


p1 = Person("Jason", 45)
1
2
You just created an instance <__main__.Person object at 0x7f9cc6d7fc70>
which holds attributes name: Jason, age:45

I mentioned above that the self indeed is exactly the same as the newly created instance which is stored in the variable name called p1. Since python stores reference of the object into the variable name, p1 should be equal to self. Let’s see if this is true.

1
print(p1)
1
<__main__.Person object at 0x7f9cc6d7fc70>

Notice that the memory location of p1 is exactly the same as that of self! Now we have a full understanding of what’s really happening when creating an instance. Great job.

Side note: By default, __init__ method must return None. If you ever try to return anything else, it will raise an TypeError.

1
2
3
4
5
class Person:
    def __init__(self):
        return "Hello World"

p1 = Person()
1
2
3
4
5
6
7
8
9
10
11
---------------------------------------------------------------------------

TypeError                                 Traceback (most recent call last)

Input In [86], in <module>
      2     def __init__(self):
      3         return "Hello World"
----> 5 p1 = Person()


TypeError: __init__() should return None, not 'str'

It’s worthless, but you can actually write return None just to see if it works

1
2
3
4
5
class Person:
    def __init__(self):
        return None

p1 = Person()

Cool! I believe you’ve gained sufficient knowledge to deal with class object and instantiation. Now, let’s go deep into what we can do with Instance Objects

Instance Objects

So far, we’ve talked about what class object is and how we can create an instance object from the class which is the ultimate blueprint for instances. Now, what are the cool things we can do we instance objects?

The only operations allowed for instance objects are attribute references. There are two types of attribute references.

  1. data attributes $(instance\ variables)$
  2. method attributes $(function\ that\ “belongs”\ to\ an\ object)$

-> In python, method does not specifically belong to class. But for simplicity, method will refer to functions of instance objects

Remember: Everything is an object. The instances variables are objects and so are method objects

1
2
3
4
5
6
7
8
9
10
11
class Person:
    planet = "Earth"
    def __init__(self, name, age):
        self.name = name
        self.age = age

    def introduce(self):
        print(f"My name is {self.name} and {self.age} years old")

p1 = Person("Jason", 88)
p2 = Person("Amanda", 102)

Notice that Person class has planet and f attributes. When we create an instance p1 from Person class, the attributes of the class come into existence for that p1 instance object. Therefore, we can access the attributes of Person class by p1.xxx.

Here, it’s worth mentioning that the class attribute planet = "Earth" is not assigned using self. This means that this variable does not belong to any specific instance object but belongs to the class. Even though we can access this variable from instance objects, it will always be "Earth".

1
2
p1.introduce()
p2.introduce()
1
2
My name is Jason and 88 years old
My name is Amanda and 102 years old

As you can see above, we can now access method objects for each instance object. Since we assigned instance varialbes differently for p1 and p2 upon instantiation, the name and age are different.

1
2
3
4
print(p1.planet)
print(p2.planet)
print(p1.planet is p2.planet)
print(id(p1.planet) == id(p2.planet))
1
2
3
4
Earth
Earth
True
True

As mentioned above, both will have the same planet value.

INFO: Person.introduce is not same as p1.introduce or p2.introduce. Person.introduce is an function object while p1.introduce and p2.introduce are method objects

1
2
3
print(type(Person.introduce))
print(type(p1.introduce))
print(type(p2.introduce))
1
2
3
<class 'function'>
<class 'method'>
<class 'method'>

Now, let’s talk about something very important. You may have noticed there’s self in the class function attribute def introduce(self). It made sense for __init__ but why for this?

The answer is when you call method object from p1 by p1.introduce(), what’s really happening behind the hood is Person.introduce(p1). They are exactly equivalent. Let’s see with an example.

1
p1.introduce()
1
My name is Jason and 88 years old
1
Person.introduce(p1)
1
My name is Jason and 88 years old

Boom! since it’s tiresome to write Person.introduce(p1) everytime we call methods, the developer of python made it possible to call it with p1.introduce(). This is why you need self in class function attributes as well since we’re literally passing p1 in Person.introduce(p1).

When you have other parameters than self, it works exactly the same.

1
2
3
4
5
6
7
8
9
10
11
class Person:

    def __init__(self):
        pass

    def introduce(self, name, mood):
        print(f"My name is {name} and I'm {mood} today.")

p1 = Person()
p1.introduce("Jason","happy")
Person.introduce(p1, "Jason", "happy")
1
2
My name is Jason and I'm happy today.
My name is Jason and I'm happy today.

When you have multiple parameters, the instance object is inserted before the passed arguments. So in this case, p1, "Jason", "happy"

Class and Instance variables


Putting everyting general,

  • Instance variables are unique to each instance
  • Class variables are shared among all instances
1
2
3
4
5
6
7
8
9
10
11
12
class Person:
    class_var = "This is class variable"

    def __init__(self, inst_var):
        self.inst_var = inst_var

p1 = Person('Person 1')
p2 = Person('Person 2')

print(Person.class_var)
print(p1.class_var)
print(p2.class_var)
1
2
3
This is class variable
This is class variable
This is class variable

In other words, you cannot access inst_var from Person class.

1
print(Person.inst_var)
1
2
3
4
5
6
7
8
9
---------------------------------------------------------------------------

AttributeError                            Traceback (most recent call last)

Input In [129], in <module>
----> 1 print(Person.inst_var)


AttributeError: type object 'Person' has no attribute 'inst_var'

What if there’re both class variable and instance variable with same name?

1
2
3
4
5
6
7
8
class Person:
    var1 = "hello"
    def __init__(self, var1):
        self.var1 = var1

p1 = Person("bye")
print(Person.var1)
print(p1.var1)
1
2
hello
bye

Obviously, Person.var1 will print the original “hello” but p1.var1 prints modified “bye” value. If both an instance and class have same variable name, the attribute lookup prioritizes the instance variable.

Some remarks

A function object that is a class attribute defines a method for instances of that class. However, it’s not necessary to always textually include the function object inside the indented block of the class. You can just simply assign a normal function declared outside the class scope and assign it.

1
2
3
4
5
6
7
8
def f(self):
    print("hello")

class Person:
    g = f

p = Person()
p.g()
1
hello

Although f() is declared outside the class scope, you referenced it and made g as a class attribute, there’s absolutely no difference!

One last note

Everything is an object in python! Therefore, it has a class (also called its type) which is stored in object.__class__.

The built-in function type(obj) generally returns the same object as obj.__class__.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
def div():
    print("-"*50)
a = 1
b = "jason"
c = lambda x: x + 1

print(object.__class__)
div()

print(a.__class__)
print(type(a))
div()

print(b.__class__)
print(type(b))
div()

print(c.__class__)
print(type(c))
1
2
3
4
5
6
7
8
9
10
<class 'type'>
--------------------------------------------------
<class 'int'>
<class 'int'>
--------------------------------------------------
<class 'str'>
<class 'str'>
--------------------------------------------------
<class 'function'>
<class 'function'>

Private Variables


Straight to the point, python DOES NOTE support true private variables. However, there’re conventions and name mangling you might find useful.

  1. Sometimes we do not want to show our internal details of the class. One typical convention is that a name prefixed with _, _name should be treated as an private part of API.

  2. Any identifier of the form __name $(at\ least\ two\ leading\ underscores\ and\ at\ most\ one\ trailing\ underscore)$ is textually replaced by _classname__name with leading underscore(s) stripped.

1
2
3
4
5
6
7
8
9
10
11
12
13
class Person:
    __var1_ = "hello" # at least 2 leading _ and at most 1 training _
    __var2 = "bye" # at least 2 leading _ and at most 1 training _
    ___var3 = "what's up" # at least 2 leading _ and at most 1 training _
    _nonprivate1 = "public1" # only one leading _ : not private (no name-mangling)
    __nonprivate2__ = "public2" # two training _ : not private (no name-mangling)

p = Person()
print(Person._Person__var1_)
print(Person._Person__var2)
print(Person._Person___var3)
print(Person._nonprivate1) # at least two leading underscores required but found ONLY ONE.""
print(Person.__nonprivate2__) # at most one trailing underscore required but found TWO
1
2
3
4
5
hello
bye
what's up
public1
public2
1
Person.__dict__
1
2
3
4
5
6
7
8
9
mappingproxy({'__module__': '__main__',
              '_Person__var1_': 'hello',
              '_Person__var2': 'bye',
              '_Person___var3': "what's up",
              '_nonprivate1': 'public1',
              '__nonprivate2__': 'public2',
              '__dict__': <attribute '__dict__' of 'Person' objects>,
              '__weakref__': <attribute '__weakref__' of 'Person' objects>,
              '__doc__': None})

It might be a bit hard to understand at first, but those 5 examples will be enough for you to understand the subtleties.


Congratulations! We have looked into much details of class object, instance object, intricacies of Instantiations as well as private variables. In the next post, we’ll jump into Inheritance.

This post is licensed under CC BY 4.0 by the author.
Trending Tags