Class, Instance & Functions in Class

class SoftwareEngineer:
	
	# class attributes : these attributes can be accessed using the class object
	# SoftwareEngineer.alias ✅
	alias = "Keyboard Magician"
	
	def __init__(self, name, age, salary):
		# instance attributes : these are tied to only one instance
		# e.g., let's say se1 is an instance of the class SoftwareEngineer
		# these instance attributes can only be accessed through that instance
		# se1.name, se1.age, se1.salary ✅
		# but it is not possible to access through the class object itself 
		# SoftwareEngineer.name, SoftwareEngineer.age, SoftwareEngineer.salary ❌
		self.name = name
		self.age = age
		self.salary = salary
		
	# instance method
    def code(self):
        print(f"{self.name} is writing code...")
    
    def code_in_language(self, language):
        print(f"{self.name} is coding in {language}")
    
    # def information(self):
    #     information = f"Name = {self.name}, Age = {self.age}, Salary = {self.salary}"
    #     return information
    
    # dunder methods 
    # string reprresentation method
    def __str__(self):
        information = f"Name = {self.name}, Age = {self.age}, Salary = {self.salary}"
        return information
    
    # equals method
    def __eq__(self, other):
        return self.name == other.name and self.age == other.age and self.salary == other.salary
    
    # the below method is not tied to a specific instance, and can be accessed directly by the class itself
    @staticmethod
    def entry_salary(age):
        if age < 25:
            return 70000
        elif age < 30:
            return 80000
        return 100000
	    

se1 = SoftwareEngineer("Max", 29, 70000)
se2 = SoftwareEngineer("Max", 29, 70000)

# print(SoftwareEngineer.name) -> not possible to access instance attributes using class name (object)

# print(SoftwareEngineer.alias)
# print(se1.name)
# print(se1.alias)

# print(se1)

# print(se1)
# print(se2)
# print(se1 == se2)

print(se1.entry_salary(25))
print(SoftwareEngineer.entry_salary(25))

	

Inheritance

# Inheritance (inherit, extend, override)
class Employee:
    
    def __init__(self, name, age, salary):
        self.name = name
        self.age = age
        self.salary = salary
    
    def work(self):
        print(f"{self.name} is working...")

class SoftwareEngineer(Employee):
    def __init__(self, name, age, salary, level):
        super().__init__(name, age, salary)
        self.level = level
        
    def work(self):
        print(f"{self.name} is coding...")
    
    def debug(self):
        print(f"{self.name} is debugging...")

class Designer(Employee):
    def __init__(self, name, age, salary):
        super().__init__(name, age, salary)
        
    def work(self):
        print(f"{self.name} is designing...")
    
    def draw(self):
        print(f"{self.name} is drawing...")
    
    

se = SoftwareEngineer("Max", 27, 60000, "Junior")
d = Designer("Alex", 28, 70000)

print(f"{se.name}, {se.age}, {se.level}")
se.work()   
se.debug()

print(f"{d.name}, {d.age}, {d.salary}")
d.work()
d.draw()

Encapsulation

# Encapsulation

class SoftwareEngineer:
    
    def __init__(self, name, age):
        self.name = name
        self.age = age
        # protected instance variables: name starts with a single underscore
        # private instance variables: name starts with a double underscore
        self._salary = None
        self._num_bugs_solved = 0
    
    def code(self):
        self._num_bugs_solved += 1
    
    # getter
    def get_salary(self):
        return self._salary
    
    # setter
    def set_salary(self, base_value):
        # check value, enforce constraints
        self._salary = self._calculate_salary(base_value)
    
    # private function: name starts with a leading underscore
    def _calculate_salary(self, base_value):
        if self._num_bugs_solved < 10:
            return base_value 
        if self._num_bugs_solved < 100:
            return base_value * 2
        return base_value * 3

se = SoftwareEngineer("Max", 27)
print(se.age)

for i in range(70):
    se.code()

se.set_salary(5000)
print(se.get_salary())

-----------------------------------------------------------------------------------

# Pythonic way of wrting getter and setter
class SoftwareEngineer:
    def __init__(self):
        self._salary = None
    
    @property
    def salary(self):
        return self._salary 
    
    @salary.setter
    def salary(self, value):
        self._salary = value
        
    @salary.deleter
    def salary(self):
        del self._salary

se = SoftwareEngineer()

se.salary = 5000
print(se.salary)