Python Data Types – Sets

Python Set Data Type

In Python, a Set is an unordered data type that represents a collection of unique values. Sets are defined using curly braces {}, and each element is unique. For example:

my_set = {1, 2, 3, 4, 5}

Characteristics of Sets

1. No Duplicates Allowed

Since sets do not allow duplicate values, if the same value is added multiple times, only one instance is stored.

my_set = {1, 2, 2, 3, 4}
print(my_set)  # {1, 2, 3, 4}

2. Unordered

Since sets are an unordered type, they do not support indexing or slicing. To access elements of a set, you must use a loop.

my_set = {"apple", "banana", "cherry"}
for item in my_set:
    print(item)

3. Adding and Removing Elements in a Set

Sets are mutable, which means you can add or remove elements. You can use the add() method to add elements, and remove() or discard() methods to remove them.

my_set = {1, 2, 3}
my_set.add(4)            # {1, 2, 3, 4}
my_set.remove(2)         # {1, 3, 4}
my_set.discard(5)        # {1, 3, 4} (no error when removing a non-existent element)
print(my_set)

4. Set Operations

Sets support various operations such as union, intersection, and difference. These operations can be performed using the |, &, and - operators or methods.

set1 = {1, 2, 3, 4}
set2 = {3, 4, 5, 6}

# Union
union_set = set1 | set2
print(union_set)  # {1, 2, 3, 4, 5, 6}

# Intersection
intersection_set = set1 & set2
print(intersection_set)  # {3, 4}

# Difference
difference_set = set1 - set2
print(difference_set)  # {1, 2}

5. Set Methods

Sets offer various methods for easy manipulation of elements:

  • set.add(x): Adds an element to the set.
  • set.remove(x): Removes a specific element from the set, and raises an error if the element is not present.
  • set.discard(x): Removes a specific element from the set, and does not raise an error if the element is not present.
  • set.union(other_set): Returns the union of two sets.
  • set.intersection(other_set): Returns the intersection of two sets.
  • set.difference(other_set): Returns the difference of two sets.
set1 = {1, 2, 3}
set2 = {3, 4, 5}

set1.add(6)
print(set1)  # {1, 2, 3, 6}

set1.discard(2)
print(set1)  # {1, 3, 6}

union = set1.union(set2)
print(union)  # {1, 3, 4, 5, 6}

intersection = set1.intersection(set2)
print(intersection)  # {3}

6. Applications of Sets

Sets are useful in various situations such as removing duplicate values or finding common elements through operations between multiple sets. For example, to remove duplicates from a list, you can convert it to a set.

my_list = [1, 2, 2, 3, 4, 4, 5]
my_set = set(my_list)
print(my_set)  # {1, 2, 3, 4, 5}

Summary

  • Sets represent a collection of unique values and do not allow duplicate values.
  • Sets are unordered, so they do not support indexing or slicing.
  • You can manipulate set elements using methods such as add(), remove(), and discard().
  • Sets support operations such as union, intersection, and difference, which allow for easy analysis of data relationships.
  • Sets can be used to remove duplicate values or find common elements, among other tasks.

Sets are one of the most useful data types in Python, particularly suited for handling duplicate data or performing set operations. Utilize the various features of sets to manage data efficiently!

Features of Python

Python is a powerful yet easy-to-learn programming language that is widely used in various fields. The characteristics of Python make it an easy language to learn, while also serving as a strong tool. In this article, we will look at the main features of Python.

1. Concise and Easy-to-Read Syntax

Python has a concise and intuitive syntax, allowing you to write code without using complex syntax. Python code feels like reading an English sentence, which greatly enhances the readability of the code. Beginners can quickly learn and use the basic concepts of Python.

x = 10
y = 20
print(x + y)  # 30

2. Dynamic Typing

Python is a dynamically typed language, meaning you do not need to declare the data type of a variable in advance. This allows you to assign any data type freely when declaring a variable. Thanks to this feature, coding is convenient and flexible, but caution is needed as there is a possibility of errors in large projects.

value = 10       # Integer
value = "Hello" # Changed to String

3. Rich Libraries and Frameworks

Python offers a variety of built-in libraries and open-source frameworks. These libraries help implement specific functions easily. With various libraries such as pandas for data analysis, numpy for mathematical calculations, and Django and Flask for web development, you can handle complex tasks simply.

4. Cross-Platform Support

