Skip to content

Commit b1a9fc3

Browse files
authored
Merge branch 'main' into main
2 parents 26b6691 + 22fb668 commit b1a9fc3

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

44 files changed

+1980
-14
lines changed

contrib/advanced-python/generators.md

Lines changed: 87 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,87 @@
1+
# Generators
2+
3+
## Introduction
4+
5+
Generators in Python are a sophisticated feature that enables the creation of iterators without the need to construct a full list in memory. They allow you to generate values on-the-fly, which is particularly beneficial for working with large datasets or infinite sequences. We will explore generators in depth, covering their types, mathematical formulation, advantages, disadvantages, and implementation examples.
6+
7+
## Function Generators
8+
9+
Function generators are created using the `yield` keyword within a function. When invoked, a function generator returns a generator iterator, allowing you to iterate over the values generated by the function.
10+
11+
### Mathematical Formulation
12+
13+
Function generators can be represented mathematically using set-builder notation. The general form is:
14+
15+
```
16+
{expression | variable in iterable, condition}
17+
```
18+
19+
Where:
20+
- `expression` is the expression to generate values.
21+
- `variable` is the variable used in the expression.
22+
- `iterable` is the sequence of values to iterate over.
23+
- `condition` is an optional condition that filters the values.
24+
25+
### Advantages of Function Generators
26+
27+
1. **Memory Efficiency**: Function generators produce values lazily, meaning they generate values only when needed, saving memory compared to constructing an entire sequence upfront.
28+
29+
2. **Lazy Evaluation**: Values are generated on-the-fly as they are consumed, leading to improved performance and reduced overhead, especially when dealing with large datasets.
30+
31+
3. **Infinite Sequences**: Function generators can represent infinite sequences, such as the Fibonacci sequence, allowing you to work with data streams of arbitrary length without consuming excessive memory.
32+
33+
### Disadvantages of Function Generators
34+
35+
1. **Single Iteration**: Once a function generator is exhausted, it cannot be reused. If you need to iterate over the sequence again, you'll have to create a new generator.
36+
37+
2. **Limited Random Access**: Function generators do not support random access like lists. They only allow sequential access, which might be a limitation depending on the use case.
38+
39+
### Implementation Example
40+
41+
```python
42+
def fibonacci():
43+
a, b = 0, 1
44+
while True:
45+
yield a
46+
a, b = b, a + b
47+
48+
# Usage
49+
fib_gen = fibonacci()
50+
for _ in range(10):
51+
print(next(fib_gen))
52+
```
53+
54+
## Generator Expressions
55+
56+
Generator expressions are similar to list comprehensions but return a generator object instead of a list. They offer a concise way to create generators without the need for a separate function.
57+
58+
### Mathematical Formulation
59+
60+
Generator expressions can also be represented mathematically using set-builder notation. The general form is the same as for function generators.
61+
62+
### Advantages of Generator Expressions
63+
64+
1. **Memory Efficiency**: Generator expressions produce values lazily, similar to function generators, resulting in memory savings.
65+
66+
2. **Lazy Evaluation**: Values are generated on-the-fly as they are consumed, providing improved performance and reduced overhead.
67+
68+
### Disadvantages of Generator Expressions
69+
70+
1. **Single Iteration**: Like function generators, once a generator expression is exhausted, it cannot be reused.
71+
72+
2. **Limited Random Access**: Generator expressions, similar to function generators, do not support random access.
73+
74+
### Implementation Example
75+
76+
```python
77+
# Generate squares of numbers from 0 to 9
78+
square_gen = (x**2 for x in range(10))
79+
80+
# Usage
81+
for num in square_gen:
82+
print(num)
83+
```
84+
85+
## Conclusion
86+
87+
Generators offer a powerful mechanism for creating iterators efficiently in Python. By understanding the differences between function generators and generator expressions, along with their mathematical formulation, advantages, and disadvantages, you can leverage them effectively in various scenarios. Whether you're dealing with large datasets or need to work with infinite sequences, generators provide a memory-efficient solution with lazy evaluation capabilities, contributing to more elegant and scalable code.

contrib/advanced-python/index.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
11
# List of sections
22

