Introduction to Classes in Python
Published: 2024-02-22 01:12:17

# Introduction

In this chapter, we'll explore the concept of classes in Python.

A class is a blueprint for creating objects, which are instances of the class. Classes define a set of attributes and methods that are common to all objects created from the class.

# Class Methods

One of the most important methods in a class is the `__init__` method, which is used to initialize the attributes of an object when it is created. Here is an example of a simple class that represents a complex number, with an `__init__` method that takes two arguments, `real` and `imaginary`, and assigns them to the corresponding attributes of the object:

```{python}
class ComplexNumber:
    def __init__(self, real: float, imaginary: float) -> None:
        self.real = real
        self.imaginary = imaginary
```

We can create an instance of this class by calling the class name as if it were a function, passing in the arguments for the `__init__` method:

```{python}
c = ComplexNumber(real=3, imaginary=4)
print(c.real)
print(c.imaginary)
```

```
Output:
3
4
```

In addition to the `__init__` method, there are several other special methods that we can define in a class to customize its behaviour. These methods have names that start and end with double underscores, such as `__str__`, `__repr__`, and `__eq__`.

The `__str__` method is used to define a human-readable representation of the object and is called when we use the `print` function or the `str` function on the object. Here is an example of how we can define the `__str__` method for our `ComplexNumber` class:

```{python}
class ComplexNumber:
    def __init__(self, real: float, imaginary: float) -> None:
        self.real = real
        self.imaginary = imaginary

    def __str__(self) -> str:
        return f"{self.real} + {self.imaginary}i"
```

Now, when we create an instance of the `ComplexNumber` class and print it, we get a nicely formatted string representation of the complex number:

```{python}
c = ComplexNumber(real=3, imaginary=4)
print(c)
```

```
Output:
3 + 4i
```

The `__repr__` method is similar to the `__str__` method but is used to define a more formal, code-like representation of the object. It is called when we use the `repr` function on the object, or when we enter the object into the interactive interpreter. Here is an example of how we can define the `__repr__` method for our `ComplexNumber` class:

```{python}
class ComplexNumber:
    def __init__(self, real: float, imaginary: float) -> None:
        self.real = real
        self.imaginary = imaginary

    def __str__(self) -> str:
        return f"{self.real} + {self.imaginary}i"

    def __repr__(self) -> str:
        return f"ComplexNumber({self.real}, {self.imaginary})"
```

Now, when we create an instance of the `ComplexNumber` class and enter it into the **interactive interpreter** (example: IDLE), we get a formal representation of the object that shows how it can be constructed:

```{python}
c = ComplexNumber(real=3, imaginary=4)
c
```

```
Output:
ComplexNumber(3, 4)
```

Other special methods that we can define in a class include `__eq__`, which is used to compare two objects for equality, `__setattr__` and `__getattr__`, which are used to customize attribute access, and `__doc__`, which contains the docstring of the class.

# Regular Methods, Class Methods, and Static Methods

In addition to these special methods, we can also define regular methods in a class to perform operations on the object. For example, we can define a method to calculate the magnitude of a complex number:

```{python}
class ComplexNumber:
    def __init__(self, real: float, imaginary: float) -> None:
        self.real = real
        self.imaginary = imaginary

    def __str__(self) -> str:
        return f"{self.real} + {self.imaginary}i"

    def __repr__(self) -> str:
        return f"ComplexNumber({self.real}, {self.imaginary})"

    def magnitude(self) -> float:
        return (self.real ** 2 + self.imaginary ** 2) ** 0.5
```

We can call this method on an instance of the `ComplexNumber` class to calculate its magnitude:

```{python}
c = ComplexNumber(real=3, imaginary=4)
print(c.magnitude())
```

```
Output:
5.0
```

In addition to regular methods, we can also define class methods and static methods in a class. Class methods are methods that are bound to the class and not the instance of the class. They are defined using the `@classmethod` decorator, and take the class itself as their first argument, rather than the instance. Here is an example of a class method that creates a `ComplexNumber` object from a polar representation, given the magnitude and angle:

```{python}
class ComplexNumber:
    def __init__(self, real: float, imaginary: float) -> None:
        self.real = real
        self.imaginary = imaginary

    def __str__(self) -> str:
        return f"{self.real} + {self.imaginary}i"

    def __repr__(self) -> str:
        return f"ComplexNumber({self.real}, {self.imaginary})"

    def magnitude(self) -> float:
        return (self.real ** 2 + self.imaginary ** 2) ** 0.5

    @classmethod
    def from_polar(cls, magnitude: float, angle: float) -> "ComplexNumber":
        real = magnitude * math.cos(angle)
        imaginary = magnitude * math.sin(angle)
        return cls(real, imaginary)
```

We can call this class method on the `ComplexNumber` class itself, rather than on an instance, to create a new ComplexNumber object:

```{python}
c = ComplexNumber.from_polar(magnitude=5, angle=math.pi / 4)
print(c)
```

```
Output:
3.5355339059327373 + 3.5355339059327378i
```

Static methods are similar to class methods, but they do not take the class or the instance as their first argument. They are defined using the `@staticmethod` decorator and can be used to define utility functions that are related to the class but do not depend on its state. Here is an example of a static method that calculates the Euclidean distance between two complex numbers:

```{python}
class ComplexNumber:
    def __init__(self, real: float, imaginary: float) -> None:
        self.real = real
        self.imaginary = imaginary

    def __str__(self) -> str:
        return f"{self.real} + {self.imaginary}i"

    def __repr__(self) -> str:
        return f"ComplexNumber({self.real}, {self.imaginary})"

    def magnitude(self) -> str:
        return (self.real ** 2 + self.imaginary ** 2) ** 0.5

    @classmethod
    def from_polar(cls, magnitude: float, angle: float) -> "ComplexNumber":
        real = magnitude * math.cos(angle)
        imaginary = magnitude * math.sin(angle)
        return cls(real, imaginary)

    @staticmethod
    def distance(c1: "ComplexNumber", c2: "ComplexNumber") -> float:
        return ((c1.real - c2.real) ** 2 + (c1.imaginary - c2.imaginary) ** 2) ** 0.5
```

We can call this static method on the `ComplexNumber` class itself, or on an instance, to calculate the distance between two complex numbers:

```{python}
c1 = ComplexNumber(real=3, imaginary=4)
c2 = ComplexNumber(real=6, imaginary=8)
print(ComplexNumber.distance(c1=c1, c2=c2))
```

```
Output:
5.0
```

# Built-in Functions

In addition to defining methods, we can also use several built-in functions to interact with the attributes of an object. The `setattr` function can be used to set the value of an attribute, the `getattr` function can be used to get the value of an attribute, and the `hasattr` function can be used to check if an object has a specific attribute. Here is an example of how we can use these functions with our `ComplexNumber` class:

```{python}
c = ComplexNumber(real=3, imaginary=4)
print(getattr(c, "real"))
setattr(c, "real", 5)
print(c.real)
print(hasattr(c, "imaginary"))
```

```
Output:
3
5
True
```

# Conclusion

In summary, classes in Python provide a powerful mechanism for defining custom data types, with their attributes and methods. By defining special methods, we can customize the behaviour of the class, and by defining regular methods, class methods, and static methods, we can define operations that can be performed on the objects. We can also use built-in functions to interact with the attributes of the objects.