<jupyter_start><jupyter_text>FunctionFunctions are ﬁrst-class objects in python. They can be assigned to variables, stored in data structures, passed as arguments to other functions, and even returned as values from other functions. Just like everything else in python, functions are objects and arent any special. "def" is used to define a functio<jupyter_code>def add_nos(x,y):
    return x+y

c = add_nos #this just a fn assigned to a variable c. this function isnt called yet
print(c(1,3)) #fn is called

#Python attaches a string identiﬁer to every function at creation time for debugging purposes:
print(c.__name__)

#functions can be stored in a list
funcs = [str.lower, str.capitalize]
print(funcs)
for x in funcs:
    print(x, x('python'))
    
    
funcs[0]('Dinner')

#functions can be used as argument to other functions
def print_out(func):
    print(func(4,5))
    
print_out(c) #this is a higher-order function
#Python allows functions to be deﬁned inside other functions. These are often called nested functions or inner functions .

def get_speak_func(volume):
    def whisper(text):
        pass
    def yell(text):
        pass
    if volume > 0.5:
        return yell
    else:
        return whisper
    
    
print(get_speak_func(0.4))<jupyter_output><function get_speak_func.<locals>.whisper at 0x7f0fed361e60><jupyter_text>Class - is basically a blueprint for creating an instance of a class classes allow us to create a reusable piece of code with its data(attributes) and functions (methods). An instance of a class contains variable that is unique to that instance<jupyter_code>color = 'red'
price = 5000

#every data in python is an obj that has been instiated earlier by some class
print(type(color)) #str type of class 
print(type(price)) #int type of class 
class Car:
    pass

#some instance of the class
car1 = Car()
car2 = Car()

#hese are two instance of the class at different memory location
#print(car1, car2)

#give some attributes to the class 
car1.name = 'toyota'

print(type(car1)) #str type of class 
print(type(car1.name)) 

#assign some methods. each method receives an instance of a class as first argument
class Car:
    def return_double_name(self, name):
        return 2*name #concatenate
car3 = Car()    
car3.name = 'toyota'

car3.return_double_name(car3.name)
class Car:
    #this is a constructor/initialise. wehn we create a method within a class, they receive an instance as a 1st argument
    def __init__(self, color, price, manufacturer='ford'):
        print(f'this is initialise {manufacturer.capitalize()} when this class is called')
        self.color = color
        self.price = price
        self.manufacturer = manufacturer

    def full_detail(self):
        return f'This is a {self.color} {self.manufacturer} car sold at ${self.price}'
    
    
car1 = Car('red', 50000, 'toyota')
car2 = Car('blue',60000, 'volvo')

print(car2.full_detail())

#behind the hood this is happening, the method takes an instance as first argument
print(Car.full_detail(car2))<jupyter_output>this is initialise Toyota when this class is called
this is initialise Volvo when this class is called
This is a blue volvo car sold at $60000
This is a blue volvo car sold at $60000<jupyter_text>Dunder Method<jupyter_code>class Car:
    #this is a constructor/initialise. wehn we create a method within a class, they receive an instance as a 1st argument
    def __init__(self, color, price, manufacturer='ford'):
        #print(f'this is initialise {manufacturer.capitalize()} when this class is called')
        self.color = color
        self.price = price
        self.manufacturer = manufacturer

    def full_detail(self):
        return f'This is a {self.color} {self.manufacturer} car sold at ${self.price}'
    
    #change how your objects are printed
    def __repr__(self):
        #for developers
        return f'Car("{self.color}","{self.price}", "{self.manufacturer}")'
    
    def __str__(self):
        #for endusers
        return f'{self.color} car priced at {self.price} and produced by {self.manufacturer}'
    
    def __add__(self, other):
        return self.price + other.price
    def __sub__(self, other):
        return self.price - other.price
    def __mul__(self, other):
        return self.price * other.price
    def __len__(self):
        return len(self.color)
    
