Understanding Python's Class Better
written by Zeyu Yan, Ph.D., Head of Data Science from Nvicta AI
Data Science in Drilling is a multi-episode series written by the technical team members in Nvicta AI. Nvicta AI is a startup company who helps drilling service companies increase their value offering by providing them with advanced AI and automation technologies and services. The goal of this Data Science in Drilling series is to provide both data engineers and drilling engineers an insight of the state-of-art techniques combining both drilling engineering and data science.
In this episode, we will cover how to use Python's class in a correct way.
A First Example
As a first example, let's define the following Python class:
class Student:
def __init__(self, name, score, num_of_pets):
self.__name = name
self.__score = score
self._nop = num_of_pets
def print_score(self):
print(f"{self.__name}, {self.__score}")
Let's create a student instance:
michael = Student("Michael Jordan", 23, 2)
Call the print_score method from the instance,:
michael.print_score()
The result is as follows:
Michael Jordan, 23
The attributes start with "_" can be accessed directly:
michael._nop
The result is:
2
The attributes start with "__" cannot be accessed directly:
michael.__name
It will generate an error:
---------------------------------------------------------------------------AttributeError Traceback (most recent call last)
<ipython-input-103-4346592ec61a> in <module>()----> 1 michael.__name
AttributeError: 'Student' object has no attribute '__name'
In fact, we can still access the __name attribute of the instance through some tricks:
michael._Student__name
The result is:
'Michael Jordan'
Instance Variable vs. Class Variable
Define the following class:
class Employee:
num_of_emps = 0
raise_amt = 1.04
def __init__(self, first, last, pay):
self.__first = first
self.__last = last
self.__email = first + "." + last + "@gmail.com"
self.__pay = pay
Employee.num_of_emps += 1
def fullname(self):
return f"{self.__first} {self.__last}"
def apply_raise(self):
self.__pay = int(self.__pay * self.raise_amt)
Then create 2 employees:
emp1 = Employee("Kobe", "Bryant", 50000)
emp2 = Employee("Lebron", "James", 60000)
Check the total number of employees through the class attribute:
Employee.num_of_emps
The result is:
2
The class attributes can also be accessed through the instances:
emp1.raise_amt
The result is:
1.04
Use the __dict__ method to check the instance attributes:
emp1.__dict__
The results are:
{'_Employee__first': 'Kobe',
'_Employee__last': 'Bryant',
'_Employee__email': 'Kobe.Bryant@gmail.com',
'_Employee__pay': 50000}
Now let's add an instance attribute also called raise_amt to emp1:
emp1.raise_amt = 1.05
Check the instance attributes of emp1 again:
emp1.__dict__
The results are:
{'_Employee__first': 'Kobe',
'_Employee__last': 'Bryant',
'_Employee__email': 'Kobe.Bryant@gmail.com',
'_Employee__pay': 50000,
'raise_amt': 1.05}
Let's call the apply_raise method from the emp2 instance:
emp2.apply_raise()
emp2.__dict__
The results are:
{'_Employee__first': 'Lebron',
'_Employee__last': 'James',
'_Employee__email': 'Lebron.James@gmail.com',
'_Employee__pay': 62400}
It can be seen that the emp2 instance used the raise_amt attribute from the class to calculate the raise. Let's try the same for the emp1 instance:
emp1.apply_raise()
emp1.__dict__
The results are:
{'_Employee__first': 'Kobe',
'_Employee__last': 'Bryant',
'_Employee__email': 'Kobe.Bryant@gmail.com',
'_Employee__pay': 52500,
'raise_amt': 1.05}
It can be seen that the emp1 instance used the raise_amt attribute from the instance itself to calculate the raise. This proved that the instance attribute overwrote the class attribute.
Regular Method, Class Method and Static Method
Let's improve the Employee Class from the last section as follows:
class Employee:
num_of_emps = 0
raise_amt = 1.04
def __init__(self, first, last, pay):
self.__first = first
self.__last = last
self.__email = first + "." + last + "@gmail.com"
self.__pay = pay
Employee.num_of_emps += 1
def fullname(self):
return f"{self.__first} {self.__last}"
def apply_raise(self):
self.__pay = int(self.__pay * self.raise_amt)
@classmethod
def set_raise_amt(cls, amt):
cls.raise_amt = amt
@classmethod
def from_str(cls, emp_str):
first, last, pay = emp_str.split("-")
return cls(first, last, pay)
@staticmethod
def is_workday(day):
if day.weekday() == 5 or day.weekday() == 6:
return False
else:
return True
Use the class method to set the raise amount:
Employee.set_raise_amt(1.05)
Employee.raise_amt
The result is:
1.05
Create 2 employee instances:
emp1 = Employee("Kobe", "Bryant", 50000)
emp2 = Employee("Lebron", "James", 60000)
Check the raise amount:
print(Employee.raise_amt)
print(emp1.raise_amt)
print(emp2.raise_amt)
The results are:
1.05
1.05
1.05
Create an employee string:
emp_str = 'John-Doe-70000'
The class method from_str receives an employee string and returns an new employee instance:
new_emp1 = Employee.from_str(emp_str_1)
Finally, let's give the static method a try:
import datetime
my_date = datetime.date(2018, 1, 5)
Employee.is_workday(my_date)
The result is:
True
The static method can also be accessed through an instance of the class:
emp1.is_workday(my_date)
The result is:
True
Data Packing
Define the following class:
class Student:
def __init__(self, name="Zeyu Yan", score=100):
self.__name = name
self.__score = score
@property
def name(self):
return self.__name
@property
def score(self):
return self.__score
@name.setter
def name(self, value):
self.__name = value
@score.setter
def score(self, value):
if value > 0 and value <= 100:
self.__score = value
else:
raise ValueError("Bad score!")
The @property and the @attribute.setter decorators define the getters and setters of the class instances, respectively. Create an instance of the class:
zeyu = Student()
The attributes of the instance can be accessed through the getters:
print(zeyu.name)
print(zeyu.score)
The results are:
'Zeyu Yan'
100
The instance's attributes can also be modified using the setters:
zeyu.name = "Michael Jordan"
zeyu.name
The result is:
'Michael Jordan'
An advantage of using setters is that improper values can be handled:
zeyu.score = 200
An exception will be raised in the case:
---------------------------------------------------------------------------ValueError Traceback (most recent call last)
<ipython-input-42-0611ef19e7de> in <module>()----> 1 zeyu.score = 200 2 zeyu.score
<ipython-input-38-354884bdc5a5> in score(self, value) 21 self.__score = value
22 else:---> 23 raise ValueError("Bad score!")ValueError: Bad score!
Inheritence
Define the parent class as follows:
class Person:
def __init__(self, name, sex):
self.name = name
self.sex = sex
def print_title(self):
if self.sex == "male":
print("man")
if self.sex == "female":
print("woman")
Define a child class which inherits from the Person class:
class Child(Person):
def __init__(self, name, sex, mother, father):
Person.__init__(self, name, sex)
self.mother = mother
self.father = father
def print_title(self):
if self.sex == "male":
print("boy")
if self.sex == "female":
print("girl")
Create a child instance:
may = Child("May", "female", "April", "June")
print(may.name, may.sex, may.mother, may.father)
The results are:
May female April June
Test the print_title method on the child instance:
may.print_title()
The result is:
girl
The isinstance method can be used to check if one class inherits from the other:
print(isinstance(may, Child))
print(isinstance(may, Person))
The results are:
True
True
Mixin
Mixin is a way to "mixin" specific functions to the class. Take a look at the following example:
class Vehicle:
pass
class PlaneMixin:
def fly(self):
print("I am flying!")
class Airplane(Vehicle, PlaneMixin):
pass
Create an airplane instance and test if the fly method works on it.
airplane = Airplane()
airplane.fly()
The result is:
I am flying!
Attr Related Methods
Define the following class:
class MyObject:
def __init__(self):
self.x = 9
def power(self):
return self.x * self.x
Do the following tests:
obj = MyObject()
print(obj.x)
print(obj.power())
The results are:
9
81
Check if the object has a specific attribute:
print(hasattr(obj, "x"))
print(hasattr(obj, "y"))
The results are:
True
False
We can also set the attributes of an object:
setattr(obj, "y", 19)
print(hasattr(obj, "y"))
The result is:
True
We can also retrieve the attributes of an object:
getattr(obj, "y")
The result is:
19
If the attribute doesn't exist, an error will be raised:
getattr(obj, "z")
The result is:
---------------------------------------------------------------------------AttributeError Traceback (most recent call last)
<ipython-input-82-6912618b7915> in <module>()----> 1 getattr(obj, "z")AttributeError: 'MyObject' object has no attribute 'z'
Instead of raising an error, we can set a default value to return when the attribute doesn't exist:
getattr(obj, "z", 404)
The result is:
404
When the attribute is a function, we can use it outside the class like this:
fn = getattr(obj, "power")
fn()
The result is:
81
__repr__ and __str__ methods
The repr and str methods can be used to change how a class instance looks like when displayed in a Jupyter Notebook or printed, let's take a look at an example. Define the following class:
class Test:
def __init__(self, value="Hello, world!"):
self.data = value
Create an instance of the class:
t = Test()
If we display the instance in a Jupyter Notebook:
t
The result is:
<__main__.Test at 0x109ffd320>
If we print it:
print(t)
The result is:
<__main__.Test object at 0x109ffd320>
Now let's take a look at the __repr__ method first. Define a class:
class TestRepr(Test):
def __repr__(self):
return f"TestRepr({self.data})"
Create an instance:
tr = TestRepr()
Display the new instance in a Jupyter Notebook:
tr
The result is:
TestRepr(Hello, world!)
Print the new instance:
print(tr)
The result is:
TestRepr(Hello, world!)
The __str__ method will only change the way the instance looks when printed. Define the following new class:
class TestStr(Test):
def __str__(self):
return f"[Value: {self.data}]"
Create an instance:
ts = TestStr()
Display the new instance in a Jupyter Notebook:
ts
The result is:
<__main__.TestStr at 0x109ffd5c0>
Print the new instance:
print(ts)
The result is:
[Value: Hello, world!]
Conclusions
In this article, we have an in-depth tutorial on how to use Python's class. More interesting topics will be covered in future episodes. Stay tuned!
Get in Touch
Thank you for reading! Please let us know if you like this series or if you have critiques. If this series was helpful to you, please follow us and share this series to your friends.
If you or your company needs any help on projects related to drilling automation and optimization, AI, and data science, please get in touch with us Nvicta AI. We are here to help. Cheers!
Comentarios