<SYSTEM_TASK:>
Given the following text description, write Python code to implement the functionality described below step by step
<END_TASK>
<USER_TASK:>
Description:
 Youtube Videos
Step1:  Class Methods
Step2:  Class Methods can be used to create alternate constructors
Step3:  Static Methods
Instance methods take self as the first argument
Class methods take cls as the first argument
Static methods don't take instance or class as their argument, we just pass the arguments we want to work with.
Static methods don't operate on instance or class.
Step4:  Inheritance - Creating subclasses
Step5:  Now what if you want Developer's raise_amount to be 10%?
Step6:  Now what if we want the Developer class to have an extra attribute like prog_lang?
Step7:  Gotcha - Mutable default arguments
* https
Step8:  Magic or Dunder Methods
https
Step9:  __add__
__len__
Step10:  Property Decorators
Step11:  Abstract Base Classes in Python
What are Abstract Base Classes good for? A while ago I had a discussion about which pattern to use for implementing a maintainable class hierarchy in Python. More specifically, the goal was to define a simple class hierarchy for a service backend in the most programmer-friendly and maintainable way.
There was a BaseService that defines a common interface and several concrete implementations that do different things but all provide the same interface (MockService, RealService, and so on). To make this relationship explicit the concrete implementations all subclass BaseService.
To be as maintainable and programmer-friendly as possible the idea was to make sure that
<ASSISTANT_TASK:>
Python Code:
class Employee:
    emp_count = 0 # Class Variable
    company = 'Google' # Class Variable 
    def __init__(self, fname, lname):
        self.fname = fname
        self.lname = lname
        self.email = self.fname + '.' + self.lname + '@' + self.company + '.com'
        Employee.emp_count += 1
        
    def get_fullname(self):
        return '{} {}'.format(self.fname, self.lname)
    
    def get_company(self):
        return 'Company Name is: {}'.format(Employee.company)
emp1 = Employee('Sri', 'Paladugu')
emp2 = Employee('Dhruv', 'Paladugu')
print( emp1.get_fullname() )
print( Employee.emp_count  )
# Trobule ensues when you treat class variables as instance attribute. 
# What the interpreter does in this case is, it creates an instance attribute with the same name and assigns to it.
# The class variable still remains intact with old value.
emp1.company = 'Verily'
print(emp1.company)
print(emp1.get_company())
print(emp2.company)
print(emp2.email)
Explanation: Youtube Videos:
    * https://www.youtube.com/watch?v=rq8cL2XMM5M&index=3&list=PL-osiE80TeTsqhIuOqKhwlXsIBIdSeYtc
    * https://www.youtube.com/watch?v=RSl87lqOXDE&index=4&list=PL-osiE80TeTsqhIuOqKhwlXsIBIdSeYtc
Online References:
    * https://jeffknupp.com/blog/2017/03/27/improve-your-python-python-classes-and-object-oriented-programming/
    * https://dbader.org/blog/abstract-base-classes-in-python
End of explanation
class Employee:
    emp_count = 0 # Class Variable
    company = 'Google' # Class Variable
    raise_amount = 1.04
    def __init__(self, fname, lname):
        self.fname = fname
        self.lname = lname
        self.email = self.fname + '.' + self.lname + '@' + self.company + '.com'
        Employee.emp_count += 1
        
    def get_fullname(self):
        return '{} {}'.format(self.fname, self.lname)
    
    def get_company(self):
        return 'Company Name is: {}'.format(Employee.company)
    
    @classmethod
    def set_raise_amt(cls, amount):
        cls.raise_amount = amount
emp1 = Employee('Sri', 'Paladugu')
emp2 = Employee('Dhruv', 'Paladugu')
Employee.set_raise_amt(1.05)
print(Employee.raise_amount)
print(emp1.raise_amount)
print(emp2.raise_amount)
Explanation: Class Methods
End of explanation
class Employee:
    emp_count = 0 # Class Variable
    company = 'Google' # Class Variable
    raise_amount = 1.04
    def __init__(self, fname, lname, salary):
        self.fname = fname
        self.lname = lname
        self.salary = salary
        self.email = self.fname + '.' + self.lname + '@' + self.company + '.com'
        Employee.emp_count += 1
        
    def get_fullname(self):
        return '{} {}'.format(self.fname, self.lname)
    
    def get_company(self):
        return 'Company Name is: {}'.format(Employee.company)
    
    @classmethod
    def set_raise_amt(cls, amount):
        cls.raise_amount = amount
    
    @classmethod
    def from_string(cls, emp_str):
        fname, lname, salary = emp_str.split("-")
        return cls(fname, lname, salary)