car1 = Car('red', 50000, 'toyota')
car2 = Car('blue', 10000, 'ford')
print(car1)
print(car1.__str__())
print(car1.__repr__())
print(car1 * car2)
print(len(car1))
print(len('delali'))
print('delali'.__len__())
class Car:
    #class attributes belongs to the class itself
    discount = 0.7 #30% discount
    count  = 0
    #this is a constructor/initialise. wehn we create a method within a class, they receive an instance as a 1st argument
    def __init__(self, color: str, price: float, manufacturer: str): #set argument type
        #run a validation of received arg
        
        assert price>=0, f'{price} must be a positive number'
        
        #assign self object, instance attributes
        self.color = color
        self.price = price
        self.manufacturer = manufacturer
        Car.count+=1
        
    def full_detail(self):
        return f'This is a {self.color} {self.manufacturer} car sold at ${self.price}'
    
    def cal_new_price(self):
        # self.new_increase allows us to override the class var for each instance
        self.price = self.price * self.discount
        
    #change how your objects are printed
    def __repr__(self):
        #for developers
        return f'Car("{self.color}","{self.price}", "{self.manufacturer}")'
    
    def __str__(self):
        #for endusers
        return f'{self.color} car priced at {self.price} and produced by {self.manufacturer}'
    
    def __add__(self, other):
        return self.price + other.price
    def __sub__(self, other):
        return self.price - other.price
    def __mul__(self, other):
        return self.price * other.price
    def __len__(self):
        return len(self.color)
    
print(Car.count)
car1 = Car('red', 50000, 'toyota')
print(Car.count)

print(Car.discount)
print(car1.discount)

class Car:
    #class attributes belongs to the class itself
    discount = 0.7 #30% discount
    count  = 0
    #this is a constructor/initialise. wehn we create a method within a class, they receive an instance as a 1st argument
    def __init__(self, color: str, price: float, manufacturer: str): #set argument type
        #run a validation of received arg
        
        assert price>=0, f'{price} must be a positive number'
        
        #assign self object, instance attributes
        self.color = color
        self.price = price
        self.manufacturer = manufacturer
        Car.count+=1
        
    def full_detail(self):
        return f'This is a {self.color} {self.manufacturer} car sold at ${self.price}'
    
    def cal_new_price(self):
        # self.new_increase allows us to override the class var for each instance
        self.price = self.price * self.discount
        
    #change how your objects are printed
    def __repr__(self):
        #for developers
        return f'Car("{self.color}","{self.price}", "{self.manufacturer}")'
    
    def __str__(self):
        #for endusers
        return f'{self.color} car priced at {self.price} and produced by {self.manufacturer}'
    
    def __add__(self, other):
        return self.price + other.price
    def __sub__(self, other):
        return self.price - other.price
    def __mul__(self, other):
        return self.price * other.price
    def __len__(self):
        return len(self.color)
        
car1 = Car('red', 50000, 'toyota')
print(car1.price)
car1.cal_new_price()
print(car1.price)

#find variable in the name space of both the class and its methods
#print(car1.__dict__) #attributes of the instance
#print(Car.__dict__) #attributes of the class

#override the class var for car1 and includes this varible in its name space
car2 = Car('red', 50000, 'toyota')
car2.discount = .5
car2.cal_new_price()
print(car2.price)

#print(car1.__dict__)
#print(Car.__dict__)
class Car:
    #class attributes belongs to the class itself
    discount = 0.7 #30% discount
    count  = 0
    alls = []
    #this is a constructor/initialise. wehn we create a method within a class, they receive an instance as a 1st argument
    def __init__(self, color: str, price: float, manufacturer: str): #set argument type
        #run a validation of received arg
        
        assert price>=0, f'{price} must be a positive number'
        
        #assign self object, instance attributes
        self.color = color
        self.price = price
        self.manufacturer = manufacturer
        Car.count+=1
        
        Car.alls.append(self)
        
    def full_detail(self):
        return f'This is a {self.color} {self.manufacturer} car sold at ${self.price}'
    
    def cal_new_price(self):
        # self.new_increase allows us to override the class var for each instance
        self.price = self.price * self.discount
    
    
    #change how your objects are printed
    def __repr__(self):
        #for developers
        return f'Car("{self.color}","{self.price}", "{self.manufacturer}")'
    
    def __str__(self):
        #for endusers
        return f'{self.color} car priced at {self.price} and produced by {self.manufacturer}'
    
    def __add__(self, other):
        return self.price + other.price
    def __sub__(self, other):
        return self.price - other.price
    def __mul__(self, other):
        return self.price * other.price
    def __len__(self):
        return len(self.color)
    
car1 = Car('red', 50000, 'toyota')
car2 = Car('blue', 150000, 'ford')
car1.color = 'green'
print(Car.alls)

for obj in Car.alls:
    print(obj.price)
