# Classes and Objects

In object-oriented programming (OOP), a class is a blueprint or template for creating objects that defines a set of attributes and methods that the objects will have. An object is an instance of a class, created using the class as a blueprint.

Classes and objects are fundamental concepts in OOP, as they allow for the organization of code into reusable and modular structures. By creating a class, we can encapsulate related data and behavior into a single unit, making our code more manageable and easier to understand. We can create multiple instances of the same class, each with their own set of data and behavior.

In Python, we create classes using the `class` keyword, followed by the name of the class and a colon. The body of the class is indented, and typically contains attributes and methods. To create an object, we use the name of the class followed by parentheses.

Here is an example of a simple class in Python:

```python
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
    
    def greet(self):
        print(f"Hello, my name is {self.name} and I am {self.age} years old.")
```

In this example, we define a `Person` class with two attributes (`name` and `age`) and one method (`greet`). The `__init__` method is a special method that is called when an object is created from the class, and is used to initialize the object's attributes. The `self` parameter refers to the object that is being created.

We can create a `Person` object like this:

```python
person = Person("Alice", 25)
```

This creates a `Person` object with the `name` attribute set to "Alice" and the `age` attribute set to 25. We can call the `greet` method on the object like this:

```python
person.greet()  # Output: "Hello, my name is Alice and I am 25 years old."
```

## Defining a Class&#x20;

To define a class in Python, we use the `class` keyword followed by the name of the class. The first method in a class is called the `__init__` method. This is a special method used to initialize the object's properties.

Here's an example of a simple class:

```python
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
```

In this example, we have defined a class called `Person`. It has two properties, `name` and `age`. The `__init__` method initializes these properties using the arguments passed to it.

The `self` parameter refers to the object itself. It is automatically passed to all instance methods in a class and is used to access the object's properties and methods.

We can create an instance of the `Person` class as follows:

```python
person1 = Person("John", 25)
```

This creates an instance of the `Person` class with the name "John" and age 25. We can access the properties of this object using the dot notation:

```python
print(person1.name) # Output: John
print(person1.age) # Output: 25
```

## Creating Objects from a Class&#x20;

Once a class is defined, objects can be created from the class using the class name followed by parentheses. This will call the class constructor, which creates an instance of the class.

Here's an example:

```python
class Car:
    def __init__(self, make, model, year):
        self.make = make
        self.model = model
        self.year = year

car1 = Car("Toyota", "Camry", 2021)
car2 = Car("Honda", "Accord", 2022)

print(car1.make, car1.model, car1.year)
print(car2.make, car2.model, car2.year)
```

In this example, we define a `Car` class with three attributes: `make`, `model`, and `year`. The `__init__()` method is called when an object is created from the class, and it initializes these attributes with the values passed in as arguments.

We then create two objects, `car1` and `car2`, using the `Car` class. We pass in different arguments for each object, and this sets the attributes for each object accordingly.

Finally, we print out the values of the attributes for each object using dot notation (`car1.make`, `car2.model`, etc.).&#x20;

## Instance Variables&#x20;

Instance variables, also known as member variables or attributes, are variables that are associated with individual objects or instances of a class. Each instance of a class has its own set of instance variables.

Instance variables are defined within a class and are accessed using the `self` keyword. They are typically initialized in the `__init__` method of a class.

Here's an example:

```python
class Car:
    def __init__(self, make, model, year):
        self.make = make
        self.model = model
        self.year = year
```

In this example, `make`, `model`, and `year` are instance variables of the `Car` class. They are initialized using the `self` keyword in the `__init__` method. When an instance of the `Car` class is created, it will have its own `make`, `model`, and `year` attributes.

```python
my_car = Car("Toyota", "Camry", 2022)
print(my_car.make)  # Output: Toyota
print(my_car.model)  # Output: Camry
print(my_car.year)  # Output: 2022
```

In this example, we create an instance of the `Car` class called `my_car`. We pass in the arguments `"Toyota"`, `"Camry"`, and `2022` to initialize the instance variables `make`, `model`, and `year`, respectively. We then print out the values of these instance variables using dot notation.

## Class Variables&#x20;

In Python, class variables are variables that are shared by all instances of a class. They are defined inside the class, but outside any of the class's methods. Class variables can be accessed by all instances of the class and can be modified by any instance.

Here's an example of a class with a class variable:

```python
class Car:
    car_count = 0
    
    def __init__(self, make, model):
        self.make = make
        self.model = model
        Car.car_count += 1
        
car1 = Car("Honda", "Civic")
car2 = Car("Toyota", "Corolla")

print(Car.car_count) # Output: 2
```