new_emp = Employee.from_string("Pradeep-Koganti-10000")
print(new_emp.email)
Explanation: Class Methods can be used to create alternate constructors
End of explanation
class Employee:
    emp_count = 0 # Class Variable
    company = 'Google' # Class Variable
    raise_amount = 1.04
    def __init__(self, fname, lname, salary):
        self.fname = fname
        self.lname = lname
        self.salary = salary
        self.email = self.fname + '.' + self.lname + '@' + self.company + '.com'
        Employee.emp_count += 1
        
    def get_fullname(self):
        return '{} {}'.format(self.fname, self.lname)
    
    def get_company(self):
        return 'Company Name is: {}'.format(Employee.company)
    
    @classmethod
    def set_raise_amt(cls, amount):
        cls.raise_amount = amount
    
    @classmethod
    def from_string(cls, emp_str):
        fname, lname, salary = emp_str.split("-")
        return cls(fname, lname, salary)
    
    @staticmethod
    def is_workday(day):
        if day.weekday() == 5 or day.weekday() == 6:
            return False
        else:
            return True
import datetime
my_date = datetime.date(2016, 7, 10)
print(Employee.is_workday(my_date))
Explanation: Static Methods
Instance methods take self as the first argument
Class methods take cls as the first argument
Static methods don't take instance or class as their argument, we just pass the arguments we want to work with.
Static methods don't operate on instance or class.
End of explanation
class Employee:
    emp_count = 0 # Class Variable
    company = 'Google' # Class Variable
    raise_amount = 1.04
    def __init__(self, fname, lname, salary):
        self.fname = fname
        self.lname = lname
        self.salary = salary
        self.email = self.fname + '.' + self.lname + '@' + self.company + '.com'
        Employee.emp_count += 1
        
    def get_fullname(self):
        return '{} {}'.format(self.fname, self.lname)
    
    def get_company(self):
        return 'Company Name is: {}'.format(Employee.company)
    
    def apply_raise(self):
        self.salary = self.salary * self.raise_amount
class Developer(Employee):
    pass
dev1 = Developer('Sri', 'Paladugu', 1000)
print(dev1.get_fullname())
print(help(Developer)) # This command prints the Method resolution order. 
# Indicating the order in which the interpreter is going to look for methods.
Explanation: Inheritance - Creating subclasses
End of explanation
class Employee:
    emp_count = 0 # Class Variable
    company = 'Google' # Class Variable
    raise_amount = 1.04
    def __init__(self, fname, lname, salary):
        self.fname = fname
        self.lname = lname
        self.salary = salary
        self.email = self.fname + '.' + self.lname + '@' + self.company + '.com'
        Employee.emp_count += 1
        
    def get_fullname(self):
        return '{} {}'.format(self.fname, self.lname)
    
    def get_company(self):
        return 'Company Name is: {}'.format(Employee.company)
    def apply_raise(self):
        self.salary = self.salary * self.raise_amount    
    
class Developer(Employee):
    raise_amount = 1.10
dev1 = Developer('Sri', 'Paladugu', 1000)
dev1.apply_raise()
print(dev1.salary)
Explanation: Now what if you want Developer's raise_amount to be 10%?
End of explanation
class Employee:
    emp_count = 0 # Class Variable
    company = 'Google' # Class Variable
    raise_amount = 1.04
    def __init__(self, fname, lname, salary):
        self.fname = fname
        self.lname = lname
        self.salary = salary
        self.email = self.fname + '.' + self.lname + '@' + self.company + '.com'
        Employee.emp_count += 1
        
    def get_fullname(self):
        return '{} {}'.format(self.fname, self.lname)
    
    def get_company(self):
        return 'Company Name is: {}'.format(Employee.company)
    def apply_raise(self):
        self.salary = self.salary * self.raise_amount    
    
class Developer(Employee):
    raise_amount = 1.10
    
    def __init__(self, fname, lname, salary, prog_lang):
        super().__init__(fname, lname, salary)
        # or you can also use the following syntax
        # Employee.__init__(self, fname, lname, salary)
        self.prog_lang = prog_lang
dev1 = Developer('Sri', 'Paladugu', 1000, 'Python')
print(dev1.get_fullname())
print(dev1.prog_lang)
Explanation: Now what if we want the Developer class to have an extra attribute like prog_lang?
End of explanation
class Employee:
    emp_count = 0 # Class Variable
    company = 'Google' # Class Variable
    raise_amount = 1.04
    def __init__(self, fname, lname, salary):
        self.fname = fname
        self.lname = lname
        self.salary = salary
        self.email = self.fname + '.' + self.lname + '@' + self.company + '.com'
        Employee.emp_count += 1
        
    def get_fullname(self):
        return '{} {}'.format(self.fname, self.lname)
    
    def get_company(self):
        return 'Company Name is: {}'.format(Employee.company)
    def apply_raise(self):
        self.salary = self.salary * self.raise_amount    
    
class Developer(Employee):
    raise_amount = 1.10
    
    def __init__(self, fname, lname, salary, prog_lang):
        super().__init__(fname, lname, salary)
        # or you can also use the following syntax
        # Employee.__init__(self, fname, lname, salary)
        self.prog_lang = prog_lang
class Manager(Employee):
    def __init__(self, fname, lname, salary, employees = None): # Use None as default not empty list []
        super().__init__(fname, lname, salary)
        if employees is None:
            self.employees = []
        else:
            self.employees = employees
    def add_employee(self, emp):
        if emp not in self.employees:
            self.employees.append(emp)
    def remove_employee(self, emp):
        if emp in self.employees:
            self.employees.remove(emp)
    def print_emps(self):
        for emp in self.employees:
            print('--->', emp.get_fullname())