#print(car1.__dict__)
#print(Car.__dict__)<jupyter_output>[Car("green","50000", "toyota"), Car("blue","150000", "ford")]
50000
150000<jupyter_text>Classmethod and Staticmethod<jupyter_code>class Car:
    #class attributes belongs to the class itself
    discount = 0.7 #30% discount
    count  = 0
    alls = []
    #this is a constructor/initialise. wehn we create a method within a class, they receive an instance as a 1st argument
    def __init__(self, color: str, price: float, manufacturer: str): #set argument type
        #run a validation of received arg
        
        assert price>=0, f'{price} must be a positive number'
        
        #assign self object, instance attributes
        self.color = color
        self.price = price
        self.manufacturer = manufacturer
        Car.count+=1
        
        Car.alls.append(self)
    #regular methods take an instance as first arg    
    def full_detail(self):
        return f'This is a {self.color} {self.manufacturer} car sold at ${self.price}'
    
    def cal_new_price(self):
        # self.new_increase allows us to override the class var for each instance
        self.price = self.price * self.discount
    
    #create an instance of the object itself and so could not be called on an instance. it takes a class as first arg
    #classmethod can be used as constructor
    @classmethod
    def create_from_csv(cls):
        
        df = pd.read_csv('data.csv')

        for (c, p, m) in zip(df.get('color'), df.get('price'), df.get('manufacturer')):
            cls(c,float(p),m)
            #Car(c,float(p),m) #same as above
    #here this change the class variable but not instance variable        
    @classmethod
    def set_discount(cls, amount):
        cls.discount = amount 
        
    @staticmethod        
    def is_integer(num):
        # We will count out the floats that are point zero
        # For i.e: 5.0, 10.0
        if isinstance(num, float):
            # Count out the floats that are point zero
            return num.is_integer()
        elif isinstance(num, int):
            return True
        else:
            return False
        
    @staticmethod
    def is_workday(day):
        if day.weekday() == 5 or day.weekday() == 6:
            return False
        return True   
    #change how your objects are printed
    def __repr__(self):
        #for developers
        return f'Car("{self.color}","{self.price}", "{self.manufacturer}")'
    
    def __str__(self):
        #for endusers
        return f'{self.color} car priced at {self.price} and produced by {self.manufacturer}'
    
    def __add__(self, other):
        return self.price + other.price
    def __sub__(self, other):
        return self.price - other.price
    def __mul__(self, other):
        return self.price * other.price
    def __len__(self):
        return len(self.color)
    
    
Car.create_from_csv()
print(Car.alls, Car.count, Car.is_integer('volvo'))

car1 = Car('red', 50000, 'toyota')
print(Car.discount)
print(car1.discount)

#changes the discount for a single instance
car1.discount = 0.4
print(Car.discount)
print(car1.discount)



Car.set_discount(0.9)
car1 = Car('red', 50000, 'toyota')
print(Car.discount)
print(car1.discount)


import datetime
my_date = datetime.date(2021, 11, 18)

print(Car.is_workday(my_date))<jupyter_output>[Car("red","110.0", "'Toyota'"), Car("blue","15800.0", "'Ford'"), Car("magenta","1000.0", "'Citroen'"), Car("black","3000.0", "'BMW'"), Car("yellow","7500.0", "'Volvo'")] 5 False
0.7
0.7
0.7
0.4
0.9
0.9
True<jupyter_text>Inheritance* inheriting att and methods from a parent class. Allows to create subclass and get all the functionality of our parent class without overriding the parent class<jupyter_code>#Car is a parent class and Expensive is a child class
class Car:
    #class attributes belongs to the class itself
    discount = 0.7 #30% discount
    count  = 0
    alls = []
    #this is a constructor/initialise. wehn we create a method within a class, they receive an instance as a 1st argument
    def __init__(self, color: str, price: float, manufacturer: str): #set argument type
        #run a validation of received arg
        
        assert price>=0, f'{price} must be a positive number'
        
        #assign self object, instance attributes
        self.color = color
        self.price = price
        self.manufacturer = manufacturer
        Car.count+=1
        
        Car.alls.append(self)
    #regular methods take an instance as first arg    
    def full_detail(self):
        return f'This is a {self.color} {self.manufacturer} car sold at ${self.price}'
    
    def cal_new_price(self):
        # self.new_increase allows us to override the class var for each instance
        self.price = self.price * self.discount
    
    #create an instance of the object itself and so could not be called on an instance. it takes a class as first arg
    #classmethod can be used as constructor
    @classmethod
    def create_from_csv(cls):
        
        df = pd.read_csv('data.csv')

        for (c, p, m) in zip(df.get('color'), df.get('price'), df.get('manufacturer')):
            cls(c,float(p),m)
            #Car(c,float(p),m) #same as above
    #here this change the class variable but not instance variable        
    @classmethod
    def set_discount(cls, amount):
        cls.discount = amount 
        
    @staticmethod        
    def is_integer(num):
        # We will count out the floats that are point zero
        # For i.e: 5.0, 10.0
        if isinstance(num, float):
            # Count out the floats that are point zero
            return num.is_integer()
        elif isinstance(num, int):
            return True
        else:
            return False
        
    @staticmethod
    def is_workday(day):
        if day.weekday() == 5 or day.weekday() == 6:
            return False
        return True   
    
    def __repr__(self):
        return f'{self.__class__.__name__}("{self.color}","{self.price}", "{self.manufacturer}")'

