-> class in python:

    for ex:

        class PlayerCharacter:
            #class object attributes
            membership = True
            def __init__(self, name, age):
                if( PlayerCharacter.membership):
                    self.name = name #attributes
                    self.age = age
            
            def run(self):
                print('run')
                return 'done'

        
        player1 = PlayerCharacter('Cindy', 44)
        player2 = PlayerCharacter('Tom', 21)
        

        print(player1.name)
        print(player2.name)


        __init__: It's a dunder function.
                    It's like constructor of class.
                    It is defined for every class.
    
    help(<object-name>): gives entire blueprint of the object
    help(<class-name>): gives entire blueprint of the object

    class object attributes: its like a static data member of class


-> @classmethod and @staticmethod:

    for ex:
        class dummy:
            @classmethod
            def fun1(cls, param1, param2):
                ...do something...

            @staticmethod
            def fun1( param1, param2):
                ...do something...

        @classmethod and @staticmethod: Just like static member function
                                        of class in C++ except in @classmethod
                                        we use class attributes using 'cls'
                                        keyword which acts like a this pointer
                                        but in @staticmethod we can't do that.

-> protected variable and function:

    python doesn't provide any means to limit the access of data members
    of class. The only thing smart is to use convention.

    Any variable with underscore means it's a protected variable and
    don't modify it.

    Same goes for function in a class (methods).

    For ex:
        _name is protected variable in a class.

        _getName() is protected function in a class.


-> Inheritance:

    For ex:

        class User():
            def sign_in(self):
                print('logged in')

        class Wizard(User):
            def __init__(self, name, power):
                self.name = name 
                self.power = power

            def attack(self):
                print(f'attacking with power of {self.power}')

        class Archer(User):
            pass

        wizard1 = Wizard()
        print(wizard1.sign_in())

-> isinstance function:

    isinstance is a function to check if the instance belongs 
    to a given class.

    for ex:
        isinstance(<instance>, <class>)

-> Object class in python:

    Every class inherits from Object class.

-> Polymorphism:

    The child class have function with same name doing different
    functionality and the base class can also have same function
    name but the child class could overide those function to do
    its own functional logic

-> dir():

    It specify what are the functions and variable the instance of
    a class(object) has access to.

-> Dunder methods:

    Dunder methods allow us to modify and implement built-in function
    of objects. This allow us to implement our own custom logic for
    built-in function which work for objects. The modification only
    works for that same object only and if we apply it on different
    object then it would behave as usual(or as implemented for other
    object)


    for ex:

        class Toy():
            def __init__(self, color, age):
                self.color = color
                self.age = age
                self.my_dict = {
                    'name' : 'Yoyo',
                    'has_pets' : False
                }

            def __str__(self):
                return f'{self.color}'

            def __len__(self):
                return 5

            def __call__(self):
                return ('yes??')

            def __getitem__(self, i):
                return self.my_dict[i]


        action_figure = Toy('red',0)
        print(action_figure.__str__())
        print(str(action_figure))
        print(len(action_figure))
        print(action_figure())
        print(action_figure['name'])

-> MRO (Method Resolution order):

    MRO allow us to determine the order in which 
    the Inheritance need to be processed. This 
    order is determined using Depth first search. 


-> private variable and function:

    Any variable with double underscore means it's a protected variable and
    don't modify it.

    Same goes for function in a class (methods).

    For ex:
        __name is protected variable in a class.

        __modifyName() is protected function in a class.