dev_1 = Developer('Sri', 'Paladugu', 1000, 'Python')
dev_2 = Developer('Dhruv', 'Paladugu', 2000, 'Java')
mgr_1 = Manager('Sue', 'Smith', 9000, [dev_1])
print(mgr_1.email)
print(mgr_1.print_emps())
mgr_1.add_employee(dev_2)
print(mgr_1.print_emps())
print('Is dev_1 an instance of Developer: ', isinstance(dev_1, Developer))
print('Is dev_1 an instance of Employee: ', isinstance(dev_1, Employee))
print('Is Developer an Subclass of Developer: ', issubclass(Developer, Developer))
print('Is Developer an Subclass of Employee: ', issubclass(Developer, Employee))
Explanation: Gotcha - Mutable default arguments
* https://pythonconquerstheuniverse.wordpress.com/2012/02/15/mutable-default-arguments/
End of explanation
class Employee:
    company = 'Google'
    def __init__(self, fname, lname, salary):
        self.fname = fname
        self.lname = lname
        self.salary = salary
        self.email = self.fname + '.' + self.lname + '@' + self.company + '.com'
    def __repr__(self): # For other developers
        return "Employee('{}','{}','{}')".format(self.fname, self.lname, self.salary)
    def __str__(self): # For end user
        return '{} - {}'.format(self.get_fullname(), self.email)
    def get_fullname(self):
        return '{} {}'.format(self.fname, self.lname)
emp1 = Employee('Sri', 'Paladugu', 5000)
print(emp1)
print(repr(emp1))
Explanation: Magic or Dunder Methods
https://www.youtube.com/watch?v=3ohzBxoFHAY&index=5&list=PL-osiE80TeTsqhIuOqKhwlXsIBIdSeYtc
Dunder methods:
1. __repr__
2. __str__
End of explanation
# if you do: 1 + 2 internally the interpreter calls the dunder method __add__
print(int.__add__(1,2))
# Similarly # if you do: [2,3] + [4,5] internally the interpreter calls the dunder method __add__
print(list.__add__([2,3],[4,5]))
print('Paladugu'.__len__()) # This is same as len('Paladugu')
class Employee:
    company = 'Google'
    def __init__(self, fname, lname, salary):
        self.fname = fname
        self.lname = lname
        self.salary = salary
        self.email = self.fname + '.' + self.lname + '@' + self.company + '.com'
    def __repr__(self): # For other developers
        return "Employee('{}','{}','{}')".format(self.fname, self.lname, self.salary)
    def __str__(self): # For end user
        return '{} - {}'.format(self.get_fullname(), self.email)
    def get_fullname(self):
        return '{} {}'.format(self.fname, self.lname)
    def __add__(self, other):
        return self.salary + other.salary
    def __len__(self):
        return len(self.get_fullname())
emp1 = Employee('Sri', 'Paladugu', 5000)
emp2 = Employee('Dhruv', 'Paladugu', 5000)
print(emp1 + emp2)
print(len(emp1))
Explanation: __add__
__len__
End of explanation
class Employee:
    company = 'Google'
    def __init__(self, fname, lname, salary):
        self.fname = fname
        self.lname = lname
        self.salary = salary
    @property
    def email(self):
        return '{}.{}@{}.com'.format(self.fname, self.lname, self.company)
    @property
    def fullname(self):
        return '{} {}'.format(self.fname, self.lname)
    
    @fullname.setter
    def fullname(self, name):
        first, last = name.split(' ')
        self.fname = first
        self.lname = last
    
    @fullname.deleter
    def fullname(self):
        print('Delete Name!')
        self.fname = None
        self.lname = None
emp1 = Employee('Sri', 'Paladugu', 5000)
print(emp1.email)
print(emp1.fullname)
emp1.fullname = 'Ramki Paladugu'
print(emp1.email)
del emp1.fullname
print(emp1.email)
Explanation: Property Decorators
End of explanation
from abc import ABCMeta, abstractmethod
class Base(metaclass=ABCMeta):
    @abstractmethod
    def foo(self):
        pass
    @abstractmethod
    def bar(self):
        pass
class Concrete(Base):
    def foo(self):
        pass
    # We forget to declare bar()
c = Concrete()
Explanation: Abstract Base Classes in Python
What are Abstract Base Classes good for? A while ago I had a discussion about which pattern to use for implementing a maintainable class hierarchy in Python. More specifically, the goal was to define a simple class hierarchy for a service backend in the most programmer-friendly and maintainable way.
There was a BaseService that defines a common interface and several concrete implementations that do different things but all provide the same interface (MockService, RealService, and so on). To make this relationship explicit the concrete implementations all subclass BaseService.
To be as maintainable and programmer-friendly as possible the idea was to make sure that:
instantiating the base class is impossible; and
forgetting to implement interface methods in one of the subclasses raises an error as early as possible.
End of explanation
<END_TASK>