In the above example, `car_count` is a class variable that is incremented each time an instance of the `Car` class is created. We can access the class variable using the class name, as shown in the `print` statement.

Note that if we modify the class variable using an instance of the class, it will modify the class variable for all instances of the class:

```python
car1.car_count = 10
print(car1.car_count) # Output: 10
print(car2.car_count) # Output: 2
print(Car.car_count) # Output: 2
```

In the above example, we modified the `car_count` class variable using the `car1` instance, but it only modified it for that instance. The `car_count` variable for `car2` and the `Car` class itself remained unchanged.&#x20;

## Methods&#x20;

Methods in Python are functions that are defined within a class and perform some action on the object created from that class. They are used to define the behavior of an object. There are three types of methods in Python:

1. Instance methods: These methods are defined within a class and take an instance of the class (i.e., an object) as the first argument. They can access instance variables and class variables.
2. Class methods: These methods are defined within a class and take the class itself as the first argument. They can access class variables but not instance variables.
3. Static methods: These methods are also defined within a class, but they do not take the instance or class as the first argument. They are used when we need to perform some operation that is not dependent on the state of the object or class. They can neither access class variables nor instance variables, and can only access the variables passed to them as arguments.

To define a method in Python, we simply define a function within the class. For example, to define an instance method called `my_method` that takes no arguments, we can write:

```ruby
class MyClass:
    def my_method(self):
        # method body goes here
```

To define a class method, we use the `@classmethod` decorator, and to define a static method, we use the `@staticmethod` decorator. For example:

```python
class MyClass:
    my_class_variable = 42

    def my_instance_method(self):
        print("This is an instance method")

    @classmethod
    def my_class_method(cls):
        print("This is a class method")
        print("The value of my_class_variable is:", cls.my_class_variable)

    @staticmethod
    def my_static_method(arg1, arg2):
        print("This is a static method")
        print("The arguments are:", arg1, arg2)
```

Here, `my_instance_method` is an instance method, `my_class_method` is a class method, and `my_static_method` is a static method. Note that the first argument to the class method is `cls` (i.e., the class itself), while the first argument to the static method is not specified.<br>

## The init() Method&#x20;

The `__init__()` method is a special method in Python that is called when an object is created from a class. It is also known as a constructor method because it is used to initialize the attributes of an object.

The `__init__()` method takes `self` as its first parameter, followed by any additional parameters that you want to pass in. The `self` parameter refers to the object that is being created.

Here is an example of a class with an `__init__()` method:

```python
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
```

In this example, the `Person` class has two instance variables, `name` and `age`, which are initialized in the `__init__()` method.

To create an object from the `Person` class and initialize its attributes, you would use code like this:

```python
p = Person("John", 35)
```

## Class Methods&#x20;

In Python, a class method is a method that is bound to the class and not the instance of the class. It can be called on the class itself, rather than on an instance of the class.

To define a class method in Python, you need to use the `@classmethod` decorator before the method definition. The first parameter of a class method is always the class itself, which is conventionally named `cls`.

Here is an example of defining a class method in Python:

```python
class MyClass:
    x = 0
    
    @classmethod
    def set_x(cls, value):
        cls.x = value
```

In this example, the `set_x` method is a class method. It takes the class `cls` as its first parameter, and sets the class variable `x` to the given value. This method can be called on the class itself, rather than on an instance of the class, like this:

```python
MyClass.set_x(42)
```

After this call, the `x` class variable will have the value of `42`. Note that you can also call class methods on an instance of the class, in which case the instance will be passed as the first parameter (`cls`), but this is less common.

## Static Methods&#x20;

In Python, a static method is a method that belongs to a class rather than an instance of that class. It can be called on the class itself, rather than on an object of the class.

To define a static method in a class, the `@staticmethod` decorator is used before the method definition.

Here's an example:

```python
class MyClass:
    x = 0

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

    @staticmethod
    def my_static_method(a, b):
        return a + b
```

In the example above, `my_static_method()` is a static method. It can be called using the class name, like this:

```python
result = MyClass.my_static_method(1, 2)
```

Here, we are calling the `my_static_method()` method on the `MyClass` class, passing in two arguments, `1` and `2`. The method returns the sum of the two arguments, which is assigned to the `result` variable.

Static methods are often used when a method does not require access to any instance variables or methods, and is only related to the class as a whole.

## Overriding Methods&#x20;

Overriding methods is a concept in Object-Oriented Programming (OOP) where a subclass provides its implementation of a method that is already defined in its superclass. When a method is called on an object of a subclass, the implementation of the method in the subclass is used instead of the one in the superclass.