Python is a platform-independent language, meaning the same code runs seamlessly on various operating systems such as Windows, MacOS, and Linux. This allows Python to be used in different environments, enabling the programs developed by developers to be easily used across multiple platforms.

5. Interactive Development Environment (Interactive Shell)

Python provides an interactive shell, allowing you to execute code line by line and immediately see the results. This enables developers to quickly test and experiment with code, making it easier to debug issues. Notable interactive environments include IDLE and Jupyter Notebook.

6. Support for Object-Oriented Programming

Python supports Object-Oriented Programming (OOP). This enhances the reusability of code and allows complex problems to be easily solved by dividing them into logical object units. You can write more structured programs using classes and objects.

class Animal:
    def __init__(self, name):
        self.name = name

    def speak(self):
        print(f"{self.name} makes a sound.")

cat = Animal("Cat")
cat.speak()  # Cat makes a sound.

7. Strong Community and Abundant Resources

Python has a vibrant community worldwide. This makes it easy for beginners to find abundant resources and tutorials to learn. When issues arise, you can get help from sites like Stack Overflow, and there are opportunities to contribute to various open-source projects.

Conclusion

Due to its concise syntax, powerful libraries, and broad platform support, Python is a language chosen by many developers. The features of Python help make programming easier to learn and use, functioning as a tool that can efficiently handle various complex tasks. For these reasons, Python is widely used in many fields such as web development, data analysis, artificial intelligence, and automation.

Exploring Python

Python is a programming language suitable for everyone from beginners to experts, widely used around the world thanks to its concise syntax and powerful features. In this article, we will introduce the charm of Python by exploring what kind of language it is and various features and application cases.

1. History and Philosophy of Python

Python was first released in 1991 by Dutch programmer Guido van Rossum. He wanted programming to be easier and more enjoyable, and designed Python with this in mind. Python embraces the philosophy of ‘code readability’, aiming for a concise and intuitive syntax. This allows beginners to learn easily and implement complex code simply.

2. Syntax Features of Python

  • Concise and Easy Syntax: Python has a code style that reads like English, making it easier to understand compared to other languages. Because of this simple syntax, Python is widely known as a good language for beginners.
  • if age >= 18: print(“You are an adult.”) else: print(“You are a minor.”)
  • Dynamic Typing: Python does not require explicit declaration of variable types. The type of a variable is automatically determined when the program runs, allowing for flexible and rapid code development.

3. Abundant Libraries

One of Python’s greatest advantages is its wide range of standard libraries and open-source libraries. This allows for complex tasks to be solved easily.

  • Data Science: Libraries like pandas, numpy, and matplotlib help with easy data analysis and visualization.
  • Web Development: Web frameworks like Django and Flask allow for fast and efficient development of web applications.
  • Artificial Intelligence and Machine Learning: Libraries like TensorFlow and PyTorch make it easy to start projects in artificial intelligence and machine learning.

4. Application Fields of Python

Python is used in various fields, and its potential applications are limitless.

  • Web Development: Python is widely used to build the back-end of web servers. Django and Flask are powerful web frameworks that help in developing fast and secure web applications.
  • Data Science: Python is frequently used for data analysis, data visualization, and building machine learning models. It has become an almost essential tool for data scientists.
  • Writing Automation Scripts: Python is very useful for writing scripts to automate various repetitive tasks. For example, Python scripts play a big role in file management, data crawling, and server maintenance.
  • Game Development: You can develop simple 2D games using the pygame library, which allows you to learn and practice the basics of game development.

5. Python Community and Ecosystem

Python boasts a vast user base and an active community. Many developers around the world use Python and contribute to various open-source projects. This provides beginners with easy access to help, as well as numerous tutorials and learning resources.

  • PyPI (Python Package Index): The official package repository for Python, where you can download and use thousands of packages. This allows you to easily add the required features to your projects.

6. Advantages and Disadvantages of Python

  • Advantages: Python is easy to learn with a concise syntax, supported by a variety of libraries and a robust community. It enables rapid prototype development and can run on various platforms.
  • Disadvantages: As an interpreted language, Python can be slower than compiled languages. Additionally, dynamic typing may occasionally reduce the stability of the code.

Conclusion

Python is an easy-to-learn yet powerful language that is used in various fields. It allows you to realize ideas through web development, data science, artificial intelligence, and game development. Python’s concise syntax and vast library ecosystem enable programmers to work efficiently and productively. Now, take a look at Python, and experience its charm by using it yourself!

