top of page

Data Science in Drilling - Episode 19

Writer: Zeyu YanZeyu Yan

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


bottom of page