To override a method in a subclass, you must define a method with the same name and signature (i.e., same parameters) as the method in the superclass. The method in the subclass must also have the same return type or a subtype of the return type of the method in the superclass.

Here's an example of overriding a method in Python:

```ruby
class Animal:
    def make_sound(self):
        print("The animal makes a sound.")

class Dog(Animal):
    def make_sound(self):
        print("The dog barks.")

animal = Animal()
animal.make_sound()  # prints "The animal makes a sound."

dog = Dog()
dog.make_sound()  # prints "The dog barks."
```

In this example, we define a superclass `Animal` with a method `make_sound()`, which simply prints "The animal makes a sound." We then define a subclass `Dog` that inherits from `Animal` and overrides the `make_sound()` method with its own implementation that prints "The dog barks."

When we create an instance of the `Animal` class and call its `make_sound()` method, it prints "The animal makes a sound." When we create an instance of the `Dog` class and call its `make_sound()` method, it prints "The dog barks." The `make_sound()` method is overridden in the `Dog` subclass, so its implementation is used instead of the one in the `Animal` superclass.<br>

## Class Naming Convention&#x20;

In Python, the naming convention for classes is to use CamelCase, which is a naming convention where each word in a name is capitalized and concatenated together without any underscores. This is done to make the class name more readable and to differentiate it from variable names, which typically use underscores to separate words.

For example, consider a class that represents a car. A suitable name for the class would be `Car` in CamelCase notation, while a variable that represents a car object could be named `my_car` or `the_car` using underscores to separate the words.

It is important to follow naming conventions to ensure that code is easily readable and maintainable.&#x20;

## Object Properties  <a href="#h-object-properties" id="h-object-properties"></a>

In Python, object properties are also known as instance variables. These variables are specific to an instance of a class, meaning that each object or instance of a class can have its own set of properties.

Instance variables can be defined within a class by assigning a value to a variable name using the `self` keyword. The `self` keyword refers to the instance of the class and is used to access its attributes and methods.

Here's an example of defining instance variables in a class:

```ruby
class Person:
    def __init__(self, name, age):
        self.name = name
        self.age = age
```

In this example, the `Person` class has two instance variables, `name` and `age`, which are defined in the `__init__()` method using the `self` keyword. These instance variables can be accessed and modified by creating an object of the `Person` class and using dot notation:

```python
person1 = Person("Alice", 25)
print(person1.name)  # Output: Alice
person1.age = 26
print(person1.age)   # Output: 26
```

### Modify Object Properties  <a href="#h-modify-object-properties" id="h-modify-object-properties"></a>

To modify an object's property in Python, you can simply access the property using the dot notation and assign a new value to it. Here's an example:

```python
class Car:
    def __init__(self, make, model, year):
        self.make = make
        self.model = model
        self.year = year

my_car = Car("Toyota", "Corolla", 2020)

print(my_car.make)  # output: Toyota

my_car.make = "Honda"

print(my_car.make)  # output: Honda
```

In this example, we define a `Car` class with `make`, `model`, and `year` properties. We create an instance of the `Car` class and assign it to the variable `my_car`. We then print the value of the `make` property using `my_car.make`.

We then modify the value of the `make` property by assigning a new value to it using the dot notation: `my_car.make = "Honda"`. Finally, we print the new value of the `make` property using `print(my_car.make)`. The output of the program will be `Honda`.

### &#x20;Delete object properties&#x20;

To delete an object property in Python, you can use the `del` statement followed by the object name and the property name. Here is an example:

```python
class Person:
  def __init__(self, name, age):
    self.name = name
    self.age = age

person = Person("John", 25)
print(person.name)  # Output: John

del person.age
print(person.age)  # Raises AttributeError: 'Person' object has no attribute 'age'
```

In this example, we define a `Person` class with a `name` and `age` property. We create an instance of the `Person` class and assign it to the `person` variable. We then print the `name` property of the `person` object, which outputs "John".

Next, we use the `del` statement to delete the `age` property of the `person` object. When we try to print the `age` property of the `person` object again, we get an `AttributeError` because the property no longer exists.&#x20;

## Delete Objects  <a href="#h-delete-objects" id="h-delete-objects"></a>

To delete an object in Python, you can use the `del` keyword followed by the object name.

Here's an example:

```python
my_object = MyClass()
# do something with my_object

# delete the object
del my_object
```

After this, the `my_object` variable will no longer exist and the memory allocated to it will be freed up by Python's garbage collector.

<br>