class Damaged(Car):
    pass

car1 = Damaged('red', 50000, 'toyota')

#print(help(car1))
class Damaged(Car):
    
    def __init__(self, color: str, price: float, manufacturer: str, damaged_cars=0): #set argument type
        
        #access to all attr and methods from the parent class
        super().__init__(color, price, manufacturer)
        
        #run a validation of received arg
        assert damaged_cars>=0
        
        #assign self object, instance attributes
        self.damaged_cars = damaged_cars

    def depreciate_car(self):
        
        self.price = self.price * self.discount * .95
        
car1 = Damaged('red', 50000, 'toyota', 4)

car2 = Damaged('green', 1000, 'hyundai', 1)

print(car1.damaged_cars, car1.price)

#print(Damaged.alls, Car.alls)

car1.depreciate_car()

print(car1.price)<jupyter_output>4 50000
33250.0<jupyter_text>Getters, setters and deleters<jupyter_code>class Car:
    #class attributes belongs to the class itself
    discount = 0.7 #30% discount
    count  = 0
    alls = []
    #this is a constructor/initialise. wehn we create a method within a class, they receive an instance as a 1st argument
    def __init__(self, color: str, price: float, manufacturer: str): #set argument type
        #run a validation of received arg
        
        assert price>=0, f'{price} must be a positive number'
        
        #assign self object, instance attributes
        self.__color = color #double dunder prevent access to those attr outside the class. makes a clean read only attr
        self.__price = price
        self.manufacturer = manufacturer
        Car.count+=1
        
        Car.alls.append(self)
        
    @property #create a read only attribute of color -> encapsulation restrict direct access to attr
    def price(self):
        return self.__price
    
    @property 
    def color(self):
        return self.__color
    
    @color.setter
    def color(self, value):
        self.__color = value
    
    @color.deleter
    def color(self):
        self.__color = None
        
    #regular methods take an instance as first arg    
    def full_detail(self):
        return f'This is a {self.__color} {self.manufacturer} car sold at ${self.price}'
    
    def cal_new_price(self):
        # self.new_increase allows us to override the class var for each instance
        self.__price = self.__price * self.discount
    
    def apply_xmas_bonus(self, inc):
        # self.new_increase allows us to override the class var for each instance
        self.__price = self.__price * inc
        
    #create an instance of the object itself and so could not be called on an instance. it takes a class as first arg
    #classmethod can be used as constructor
    @classmethod
    def create_from_csv(cls):
        
        df = pd.read_csv('data.csv')

        for (c, p, m) in zip(df.get('color'), df.get('price'), df.get('manufacturer')):
            cls(c,float(p),m)
            #Car(c,float(p),m) #same as above
    #here this change the class variable but not instance variable        
    @classmethod
    def set_discount(cls, amount):
        cls.discount = amount 
        
    @staticmethod        
    def is_integer(num):
        # We will count out the floats that are point zero
        # For i.e: 5.0, 10.0
        if isinstance(num, float):
            # Count out the floats that are point zero
            return num.is_integer()
        elif isinstance(num, int):
            return True
        else:
            return False
        
    @staticmethod
    def is_workday(day):
        if day.weekday() == 5 or day.weekday() == 6:
            return False
        return True  
    
    def __connect(self): #abstraction -> prevents methods from being accessed outside the class by using '__'
        pass
    #change how your objects are printed
    def __repr__(self):
        #for developers
        return f'Car("{self.__color}","{self.__price}", "{self.manufacturer}")'
    
    def __str__(self):
        #for endusers
        return f'{self.color} car priced at {self.__price} and produced by {self.manufacturer}'
    
    def __add__(self, other):
        return self.__price + other.__price
    def __sub__(self, other):
        return self.__price - other.__price
    def __mul__(self, other):
        return self.__price * other.__price
    def __len__(self):
        return len(self.__color)
    
    
    