Python Multithreading 2 (Synchronization)

This article explains the concept of thread synchronization in the case of multithreading in the Python programming language.

Thread Synchronization

Thread synchronization is defined as a mechanism that prevents two or more concurrent threads from executing specific segments of a program simultaneously, known as critical sections.

A critical section refers to a part of a program that accesses shared resources.

For example, in the diagram below, three threads are trying to access shared resources or critical sections simultaneously.

Concurrent access to shared resources can lead to race conditions.

A race condition occurs when two or more threads can access shared data and attempt to modify it at the same time. As a result, the variable values become unpredictable and can vary depending on the timing of context switches in the process.

To understand the concept of race conditions, consider the program below.

import threading# global variable xx = 0def increment():    “””    function to increment global variable x    “””    global x    x += 1def thread_task():    “””    task for thread    calls increment function 100000 times.    “””    for _ in range(100000):        increment()def main_task():    global x    # setting global variable x as 0    x = 0    # creating threads    t1 = threading.Thread(target=thread_task)    t2 = threading.Thread(target=thread_task)    # start threads    t1.start()    t2.start()    # wait until threads finish their job    t1.join()    t2.join()if __name__ == “__main__”:    for i in range(10):        main_task()        print(“Iteration {0}: x = {1}”.format(i,x))

Output:

Iteration 0: x = 175005
Iteration 1: x = 200000
Iteration 2: x = 200000
Iteration 3: x = 169432
Iteration 4: x = 153316
Iteration 5: x = 200000
Iteration 6: x = 167322
Iteration 7: x = 200000
Iteration 8: x = 169917
Iteration 9: x = 153589

In the above program:

  • The main_task function creates two threads t1 and t2 and sets the global variable x to 0.
  • Each thread has a target function thread_task that calls the increment function 100000 times.
  • The increment function increases the global variable x by 1 each time it is called.

The expected final value of x is 200000, but the value obtained by repeating the main_task function 10 times varies.

This occurs due to concurrent access to the shared variable x. This unpredictability of the x value is simply a race condition.

Below is a diagram showing how a race condition occurs in the above program.

In the above diagram, the expected value of x is 12, but due to the race condition, it turned out to be 11!

Therefore, there is a need for tools to properly synchronize multiple threads.

Using Locks

The threading module provides the Lock class to handle race conditions. Locks are implemented using Semaphore objects provided by the operating system.

A semaphore is a synchronization object that controls access to common resources by multiple processes/threads in a parallel programming environment. It is simply a value located in a designated area of the operating system (or kernel) storage that each process/thread can check and modify. Depending on the discovered value, the process/thread can either use the resource or confirm that the resource is already in use and must wait for a certain period before retrying. A semaphore can be binary (0 or 1) or can have additional values. Generally, a process/thread that uses a semaphore checks the value and then modifies it to reflect that a subsequent semaphore user must wait.

The Lock class provides the following methods:

  • acquire([blocking]) : Acquires the lock. The lock can be blocking or non-blocking.
    • Calling with the blocking argument set to True (default) causes the thread execution to block until the lock is released, after which the lock is set and True is returned.
    • Calling with the blocking argument set to False causes the thread execution not to block. If the lock is released, it sets the lock and returns True; otherwise, it immediately returns False.
  • release() : Releases the lock.
    • If the lock is locked, it resets to the unlocked state and returns. If there are other threads waiting for the lock to be released, exactly one of them is allowed to proceed.
    • If the lock is already released, a ThreadError is raised.

Consider the following example.

import threading# global variable xx = 0def increment():    “””    function to increment global variable x    “””    global x    x += 1def thread_task(lock):    “””    task for thread    calls increment function 100000 times.    “””    for _ in range(100000):        lock.acquire()        increment()        lock.release()def main_task():    global x    # setting global variable x as 0    x = 0    # creating a lock    lock = threading.Lock()    # creating threads    t1 = threading.Thread(target=thread_task, args=(lock,))    t2 = threading.Thread(target=thread_task, args=(lock,))    # start threads    t1.start()    t2.start()    # wait until threads finish their job    t1.join()    t2.join()if __name__ == “__main__”:    for i in range(10):        main_task()        print(“Iteration {0}: x = {1}”.format(i,x))

Output:

Iteration 0: x = 200000
Iteration 1: x = 200000
Iteration 2: x = 200000
Iteration 3: x = 200000
Iteration 4: x = 200000
Iteration 5: x = 200000
Iteration 6: x = 200000
Iteration 7: x = 200000
Iteration 8: x = 200000
Iteration 9: x = 200000

Now, let’s understand the code step by step.

  • First, a Lock object is created using lock = threading.Lock()
  • Then, the lock is passed as an argument to the target function. t1 = threading.Thread(target=thread_task, args=(lock,)) t2 = threading.Thread(target=thread_task, args=(lock,))
  • In the critical section of the target function, the lock.acquire() method is used to apply the lock. Once the lock is acquired, the lock.release() method is used to release the lock, preventing other threads from accessing the critical section (in this case, the increment function) until it is released. lock.acquire() increment() lock.release() As seen in the results, the final value of x results in 200000 each time (the final expected result).

Below is a diagram illustrating the lock implementation in the above program.

This concludes the tutorial series on multithreading in Python.
In conclusion, here are some advantages and disadvantages of multithreading:

Advantages:

  • Does not block the user. Threads are independent of each other.
  • Threads run tasks in parallel, making more efficient use of system resources.
  • Improved performance on multiprocessor systems.
  • Multithreaded servers and interactive GUIs exclusively use multithreading.

Disadvantages:

  • As the number of threads increases, complexity also increases.
  • Synchronization of shared resources (objects, data) is necessary.
  • Debugging can be difficult, and results may be unpredictable.
  • Potential deadlocks leading to starvation, meaning some threads may not get resources due to poor design.
  • Thread configuration and synchronization can be CPU/memory intensive.

Python Object-Oriented Programming (OOP): Tutorial

Learn the basics of object-oriented programming (OOP) in Python: explore classes, objects, instance methods, attributes, and more!

Object-oriented programming is a widely used concept for writing powerful applications. As a data scientist, you will need to create applications that handle data and perform a variety of other tasks. In this tutorial, we will explore the basics of object-oriented programming in Python. You will learn the following.

  • How to create a class
  • Object instantiation
  • Adding attributes to a class
  • Defining methods within a class
  • Passing arguments to methods
  • How to use OOP in finance with Python

 

OOP: Introduction

Object-oriented programming has several advantages over other design patterns. Development is faster and cheaper, and the maintainability of software is better. This ultimately leads to higher-quality software that can be extended with new methods and attributes. However, the learning curve is steeper, and the concepts can be too complex for beginners. Computationally, OOP software may be slower and require more lines of code, thus consuming more memory.

Object-oriented programming is based on the imperative programming paradigm, which uses statements to change the program’s state. It focuses on how the program should work. Examples of imperative programming languages include C, C++, Java, Go, Ruby, and Python. This contrasts with declarative programming, which focuses on what the computer program should achieve without specifying how. Examples include database query languages such as SQL and XQuery, where you tell the computer where and what data to query but not how to do it.

OOP uses the concepts of objects and classes. A class can be thought of as a ‘blueprint’ for creating objects. These objects can have unique properties (characteristics they own) and methods (actions they perform).

 

OOP Example

An example of a class is a class called Dog. Do not think of it as a specific dog or your own dog. We are describing what a dog is and what it can do in general. A dog typically has a name and an age. These are instance attributes. A dog can also bark; this is a method.

When talking about a specific dog, there will be an object in programming. An object is an instance of a class. This is the core principle of object-oriented programming. For example, my dog Ozzy belongs to the class Dog, and his attributes are name = ‘Ozzy’ and age = ‘2’. Other dogs will have different attributes.

 

Object-Oriented Programming in Python

Is Python Object-Oriented?

Python is a great programming language that supports OOP. You can define classes with properties and methods and then call them. Python offers several advantages over other programming languages such as Java, C++, or R. It is a dynamic language with high-level data types, which means it allows much faster development compared to Java or C++. Programmers do not need to declare variable and argument types. Additionally, Python is easy for beginners to understand and learn, and its code is more readable and intuitive.

 

How to Create a Class

To define a class in Python, use the class keyword, followed by the class name and a colon. Inside the class, methods should be defined with def __init__. This acts as an initializer that can later be used to instantiate an object. It is similar to constructors in Java. The __init__ method must always exist! It takes one argument, self, which refers to the object itself. Inside the method, you can use the pass keyword because Python expects something to be input there. Don’t forget to use the correct indentation!

class Dog:

    def __init__(self):
        pass

Note: In Python, self is equivalent to this in C++ or Java.

