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.
Attribute references
$( including\ functions )$ by .name
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
| '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
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
| <__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.
data attributes
$(instance\ variables)$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
| My name is Jason and 88 years old
|
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 instanceClass 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
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)
|
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
.
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()
|
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.
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.
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
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.