3-
- [OOPs](OOPs.md)
3+
- [OOPs](oops.md)
44
- [Decorators/\*args/**kwargs](decorator-kwargs-args.md)
55
- [Lambda Function](lambda-function.md)
66
- [Working with Dates & Times in Python](dates_and_times.md)
77
- [Regular Expressions in Python](regular_expressions.md)
88
- [JSON module](json-module.md)
99
- [Map Function](map-function.md)
10+
- [Protocols](protocols.md)
1011
- [Exception Handling in Python](exception-handling.md)
12+
- [Generators](generators.md)
File renamed without changes.

contrib/advanced-python/protocols.md

Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
# Protocols in Python
2+
Python can establish informal interfaces using protocols In order to improve code structure, reusability, and type checking. Protocols allow for progressive adoption and are more flexible than standard interfaces in other programming languages like JAVA, which are tight contracts that specify the methods and attributes a class must implement.
3+
4+
>Before going into depth of this topic let's understand another topic which is pre-requisite od this topic \#TypingModule
5+
6+
## Typing Module
7+
This is a module in python which provides
8+
1. Provides classes, functions, and type aliases.
9+
2. Allows adding type annotations to our code.
10+
3. Enhances code readability.
11+
4. Helps in catching errors early.
12+
13+
### Type Hints in Python:
14+
Type hints allow you to specify the expected data types of variables, function parameters, and return values. This can improve code readability and help with debugging.
15+
16+
Here is a simple function that adds two numbers:
17+
```python
18+
def add(a,b):
19+
return a + b
20+
add(10,20)
21+
```
22+
>Output: 30
23+
24+
While this works fine, adding type hints makes the code more understandable and serves as documentation:
25+
26+
```python
27+
def add(a:int, b:int)->int:
28+
return a + b
29+
print(add(1,10))
30+
```
31+
>Output: 11
32+
33+
In this version, `a` and `b` are expected to be integers, and the function is expected to return an integer. This makes the function's purpose and usage clearer.
34+
35+
#### let's see another example
36+
37+
The function given below takes an iterable (it can be any off list, tuple, dict, set, frozeset, String... etc) and print it's content in a single line along with it's type.
38+
39+
```python
40+
from typing import Iterable
41+
# type alias
42+
43+
def print_all(l: Iterable)->None:
44+
print(type(l),end=' ')
45+
for i in l:
46+
print(i,end=' ')
47+
print()
48+
49+
l = [1,2,3,4,5] # type: List[int]
50+
s = {1,2,3,4,5} # type: Set[int]
51+
t = (1,2,3,4,5) # type: Tuple[int]
52+
53+
for iter_obj in [l,s,t]:
54+
print_all(iter_obj)
55+
56+
```
57+
Output:
58+
> <class 'list'> 1 2 3 4 5
59+
> <class 'set'> 1 2 3 4 5
60+
> <class 'tuple'> 1 2 3 4 5
61+
62+
and now lets try calling the function `print_all` using a non-iterable object `int` as argument.
63+
64+
```python
65+
a = 10
66+
print_all(a) # This will raise an error
67+
```
68+
Output:
69+
>TypeError: 'int' object is not iterable
70+
71+
This error occurs because `a` is an `integer`, and the `integer` class does not have any methods or attributes that make it work like an iterable. In other words, the integer class does not conform to the `Iterable` protocol.
72+
73+
**Benefits of Type Hints**
74+
Using type hints helps in several ways:
75+
76+
1. **Error Detection**: Tools like mypy can catch type-related problems during development, decreasing runtime errors.
77+
2. **Code Readability**: Type hints serve as documentation, making it easy to comprehend what data types are anticipated and returned.
78+
3. **Improved Maintenance**: With unambiguous type expectations, maintaining and updating code becomes easier, especially in huge codebases.
79+
80+
Now that we have understood about type hints and typing module let's dive deep into protocols.
81+
82+
## Understanding Protocols
83+
84+
In Python, protocols define interfaces similar to Java interfaces. They let you specify methods and attributes that an object must implement without requiring inheritance from a base class. Protocols are part of the `typing` module and provide a way to enforce certain structures in your classes, enhancing type safety and code clarity.
85+
86+
### What is a Protocol?
87+
88+
A protocol specifies one or more method signatures that a class must implement to be considered as conforming to the protocol.
89+
This concept is often referred to as "structural subtyping" or "duck typing," meaning that if an object implements the required methods and attributes, it can be treated as an instance of the protocol.
90+
91+
Let's write our own protocol:
92+
93+
```python
94+
from typing import Protocol
95+
96+
# Define a Printable protocol
97+
class Printable(Protocol):
98+
def print(self) -> None:
99+
"""Print the object"""
100+
pass
101+
102+
# Book class implements the Printable protocol
103+
class Book:
104+
def __init__(self, title: str):
105+
self.title = title
106+
107+
def print(self) -> None:
108+
print(f"Book Title: {self.title}")
109+
110+
# print_object function takes a Printable object and calls its print method
111+
def print_object(obj: Printable) -> None:
112+
obj.print()
113+
114+
book = Book("Python Programming")
115+
print_object(book)
116+
```
117+
Output:
118+
> Book Title: Python Programming
119+
120+
In this example:
121+
122+
1. **Printable Protocol:** Defines an interface with a single method print.
123+
2. **Book Class:** Implements the Printable protocol by providing a print method.
124+
3. **print_object Function:** Accepts any object that conforms to the Printable protocol and calls its print method.
125+
126+
we got our output because the class `Book` confirms to the protocols `printable`.
127+
similarly When you pass an object to `print_object` that does not conform to the Printable protocol, an error will occur. This is because the object does not implement the required `print` method.
128+
Let's see an example:
129+
```python
130+
class Team:
131+
def huddle(self) -> None:
132+
print("Team Huddle")
133+
134+
c = Team()
135+
print_object(c) # This will raise an error
136+
```
137+
Output:
138+
>AttributeError: 'Team' object has no attribute 'print'
139+
140+
In this case:
141+
- The `Team` class has a `huddle` method but does not have a `print` method.
142+
- When `print_object` tries to call the `print` method on a `Team` instance, it raises an `AttributeError`.
143+
144+
> This is an important aspect of using protocols: they ensure that objects provide the necessary methods, leading to more predictable and reliable code.
145+
146+
**Ensuring Protocol Conformance**
147+
To avoid such errors, you need to ensure that any object passed to `print_object` implements the `Printable` protocol. Here's how you can modify the `Team` class to conform to the protocol:
148+
```python
149+
class Team:
150+
def __init__(self, name: str):
151+
self.name = name
152+
153+
def huddle(self) -> None:
154+
print("Team Huddle")
155+
156+
def print(self) -> None:
157+
print(f"Team Name: {self.name}")
158+
159+
c = Team("Dream Team")
160+
print_object(c)
161+
```
162+
Output:
163+
>Team Name: Dream Team
164+
165+
The `Team` class now implements the `print` method, conforming to the `Printable` protocol. and hence, no longer raises an error.
166+
167+
### Protocols and Inheritance:
168+
Protocols can also be used in combination with inheritance to create more complex interfaces.
169+
we can do that by following these steps:
170+
**Step 1 - Base protocol**: Define a base protocol that specifies a common set of methods and attributes.
171+
**Step 2 - Derived Protocols**: Create derives protocols that extends the base protocol with addition requirements
172+
**Step 3 - Polymorphism**: Objects can then conform to multiple protocols, allowing for Polymorphic behavior.
173+
174+
Let's see an example on this as well:
175+
176+
```python
177+
from typing import Protocol
178+
179+
# Base Protocols
180+
class Printable(Protocol):
181+
def print(self) -> None:
182+
"""Print the object"""
183+
pass
184+
185+
# Base Protocols-2
186+
class Serializable(Protocol):
187+
def serialize(self) -> str:
188+
pass
189+
190+
# Derived Protocol
191+
class PrintableAndSerializable(Printable, Serializable):
192+
pass
193+
194+
# class with implementation of both Printable and Serializable
195+
class Book_serialize:
196+
def __init__(self, title: str):
197+
self.title = title
198+
199+
def print(self) -> None:
200+
print(f"Book Title: {self.title}")
201+
202+
def serialize(self) -> None:
203+
print(f"serialize: {self.title}")
204+
205+
# function accepts the object which implements PrintableAndSerializable
206+
def test(obj: PrintableAndSerializable):
207+
obj.print()
208+
obj.serialize()
209+
210+
book = Book_serialize("lean-in")
211+
test(book)
212+
```
213+
Output:
214+
> Book Title: lean-in
215+
serialize: lean-in
216+
217+
In this example:
218+
219+
**Printable Protocol:** Specifies a `print` method.
220+
**Serializable Protocol:** Specifies a `serialize` method.
221+
**PrintableAndSerializable Protocol:** Combines both `Printable` and `Serializable`.
222+
**Book Class**: Implements both `print` and `serialize` methods, conforming to `PrintableAndSerializable`.
223+
**test Function:** Accepts any object that implements the `PrintableAndSerializable` protocol.
224+
225+
If you try to pass an object that does not conform to the `PrintableAndSerializable` protocol to the test function, it will raise an `error`. Let's see an example:
226+
227+
```python
228+
class Team:
229+
def huddle(self) -> None:
230+
print("Team Huddle")
231+
232+
c = Team()
233+
test(c) # This will raise an error
234+
```
235+
output:
236+
> AttributeError: 'Team' object has no attribute 'print'
237+
238+
In this case:
239+
The `Team` class has a `huddle` method but does not implement `print` or `serialize` methods.
240+
When test tries to call `print` and `serialize` on a `Team` instance, it raises an `AttributeError`.
241+
242+
**In Conclusion:**
243+
>Python protocols offer a versatile and powerful means of defining interfaces, encouraging the decoupling of code, improving readability, and facilitating static type checking. They are particularly handy for scenarios involving file-like objects, bespoke containers, and any case where you wish to enforce certain behaviors without requiring inheritance from a specific base class. Ensuring that classes conform to protocols reduces runtime problems and makes your code more robust and maintainable.

contrib/api-development/index.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
# List of sections
22

33
- [API Methods](api-methods.md)
4-
- [FastAPI](fast-api.md)
4+
- [FastAPI](fast-api.md)

contrib/ds-algorithms/index.md

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,6 @@
1010
- [Greedy Algorithms](greedy-algorithms.md)
1111
- [Dynamic Programming](dynamic-programming.md)
1212
- [Linked list](linked-list.md)
13+
- [Stacks in Python](stacks.md)
14+
- [Sliding Window Technique](sliding-window.md)
15+
- [Trie](trie.md)

0 commit comments

Comments
 (0)