In this case, we have the Dog class (mostly empty), but we do not yet have an object. Let’s create one!

Instantiating Objects

To instantiate an object, you simply type the class name followed by two parentheses. You can assign this to a variable to keep track of the object.

ozzy = Dog()

And print it:

print(ozzy)

<__main__.Dog object at 0x111f47278>

 

Adding Attributes to a Class

After printing ozzy, it becomes clear that this object is indeed a dog. But we haven’t added any attributes yet. Let’s rewrite the Dog class to specify a name and age.

class Dog:

    def __init__(self, name, age):  
        self.name = name
        self.age = age

Now the function takes two arguments: name and age. These are then assigned to self.name and self.age, respectively. Now we can create a new object named ozzy using the name and age.

ozzy = Dog(“Ozzy”, 2)

To access an object’s attributes in Python, you can use dot notation. This is done by typing the object’s name followed by a dot and the name of the attribute.

print(ozzy.name)

print(ozzy.age)

Ozzy
2

This can also be combined into more sophisticated statements:

print(ozzy.name + ” is ” + str(ozzy.age) + ” year(s) old.”)

Ozzy is 2 year(s) old.

The str() function here is used to convert the integer attribute age to a string so it can be used within the print() function.

 

Defining Methods in a Class

Now that we have a Dog class with a name and age, we can keep track of them, but they don’t actually do anything yet. This is where instance methods come into play. We can rewrite the class to include a method. Note how the keyword is reused and how arguments are used in bark().

class Dog:

    def __init__(self, name, age):  
        self.name = name
        self.age = age

    def bark(self):
        print(“bark bark!”)

Now we can instantiate a new object and use dot notation to call the method bark. The method should print “bark bark!” on the screen. Note the parentheses when calling the bark() method. This is always used when calling a method, and in this case, the method bark() does not take any arguments, so the parentheses are empty.

ozzy = Dog(“Ozzy”, 2)

ozzy.bark()

bark bark!

Do you remember what you printed before ozzy? The code below now implements this functionality in the Dog class using the doginfo() method. Then we instantiate some objects with different attributes and call the method on those objects.

class Dog:

    def __init__(self, name, age):  
        self.name = name
        self.age = age

    def bark(self):
        print(“bark bark!”)

    def doginfo(self):
        print(self.name + ” is ” + str(self.age) + ” year(s) old.”)

ozzy = Dog(“Ozzy”, 2)
skippy = Dog(“Skippy”, 12)
filou = Dog(“Filou”, 8)

ozzy.doginfo()
skippy.doginfo()
filou.doginfo()

Ozzy is 2 year(s) old.
Skippy is 12 year(s) old.
Filou is 8 year(s) old.

As you can see, you can call a method from an object using dot notation. Now the responses depend on which object is calling the doginfo() method.

As dogs age, it’s good to adjust their age accordingly. Ozzy just turned 3, so let’s change his age.

ozzy.age = 3

print(ozzy.age)

3

It is as easy as assigning a new value to the attribute. You can also implement this as a method in the Dog class called birthday().

class Dog:

    def __init__(self, name, age):  
        self.name = name
        self.age = age

    def bark(self):
        print(“bark bark!”)

    def doginfo(self):
        print(self.name + ” is ” + str(self.age) + ” year(s) old.”)

    def birthday(self):
        self.age += 1

ozzy = Dog(“Ozzy”, 2)

print(ozzy.age)

2

ozzy.birthday()

print(ozzy.age)

3

Now there’s no need to manually change the dog’s age. You just need to call the birthday() method when it is the dog’s birthday.

 

Passing Arguments to Methods

English: You want your dog to have a buddy. Not all dogs are social, so this should be optional. Take a look at the below method. As usual, you use setBuddy() with an argument. In this case, it’s another object. This sets the attributes buddy and buddy, meaning the relationship is reciprocal; you’re friends with each other’s friends. In this case, Filou becomes Ozzy’s buddy, and thus Ozzy automatically becomes Filou’s buddy. Instead of defining the method, you could manually set these attributes, but that requires more work (two lines of code instead of one) every time you set a buddy. Python does not require you to specify the types of arguments. If this were Java, it would be mandatory.

class Dog:

    def __init__(self, name, age):  
        self.name = name
        self.age = age

    def bark(self):
        print(“bark bark!”)

    def doginfo(self):
        print(self.name + ” is ” + str(self.age) + ” year(s) old.”)

    def birthday(self):
        self.age += 1

    def setBuddy(self, buddy):
        self.buddy = buddy
        buddy.buddy = self