car1 = Car('red', 50000, 'toyota')
#car1.__color = 'green' #attr cant be access outside the class
#print(car1.__color)
#print(car1._color) still can be access with one underscore
car1.color = 'green'
print(car1.color )

del car1.color

print(car1.color )
print(car1.price)
#print(car1.connect())
#print(car1.__connect())
car1.apply_xmas_bonus(1.2)
print(car1.price)
class Car:
    #class attributes belongs to the class itself
    discount = 0.7 #30% discount
    count  = 0
    alls = []
    #this is a constructor/initialise. wehn we create a method within a class, they receive an instance as a 1st argument
    def __init__(self, color: str, price: float, manufacturer: str): #set argument type
        #run a validation of received arg
        
        assert price>=0, f'{price} must be a positive number'
        
        #assign self object, instance attributes
        self.color = color
        self.price = price
        self.manufacturer = manufacturer
        Car.count+=1
        
        Car.alls.append(self)
        
    @property  #changes the method to a read only attribute 
    def brand_cost(self):
        return f'{self.manufacturer}_{self.price}'
    
    #regular methods take an instance as first arg but has been modified to an attr status 
    @property
    def full_detail(self):
        return f'{self.color}-{self.manufacturer}-{self.price}'
    
    @full_detail.setter #updates the attr info
    def full_detail(self, name):
        a,b,c = name.split(' ')
        self.color = a
        self.price = b
        self.manufacturer = c
    
    @full_detail.deleter #deletes obj attr
    def full_detail(self):
        print('item deleted')
        self.color = None
        self.price = None
        self.manufacturer = None
        
    def cal_new_price(self):
        # self.new_increase allows us to override the class var for each instance
        self.price = self.price * self.discount
    
    #create an instance of the object itself and so could not be called on an instance. it takes a class as first arg
    #classmethod can be used as constructor
    @classmethod
    def create_from_csv(cls):
        
        df = pd.read_csv('data.csv')

        for (c, p, m) in zip(df.get('color'), df.get('price'), df.get('manufacturer')):
            cls(c,float(p),m)
            #Car(c,float(p),m) #same as above
    #here this change the class variable but not instance variable        
    @classmethod
    def set_discount(cls, amount):
        cls.discount = amount 
        
    @staticmethod        
    def is_integer(num):
        # We will count out the floats that are point zero
        # For i.e: 5.0, 10.0
        if isinstance(num, float):
            # Count out the floats that are point zero
            return num.is_integer()
        elif isinstance(num, int):
            return True
        else:
            return False
        
    @staticmethod
    def is_workday(day):
        if day.weekday() == 5 or day.weekday() == 6:
            return False
        return True   
    
    #change how your objects are printed
    def __repr__(self):
        #for developers
        return f'Car("{self.color}","{self.price}", "{self.manufacturer}")'
    
    def __str__(self):
        #for endusers
        return f'{self.color} car priced at {self.price} and produced by {self.manufacturer}'
    
    def __add__(self, other):
        return self.price + other.price
    def __sub__(self, other):
        return self.price - other.price
    def __mul__(self, other):
        return self.price * other.price
    def __len__(self):
        return len(self.color)

    
car1 = Car('red', 50000, 'toyota')
#print(car1.brand_cost())
#car1.brand_cost = 'benz_444' #this is invalid b/cos it can only be read and not modified
print(car1.brand_cost)
print(car1.full_detail)

car1.full_detail = 'green benz 50000'

print(car1.full_detail, car1.color)

del car1.full_detail

print(car1.full_detail)<jupyter_output>toyota_50000
red-toyota-50000
green-50000-benz green
item deleted
None-None-None
{'fraction_non_alphanumeric': 0.09678349326633993, 'fraction_numerical': 0.016103125900910176, 'mean_word_length': 2.6290539530712898, 'pattern_counts': {'":': 0, '<': 21, '<?xml version=': 0, '>': 32, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 0, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '6', 'dir': 'jupyter-structured-clean-dedup', 'id': '274105', 'license': 'MIT', 'n_tokens_mistral': 7509, 'n_tokens_neox': 7211, 'n_words': 2469, 'path': 'OOP.ipynb', 'repo_name': 'gkrampah/Object_Oriented_Programming-', 'size': '24281'}