diff --git a/00_Python/04-Object-Oriented-Programming/01-Class-and-Objects.ipynb b/00_Python/04-Object-Oriented-Programming/01-Class-and-Objects.ipynb new file mode 100644 index 0000000..26be3ba --- /dev/null +++ b/00_Python/04-Object-Oriented-Programming/01-Class-and-Objects.ipynb @@ -0,0 +1,329 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Class and Objects\n", + "\n", + "A class is a blueprint or a template for creating objects, which are instances of the class. Classes encapsulate data and the behavior that operates on that data. A class defines a set of attributes and methods that can be used to create objects. In Python, you can define a class using the `class` keyword. Attributes and methods are the two key components of a python class. Attributes are the characteristics or properties of an object, while methods are the functions that are associated with the object.\n", + "\n", + "Here's an example of a simple class definition:" + ] + }, + { + "cell_type": "code", + "execution_count": 8, + "metadata": {}, + "outputs": [], + "source": [ + "class Person:\n", + " def __init__(self, name, age, sex):\n", + " self.name = name\n", + " self.age = age\n", + " self.sex = sex\n", + " \n", + " def gender(self):\n", + " print(f\"{self.name} is {self.sex}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "This class, called `Person`, has three attributes (`name`, `age`, and `sex`) and one method (`gender`). The `__init__` method is a special method (**constructor**) that is called when a new instance of the class is created. It takes three arguments (`name`, `age`, and `sex`) and initializes the corresponding attributes. The `gender` method is a simple method that prints a message to the console.\n", + "\n", + "To create an instance of the `Person` class, you can simply call the class with the appropriate arguments:" + ] + }, + { + "cell_type": "code", + "execution_count": 9, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n" + ] + } + ], + "source": [ + "person1 = Person(\"Luluw\", 23, \"Male\")\n", + "print(type(person1)) " + ] + }, + { + "cell_type": "code", + "execution_count": 10, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Luluw, 23, Male\n" + ] + } + ], + "source": [ + "print(person1.name, person1.age, person1.sex, sep=\", \")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "You can call the methods of an instance using the dot notation as well." + ] + }, + { + "cell_type": "code", + "execution_count": 11, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Luluw is Male\n" + ] + } + ], + "source": [ + "person1.gender()" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Types of Attributes\n", + "\n", + "#### 1. Instance Attributes\n", + "\n", + "These are the attributes that belong to instances of a class. They are defined within the constructor method `__init__` and can be accessed using the `self` keyword like `self.name`, `self.age`, and `self.sex` in the above `Person` class.. They are initialized when a new instance of the class is created.\n", + "\n", + "#### 2. Class attributes\n", + "\n", + "Class attributes are attributes that belong to the class itself. They are defined outside the constructor method `__init__` and can be accessed using the class name. Class attributes are shared by all instances of the class like `count` attribute in the `Person` class below.\n", + "\n", + "## Types of Methods\n", + "\n", + "#### 1. Instance methods\n", + "\n", + "The most common type of method in Python. These are the methods that operate on an instance of a class and have access to the instance's attributes. Instance methods are defined within the class and are called on instances of the class like `gender` method of the `Person` class above.\n", + "\n", + "#### 2. Class methods\n", + "\n", + "Class methods are methods that operate on the class itself rather than on instances of the class. They are defined using the `@classmethod` decorator and take the class itself as the first argument like `get_count` method of `Person` class below.\n", + "\n", + "#### 3. Static methods\n", + "\n", + "Static methods are methods that do not operate on the instance or the class, but are related to the class in some way. They are defined using the `@staticmethod` decorator and do not take the instance or the class as arguments like `get_full_name` in `Person` class below." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class Person:\n", + " count = 0 # Class Attribute/Variable\n", + "\n", + " def __init__(self, name):\n", + " self.name = name\n", + " Person.count += 1\n", + "\n", + " @classmethod\n", + " def get_count(cls): # Defining class method\n", + " return cls.count\n", + "\n", + " @staticmethod\n", + " def get_full_name(firstname, secondname):\n", + " return f\"{firstname} {secondname}\"\n" + ] + }, + { + "cell_type": "code", + "execution_count": 49, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "0\n", + "1\n", + "2\n", + "Chandler Bing\n" + ] + } + ], + "source": [ + "print(Person.get_count())\n", + "\n", + "person1 = Person(\"Alice\")\n", + "print(Person.get_count())\n", + "\n", + "person2 = Person(\"Bob\")\n", + "print(Person.get_count())\n", + "\n", + "print(Person.get_full_name('Chandler', 'Bing'))" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this example, we call the `get_count` method on the `Person` class to get the initial value of the `count` attribute (which is `0`). We then create two instances of the `Person` class (`person1` and `person2`), which increment the count attribute each time. Finally, we call the `get_count` method again to get the current value of the `count` attribute (which is `2`).\n", + "\n", + "This is just a brief introduction to OOP in Python. There are many more advanced features and techniques that you can use to create powerful and flexible programs." + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "✅ **Key Differences:**\n", + "| Feature | Instance Method (Default) | Class Method (`@classmethod`) | Static Method (`@staticmethod`) |\n", + "|------------------|------------------|-----------------|-----------------|\n", + "| First Parameter | `self` (instance) | `cls` (class) | No `self` or `cls` |\n", + "| Access Instance Variables? | ✅ Yes | ❌ No | ❌ No |\n", + "| Access Class Variables? | ✅ Yes | ✅ Yes | ❌ No |\n", + "| Can Modify Instance State? | ✅ Yes | ❌ No | ❌ No |" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "metadata": {}, + "outputs": [], + "source": [ + "class Car:\n", + " count = 0 # Class Attribute/Variable \n", + " \n", + " def __init__(self, brand_name: str, car_type: str, year: int, price: float) -> None:\n", + " # Car Attributes\n", + " self.brand_name: str = brand_name\n", + " self.car_type: str = car_type\n", + " self.year: int = year\n", + " self.price: float = price\n", + "\n", + " # Incrementing Car class instance count\n", + " Car.count += 1\n", + "\n", + "\n", + " def display_info(self) -> str:\n", + " \"\"\"Display the details of car\"\"\"\n", + " return f\"\"\"\n", + " ID_creation: {Car.count}\n", + " Brand: {self.brand_name},\n", + " Type: {self.car_type},\n", + " Year: {self.year},\n", + " Price: {self.price:,.2f}$\n", + " \"\"\"\n", + "\n", + " def get_discounted_price(self) -> float:\n", + " \"\"\"Return the final price after deducting discount\"\"\"\n", + " return self.price - (self.price * 0.05) # 0.05 is discount rate\n", + " \n", + " @classmethod\n", + " def from_string(cls, car_str: str) -> \"Car\":\n", + " brand, car_type, year, price = [x.strip() for x in car_str.split(\",\")]\n", + " return cls(brand, car_type, int(year), float(price))\n", + "\n", + " @staticmethod\n", + " def is_luxury(price: float) -> bool:\n", + " return price > 50000" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this example, we call the `display_info` method on the `Car` class to get the details of a car. We then create two instances of the `Car` class (`car1` and `car2`) using both the constructor and the `from_string` class method. After that, we call the `get_discounted_price` method to get the price after applying a **5% discount**. Finally, we use the `is_luxury` static method to check whether a car is luxury (if its price is above **$50,000**)." + ] + }, + { + "cell_type": "code", + "execution_count": 59, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + " ID_creation: 1\n", + " Brand: Tesla,\n", + " Type: Electronic,\n", + " Year: 2024,\n", + " Price: 55,000.00$\n", + " \n", + "Discounted Price: 52250.0\n", + "True\n" + ] + } + ], + "source": [ + "car1 = Car(\"Tesla\", \"Electronic\", 2024, 55000)\n", + "car1_info, car1_price = car1.display_info(), car1.get_discounted_price()\n", + "print(car1_info)\n", + "print(\"Discounted Price:\", car1_price)\n", + "print(Car.is_luxury(car1.price))\n" + ] + }, + { + "cell_type": "code", + "execution_count": 60, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "\n", + " ID_creation: 2\n", + " Brand: Toyota,\n", + " Type: Sedan,\n", + " Year: 2023,\n", + " Price: 32,000.00$\n", + " \n", + "Discounted Price: 30400.0\n", + "False\n" + ] + } + ], + "source": [ + "car2 = Car.from_string(\"Toyota, Sedan, 2023, 32000\")\n", + "print(car2.display_info())\n", + "print(\"Discounted Price:\", car2.get_discounted_price())\n", + "print(Car.is_luxury(car2.price))" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "env", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.2" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +} diff --git a/00_Python/04-Object-Oriented-Programming/02-Method-Overloading.ipynb b/00_Python/04-Object-Oriented-Programming/02-Method-Overloading.ipynb new file mode 100644 index 0000000..e5adabb --- /dev/null +++ b/00_Python/04-Object-Oriented-Programming/02-Method-Overloading.ipynb @@ -0,0 +1,111 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Method Overloading in Python\n", + "\n", + "**Method Overloading** is a concept in OOP where a class can have multiple methods with same name, but with different parameters or argument types. In Python, method overloading is not supported in the same way as it is in other OOP languages such as _Java_ or _C++_. However, there are some ways to achieve similar functionality in Python.\n", + "\n", + "One approach is to use default arguments in the method definition. For example, consider the following code:" + ] + }, + { + "cell_type": "code", + "execution_count": 7, + "metadata": {}, + "outputs": [], + "source": [ + "class Example:\n", + " def add(self, a, b=None, c=None):\n", + " if b is not None and c is not None:\n", + " return a + b + c\n", + " elif b is not None:\n", + " return a + c\n", + " else:\n", + " return a" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Here, the `add` method can take one, two, or three arguments. If only one argument is passed, it returns the value of that argument. If two arguments are passed, it returns the sum of those two arguments. And if three arguments are passed, it returns the sum of all three arguments.\n", + "\n", + "Another approach is to use variable-length arguments, which allow a method to take an arbitrary number of arguments. This can be achieved using the `*args` and `**kwargs` syntax. For example:" + ] + }, + { + "cell_type": "code", + "execution_count": 22, + "metadata": {}, + "outputs": [], + "source": [ + "class Example:\n", + " def __init__(self, *args):\n", + " self.args = args\n", + "\n", + " def add(self, *args):\n", + " return sum(args)\n", + "\n", + " def __str__(self):\n", + " # Display the result of the add method for the arguments\n", + " return f\"Arguments: {self.args}\\t Add result: {self.add(*self.args)}\"\n" + ] + }, + { + "cell_type": "code", + "execution_count": 23, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Arguments: (10,)\t Add result: 10\n", + "Arguments: (10, 8)\t Add result: 18\n", + "Arguments: (1, 2, 3, 4)\t Add result: 10\n" + ] + } + ], + "source": [ + "example1 = Example(10)\n", + "example2 = Example(10, 8)\n", + "example3 = Example(1, 2, 3, 4)\n", + "\n", + "print(example1, example2, example3, sep=\"\\n\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "In this example, the `add` method takes any number of arguments, and the behavior is determined by the length of the arguments tuple.\n", + "\n", + "While these approaches can achieve similar functionality to method overloading, it is important to note that they are not exactly the same, and may not be suitable in all situations. It is generally recommended to use descriptive method names that reflect the intended behavior, rather than relying on overloading." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "env", + "language": "python", + "name": "python3" + }, + "language_info": { + "codemirror_mode": { + "name": "ipython", + "version": 3 + }, + "file_extension": ".py", + "mimetype": "text/x-python", + "name": "python", + "nbconvert_exporter": "python", + "pygments_lexer": "ipython3", + "version": "3.11.2" + } + }, + "nbformat": 4, + "nbformat_minor": 2 +}