Now you can call the method using dot notation and pass another Dog object. In this case, Ozzy’s buddy becomes Filou.

ozzy = Dog(“Ozzy”, 2)
filou = Dog(“Filou”, 8)

ozzy.setBuddy(filou)

If you want to know about Ozzy’s buddy, you can use dot notation twice. The first refers to Ozzy’s buddy, and the second refers to that buddy’s attributes.

print(ozzy.buddy.name)
print(ozzy.buddy.age)

Filou
8

Note how this is also possible for Filou.

print(filou.buddy.name)
print(filou.buddy.age)

Ozzy
2

You can also call methods of friends. The argument being passed is now self, which is ozzy.buddy, meaning Filou.

ozzy.buddy.doginfo()

Filou is 8 year(s) old.

 

Python OOP Example

An example of where object-oriented programming can be useful in Python is in the Python For Finance: Algorithmic Trading tutorial, where we outline how to set up trading strategies for stock portfolios. The trading strategy is based on moving averages of stock prices; a signal is generated when signals[‘short_mavg’][short_window:] > signals[‘long_mavg’][short_window:] is met. This signal is a prediction of future price changes in the stock. In the code below, there is an initialization first and then the moving average calculations and signal generation. This is a single large chunk of code that runs at once, and this aapl is Apple’s stock ticker. You would need to rewrite the code to perform this task for another stock.

# Initialize
short_window = 40
long_window = 100
signals = pd.DataFrame(index=aapl.index)
signals[‘signal’] = 0.0

# Create short simple moving average over the short window
signals[‘short_mavg’] = aapl[‘Close’].rolling(window=short_window, min_periods=1, center=False).mean()

# Create long simple moving average over the long window
signals[‘long_mavg’] = aapl[‘Close’].rolling(window=long_window, min_periods=1, center=False).mean()

# Create signals
signals[‘signal’][short_window:] = np.where(signals[‘short_mavg’][short_window:] > signals[‘long_mavg’][short_window:], 1.0, 0.0)

# Generate trading orders
signals[‘positions’] = signals[‘signal’].diff()

# Print `signals`
print(signals)

In the object-oriented approach, you need to write the initialization and signal generation code just once. Then you can create new objects for each stock you want to compute the strategy for and call the method generate_signals() on them. The OOP code is very similar to the code above but uses self.

class MovingAverage():

    def __init__(self, symbol, bars, short_window, long_window):
        self.symbol = symbol
        self.bars = bars
        self.short_window = short_window
        self.long_window = long_window

    def generate_signals(self):
        signals = pd.DataFrame(index=self.bars.index)
        signals[‘signal’] = 0.0

        signals[‘short_mavg’] = bars[‘Close’].rolling(window=self.short_window, min_periods=1, center=False).mean()
        signals[‘long_mavg’] = bars[‘Close’].rolling(window=self.long_window, min_periods=1, center=False).mean()

        signals[‘signal’][self.short_window:] = np.where(signals[‘short_mavg’][self.short_window:] > signals[‘long_mavg’][self.short_window:], 1.0, 0.0)

        signals[‘positions’] = signals[‘signal’].diff()   

        return signals

Now you can simply instantiate an object with the desired parameters and generate signals for that object.

apple = MovingAverage(‘aapl’, aapl, 40, 100)
print(apple.generate_signals())

Doing this for other stocks becomes very easy; it’s just a matter of instantiating a new object with a different stock symbol.

microsoft = MovingAverage(‘msft’, msft, 40, 100)
print(microsoft.generate_signals())

Object-Oriented Programming in Python: Wrap Up

We have covered some of the main OOP concepts in Python. You now know how to declare classes and methods, instantiate objects, set attributes, and call instance methods. These skills will be useful in your future career as a data scientist.

With OOP, as programs grow, the complexity of code increases. There are various classes, subclasses, objects, inheritance, instance methods, and more. You need to structure your code properly and keep it readable. Following design patterns is a good way to do this. They represent a set of guidelines to avoid poor design. They address specific problems that frequently occur in Python OOP and provide solutions that can be reused. These OOP design patterns can be categorized into several groups, including creational patterns, structural patterns, and behavioral patterns. An example of a creational pattern is a singleton, which is used when you want to ensure that only one instance of a class can be created.