<jupyter_start><jupyter_text>Python OOP Tutorial 1 - Classes and Instances [(video link)](https://youtu.be/ZDa-Z5JzLYM) | [(original code)](https://github.com/CoreyMSchafer/code_snippets/tree/master/Object-Oriented/1-FirstClasses) | [(transcript)](https://github.com/faizankshaikh/Notes_PythonOOPTutorial/blob/master/transcripts/classes_and_instances.txt)---  Table of Contents 1.1 What is a class?  1.2 Use Case - Represent employees of a Company 1.3 Difference between Class and an Instance of a class 1.4 Initialize class attributes 1.5 Adding methods to class---  1.1 What is a class?  Allow us to logically group our data and functions in a way that is easy to use and also easy to build upon if needed*Note - terms associated with a specific class** *data => attributes** *functions => methods*  1.2 Use Case - Represent employees of a Company This is a great usecase for class, because each employee would have specific attributes and methods. Each employee would have* Name* Email Address* Pay* and actions they can performClass can be used as a blueprint to create each employee so that we dont have to do this manually each time from scratch.To create a class -<jupyter_code>class Employee:
    pass<jupyter_output><empty_output><jupyter_text>*Note - if you ever have a class or a function that you want to leave empty for the time being then you can simply put pass and python will know that you just want to skip that for now*  1.3 Difference between Class and an Instance of a class Class is a blueprint for creating instances. For example, each unique employee that we create using our  Employee class will be an instance of that class<jupyter_code># create class Employee
class Employee:
    pass


# create emp_1 and emp2 which are instances of class Employee
emp_1 = Employee()
emp_2 = Employee()
print(emp_1)
print(emp_2)<jupyter_output><__main__.Employee object at 0x7fbed42945f8>
<__main__.Employee object at 0x7fbed4294630><jupyter_text>*Note - Both emp_1 and emp_2 are employee objects, and they are both unique and they both have different locations in memory*  1.4 Initialize class attributes Instance variable - contain data that is unique to each instance. One way of doing this is -<jupyter_code># create class Employee
class Employee:
    pass


# create emp_1 and emp2 which are instances of class Employee
emp_1 = Employee()
emp_2 = Employee()

# print(emp_1)
# print(emp_2)

emp_1.first = "Corey"
emp_1.last = "Schafer"
emp_1.email = "<EMAIL>"
emp_1.pay = 50000

emp_2.first = "Test"
emp_2.last = "User"
emp_2.email = "<EMAIL>"
emp_2.pay = 60000

print(emp_1.email)
print(emp_2.email)<jupyter_output><EMAIL>
<EMAIL><jupyter_text>We wouldnt want to manually set these variables everytime, and its also prone to mistakes, i.e. we dont get the benefit of using classes.So to make these setup automatically when we create Emloyee, we can use special "init" method -<jupyter_code>class Employee:
    # In init method, after self, we can specify what
    # other arguments we want to accept
    def __init__(self, first, last, pay):
        self.first = first
        self.last = last
        self.pay = pay
        # can be created using 'first' and 'last' arguments
        self.email = first + "." + last + "@company.com"


# pass in values specified in our init method in correct order
# self gets passed automaticaly
emp_1 = Employee("Corey", "Schafer", 50000)
emp_2 = Employee("Test", "User", 60000)

print(emp_1.email)
print(emp_2.email)<jupyter_output><EMAIL>
<EMAIL><jupyter_text>*Note -** *init method can be thought of as a constructor** *When we create methods within a class, they recieve the instance as the first argument automatically** *By convention,*     * *we should call the instance as self*    * *and make our arguments the same as our instance variables** *Reduces manual assignments and gets rid of a lot of code* When we run emp_1 = Employee("Corey", "Schafer", 50000), init method will run automatically* "emp_1" will be passed as self* "rest of the arguments will be set in their corresponding attributes"  1.5 Adding methods to class We want an ability to display the full name of an employee.It can be done manually like this -<jupyter_code>print("{} {}".format(emp_1.first, emp_1.last))<jupyter_output><NAME><jupyter_text>This is a lot to type for each time you want to display the employee's full nama. Instead, we can create a method within our class that allows us to put this functionality in one place<jupyter_code>class Employee:
    def __init__(self, first, last, pay):
        self.first = first
        self.last = last
        self.pay = pay
        self.email = first + "." + last + "@company.com"

    # each method within a class automatically
    # takes the instance as first argument
    def fullname(self):
        # instead of using specific instance variable,
        # we use self so that it works for all instances
        return "{} {}".format(self.first, self.last)


emp_1 = Employee("Corey", "Schafer", 50000)
emp_2 = Employee("Test", "User", 60000)

# print(emp_1.email)
# print(emp_2.email)

# we need paranthesis here because this is a method
# instead of an attribute
print(emp_1.fullname())<jupyter_output><NAME><jupyter_text>*Note - if we do not use paranthesis when calling a method, it returns the method instead of the return value of the method*<jupyter_code>print(emp_1.fullname)<jupyter_output><bound method Employee.fullname of <__main__.Employee object at 0x7fbed42a9b00>><jupyter_text>To print employee 2's full name -<jupyter_code>print(emp_2.fullname())<jupyter_output>Test User<jupyter_text>*Note -**1. One common mistake when creating methods is forgetting the self argument for the instance*def fullname():    return '{} {}'.format(self.first, self.last)*If the method is not called, and has a missing self argument, it wont give any errors. But if the method is called, it will give "TypeError"*TypeError                                 Traceback (most recent call last) in      13 emp_2 = Employee("Test", "User", 60000)     14 ---> 15 print(emp_1.fullname())TypeError: fullname() takes 0 positional arguments but 1 was given*So we have to expect the instance argument in our method, and that is why we added self* *2. We can also run the methods using the class name itself, but we have to pass in the instance as an argument manually*<jupyter_code>print(Employee.fullname(emp_1))<jupyter_output><NAME>
{'fraction_non_alphanumeric': 0.0838145778055252, 'fraction_numerical': 0.021851100358982363, 'mean_word_length': 4.331114808652246, 'pattern_counts': {'":': 0, '<': 42, '<?xml version=': 0, '>': 45, 'https://': 3, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 1, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '9', 'dir': 'jupyter-structured-clean-dedup', 'id': '367137', 'license': 'MIT', 'n_tokens_mistral': 1968, 'n_tokens_neox': 1856, 'n_words': 805, 'path': 'notebooks/Python OOP Tutorial 1 - Classes and Instances.ipynb', 'repo_name': 'faizankshaikh/Notes_PythonOOPTutorial', 'size': '6524'}