<jupyter_start><jupyter_text>Heavily commented OOP alochona<jupyter_code>x=1
type(x)<jupyter_output><empty_output><jupyter_text>What is an object bruh? We see objects all the time.<jupyter_code>print(type(x))<jupyter_output><class 'int'><jupyter_text>Woah, whats with that "class" infront of the int?? I was only expecting just an 'int'. This is actually an object now. We have created an object named x, which is an integer type object , and the value of the object is 1. Let's try the same thing with a function<jupyter_code>def hello():
  print("Hello")
print(type(hello))<jupyter_output><class 'function'><jupyter_text>bruh, this is also an object. This is a function object. Whatever you create something in python, it will be an object. Type of an object gives an idea of the restrictions of different possible operations on an object. let's explore METHODS, METHODS are something you can perform on an object<jupyter_code>string="hello"
print(string.upper())<jupyter_output>HELLO<jupyter_text>bruh, here the .upper() is a method. The method .upper() is acting on a stringtype object. So, how is a method different from a function?Welp, METHOD is actually a FUNCTION that is defined inside a CLASS **Creating Class**<jupyter_code>class dog:
  '''
  here, bark and sleep are two methods, defined for the class 'dog'
  All of our methods will start with the parameter 'self'. What does this parameter mean and how does this parameter work - you might be asking.. welp, I don't know yet

  '''
  def bark(self):
    print("bark")
  def sleep(self):
    print("zzzzz")

d = dog()
print(d)
'''
assigning a variable d , an instance of class dog.
I like to think of the 'class dog:' as the cookie cutter and 'dog()' as the cookie made from the cutter

'''<jupyter_output><__main__.dog object at 0x7f08e28a71d0><jupyter_text>Now, what tf does these __main__ mean??? bruh. These main is called a module. The default module is 'main'. Idk what modules is, yet<jupyter_code>d.bark()
d.sleep()<jupyter_output>zzzzz<jupyter_text>Conventionally , we use capital letters to start the name of a class.<jupyter_code># We can make methods and pass parameters to them too. 
class one_up:
  def add_one(self,given_variable):
    return given_variable+ 1
just_a_variable = one_up()
just_a_variable.add_one(4)<jupyter_output><empty_output><jupyter_text>**So, what is the init method???**Init instantiates the class whenever it is called.<jupyter_code>class dog:
  def __init__(self,name,no_name=2):
    self.name = name # we created attribute of class Dog, which denotes a name
    self.no_name = no_name
    print(no_name)
    
  def bark(self):
    print("bark")
  def sleep(self):
    print("zzzzz")

d = dog("you can write anything, string, number, array here","this one goes to no_name")<jupyter_output>this one goes to no_name<jupyter_text>Woah, the name gets printed right away. The attribute gets executed immediately, without explicit mention. These attributes can be assigned values in the init section, but you can again assign stuffs to it, no worries. So, is attribute like a variable in a class? What makes a attribute different from a variable?  We use objects when we know we have to re-use a certain variable in future, a lot. This avoids the problem of creating multiple multiple variables.   **Making a more complex object**Why would we use multiple classes?<jupyter_code>class Student:
  def __init__ (self,name,age,grade):
    # defining three attributes for each student
    self.name = name
    self.age = age
    self.grade = grade

  def get_grade(self):
    return self.grade

class Course:
    def __init__ (self,name,max_student):
      self.name = name
      self.max_student = max_student
      self.stud = [] # creating a blank attribute 
      self.is_active = False

    def add_student(self,stud):
      if len(self.stud) < self.max_student :
        self.stud.append(stud) #instant of a student pbject
        #print(stud)
        return True
        
      else:
        return False
    # write a method that can calculate the average grade of the students in the course.
    def get_avg_grade(self):
      value = 0
      for studento in self.stud:
        value += stud.get_grade()
        return value / len(self.stud)


s1 = Student("Time",19,95)
s2 = Student("Bill",19,75)
s3 = Student("Jill",18,54)

courzez = Course("Sci",2)
courzez.add_student(s1)
courzez.add_student(s2)

print(courzez.stud[0].name)<jupyter_output>Time<jupyter_text>The Idea  Behind Inheritence When you have two very simillar classes.<jupyter_code>class Cat:
  def __init__(self,name,age):
    self.name = name
    self.age = age
  def speak(self):
    print("meow")

class Dog:
  def __init__(self,name,age):
    self.name = name
    self.age = age
  def speak(self):
    print("grrr")

# these two classes are too simillar. Just one line of code is different. Here, we can use inheritence, and use one class instead. 
c = Cat("bongkim",12)
d = Dog("Toggi",10)

c.speak()
d.speak()
# here, we will use inheritance, and reudce the need to write the init portion multiple times. 
class Pet:          # a generalized class
  def __init__(self,name='anon',age='0'):
    self.name = name
    self.age = age
  def show(self):
    print(f"I am {self.name} and I am {self.age} years old")  

class Cat(Pet):       # here we inherit the upper level class
  def speak(self):
    print("meow")
class Dog(Pet):
  def speak(self):
    print("grrr")
c = Cat("bongkim",12)
d = Dog()

c.speak()
d.speak()<jupyter_output>meow
grrr<jupyter_text>So, we can see that the attributes defined in Pet is getting used in Cat. This is called inheritance. Now, let's try using a method from the inherited class.<jupyter_code>d.show()<jupyter_output>I am anon and I am 0 years old<jupyter_text>So, we didn;t have the show() method defined for Cat, but still we can use the function. This shows that the method got inherited to Cat. The class we inherited from is the Mother class, and the class we inherited to , is the child class. (obviously named similar to how normal inheritance works :3)The method in the mother class can be over-written in the child class.   Static In-Class Methods and AttributesClass attribrutes ki? Class Methods ki? Eshob jante eii cell gulo dekhun<jupyter_code>'''
We had self everywhere in previous classes. 
What is a class attribute btw? These will remain same for any instance. 
'''

class IEEE():
  is_lame = True # No matter what branch, they lame

  def __init__ (self,name):
    self.name = name

jodu = IEEE("BUET")
kodu = IEEE("MIT")  
jodu.is_lame
kodu.is_lame
# Now, what if some miraculous thing takes place and IEEE becomes great again. Let's change the class
# The output changes for all the instances. You don;t have to change them separately anymore. 
IEEE.is_lame = False
print(jodu.is_lame)
print(kodu.is_lame)
'''
Class methods are a bit different from the regular methods. 
We use decorator to denote a class method. 
They are not referencing self, rather they are referencing class.
'''<jupyter_output><empty_output>
{'fraction_non_alphanumeric': 0.0900824097755044, 'fraction_numerical': 0.006109690252912759, 'mean_word_length': 4.187177597641857, 'pattern_counts': {'":': 0, '<': 44, '<?xml version=': 0, '>': 43, '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': '12', 'dir': 'jupyter-structured-clean-dedup', 'id': '520139', 'license': 'Apache-2.0', 'n_tokens_mistral': 2269, 'n_tokens_neox': 2194, 'n_words': 876, 'path': 'OOP_SHIKHI.ipynb', 'repo_name': 'L12161/MEASUREMENT_AND_INSTRUMENTATIO', 'size': '7038'}