Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

chapter 3 and 4 changes done #65

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions docs/chapter-3/lesson-3.2.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ for i in range(5):

The output is:

``` linenums="1"
```
0
1
2
Expand Down Expand Up @@ -53,7 +53,7 @@ for i in range(10, 100, 2):
print(i)
```

`range(10, 100, 2)` represents the sequence `10, 12, ..., 98`. In general, `range(start, stop, step)` represents the sequence `start, start + step, start + 2 * step, ..., last`, where `last` is the largest element in this sequence that is less than `stop`. This is true when the `step` parameter is positive.
`#!py range(10, 100, 2)` represents the sequence $10, 12, ..., 98$. In general, `#!py range(start, stop, step)` represents the sequence `start, start + step, start + 2 * step, ..., last` where `last` is the largest element in this sequence that is less than `stop`. This is true when the `step` parameter is positive.

The following are equivalent:

Expand Down Expand Up @@ -86,7 +86,7 @@ for i in range(10, 5):

The point to note is that neither of these code snippets produces any error. Finally, try executing the following snippet and observe the output.

```python
```python linenums="1"
##### Alarm! Wrong code snippet! #####
for i in range(0.0, 10.0):
print(i)
Expand All @@ -97,7 +97,7 @@ for i in range(0.0, 10.0):

### Iterating through Strings

Since a string is a sequence of characters, we can use the `#!py for` loop to iterate through strings. The following code will print each character of the string `x` in one line:
Since a string is a sequence of characters, we can use the `#!py for` loop to iterate through strings. The following code will print each character of the string `x` in a separate line:

```python linenums="1"
word = 'good'
Expand All @@ -107,7 +107,7 @@ for char in word:

The output is:

``` linenums="1"
```
g
o
o
Expand All @@ -126,7 +126,7 @@ for char in word:

The output is:

``` linenums="1"
```
g occurs at position 1 in the string good
o occurs at position 2 in the string good
o occurs at position 3 in the string good
Expand Down
34 changes: 18 additions & 16 deletions docs/chapter-3/lesson-3.3.md
Original file line number Diff line number Diff line change
Expand Up @@ -46,24 +46,24 @@ The basic idea behind the solution is as follows:
- The outer for loop goes through each element in the sequence $2, 3, ..., n$. `i` is the loop variable for this sequence.
- We begin with the guess that `i` is prime. In code, we do this by setting `flag` to be `True`.
- Now, we go through all potential divisors of `i`. This is represented by the sequence $2, 3, ..., i - 1$. Variable `j` is the loop variable for this sequence. Notice how the sequence for the inner loop is dependent on `i`, the loop variable for the outer loop.
- If `j` divides `i`, then `i` cannot be a prime. We correct our initial assumption by updating `flag` to `False` whenever this happens. As we know that `i` is not prime, there is no use of continuing with the inner-loop, so we break out of it.
- If `j` divides `i` at some point, then `i` cannot be a prime. We correct our initial assumption by updating `flag` to `False` whenever this happens. As we know that `i` is not prime, there is no use of continuing with the inner-loop, so we break out of it.
aravinds-arv marked this conversation as resolved.
Show resolved Hide resolved
- If `j` doesn't divide `i` for any `j` in this sequence, then `i` is a prime. In such a situation, our initial assumption is right, and `flag` stays `True`.
- Once we are outside the inner-loop, we check if `flag` is `True`. if that is the case, then we increment count as we have hit upon a prime number.
- Once we are outside the inner-loop, we check if `flag` is `True`. If this is the case, then we increment count as we have hit upon a prime number.

Some important points regarding nested loops:

- Nesting is not restricted to `#!py for` loops. Any one of the following combinations is possible:
- `#!py for` inside `#!py for`
- `#!py for` inside `#!py while`
- `#!py while` inside `#!py while`
- `#!py while` inside `#!py for`
- `#!py for` inside `#!py for`
- `#!py for` inside `#!py while`
- `#!py while` inside `#!py while`
- `#!py while` inside `#!py for`
- Multiple levels of nesting is possible.



## `#!py while` versus `#!py for`

`#!py for` loops are typically used in situations where the number of iterations can be quantified, whereas `#!py while` loops are used in situations where the number of iterations cannot be quantified exactly. This doesn't mean that the number of iterations in a `#!py for` loop is always constant. For example:
`#!py for` loops are typically used in situations where the number of iterations can be quantified and is known in advance, whereas `#!py while` loops are used in situations where the number of iterations cannot be quantified exactly. This doesn't mean that the number of iterations in a `#!py for` loop is always constant. For example:
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think it is necessary to know the number of iterations in advance to use a for loop. For example,

for i in range(int(input())):
    print(i)

I think the entire paragraph doesn't really apply to python but rather to some other language like C or Java. In python, for loops are used to iterate over sequences such as lists, sets, generators, strings, etc., whereas while loops are used to execute a block of code while a condition is true.

Copy link
Contributor Author

@aravinds-arv aravinds-arv Dec 9, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually what I believe is being implied in this paragraph is that in for loops, the number of times the code block is run can be determined in advance. Even in your example we know that the body of for only needs to executed as much number of times as the input provided. And in case of iterable objects like lists, sets etc the for loop is run exactly as many times as the length of the object.

while on the other is used commonly in cases where we don't have this knowledge in advance.. as you said we just know that it needs to be executed as long as some condition stays True.

Many programming tutorials I've seen also introduce the use of while and for using the same idea. Technicalities and implementation of loops might vary across languages but this basic idea I think applies to all. What say?


```python linenums="1"
n = int(input())
Expand All @@ -83,7 +83,7 @@ The number of iterations in the above code can be determined only after it termi



## print: `end`, `sep`
## `#!py print:` `end`,`sep`

### `end`

Expand All @@ -98,17 +98,17 @@ For a given value of `n`, say `n` = 9, we want the output to be:
1,2,3,4,5,6,7,8,9
```

The following solution won't work:
Here's an attempt at solving this using the concepts learnt so far

```python linenums="1"
n = int(input())
for i in range(1, n + 1):
print(i, ',')
```

For `n` = 9, this will give the following output:
For `n` = 9, this will give the following output, certainly not what we need:

``` linenums="1"
```
1 ,
2 ,
3 ,
Expand All @@ -135,7 +135,9 @@ For `n` = 9, this will give the required output:
1,2,3,4,5,6,7,8,9
```

Whenever we use the `#!py print()` function, it prints the expression passed to it and immediately follows it up by printing a newline. This is the default behaviour of `#!py print()`. It can be altered by using a special argument called `end`. The default value of `end` is set to the newline character. So, whenever the end argument is not explicitly specified in the print function, a newline is appended to the input expression by default. In the code given above, by setting `end` to be a comma, we are forcing the `#!py print()` function to insert a comma instead of a newline at the end of the expression passed to it. It is called `end` because it is added at the end. To get a better picture, consider the following code:
Whenever we use the `#!py print()` function, it prints the expression passed to it and immediately follows it up by printing a newline. This is the default behaviour of `#!py print()`. It can be altered by using a special argument called `end`. The default value of `end` is set to the newline character `#!py \n`[^1]. So, whenever the end argument is not explicitly specified in the print function, a newline is appended to the input expression by default. In the code given above, by setting `end` to be a comma, we are forcing the `#!py print()` function to insert a comma instead of a newline at the end of the expression passed to it. It is called `end` because it is added at the end. To get a better picture, consider the following code:
aravinds-arv marked this conversation as resolved.
Show resolved Hide resolved

[^1]: Remember [escape characters](../chapter-1/lesson-1.5.md/#escape-characters) from chapter 1?

```python linenums="1"
print()
Expand All @@ -148,13 +150,13 @@ print(3, end = ',')

This output is:

``` linenums="1"

```
,1
1,2,3,
```

Even though nothing is being passed to the print function in the first line of code, the first line in the output is a newline because the default value of `end` is a newline character (`'\n'`). No expression is passed as input to print in the second line of code as well, but `end` is set to `,`. So, only a comma is printed. Notice that line 3 of the code is printed in line 2 of the output. This is because `end` was set to `,` instead of the newline character in line 2 of the code.
Even though nothing is being passed to the print function in the first line of code, the first line in the output is a newline because the default value of `end` is the newline character `\n`. No expression is passed as input to print in the second line of code either, but `end` is set to `,`. So, only a comma is printed. Notice that line 3 of the code is printed in line 2 of the output. This is because `end` was set to `,` instead of the newline character in line 2 of the code.



Expand All @@ -172,7 +174,7 @@ The output is:
this is cool
```

What if we do not want the space or if want some other separator? This can be done using `sep`:
What if we don't want the space or if want some other separator? This can be done using the `sep` arguement:

```python
print('this', 'is', 'cool', sep = ',')
Expand Down
29 changes: 14 additions & 15 deletions docs/chapter-3/lesson-3.4.md
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,7 @@ for i in range(1, 11):

For an input of 3, this will give the following result:

``` linenums="1"
```
Multiplication table for 3
3 X 1 = 3
3 X 2 = 6
Expand All @@ -106,7 +106,7 @@ The `\t` is a tab character. It has been added before and after the `=`. Remove

Till now we have passed f-strings to the `#!py print()` function. Nothing stops us from using it to define other string variables:

```python
```python linenums="1"
name = input()
qual = input()
gender = input()
Expand All @@ -125,14 +125,14 @@ Try to guess what this code is doing.

Another way to format strings is using a string method called `#!py format()`.

```python
```python linenums="1"
name = input()
print('Hi, {}!'.format(name))
```

In the above string, the curly braces will be replaced by the value of the variable `name`. Another example:

```python
```python linenums="1"
l, b = int(input()), int(input())
print('The length of the rectangle is {} units'.format(l))
print('The breadth of the rectangle is {} units'.format(b))
Expand All @@ -157,7 +157,7 @@ fruit2 = 'banana'
print('{} and {} are fruits'.format(fruit1, fruit2))
```

In this code, the mapping is implicit. The first pair of curly braces is mapped to the first argument and so on. This can be made explicit by specifying which argument a particular curly braces will be mapped to:
In this code, the mapping is implicit. The first pair of curly braces is mapped to the first argument and so on. This can be made explicit by specifying which argument a particular pair of curly braces will be mapped to:

```python linenums="1"
fruit1 = 'apple'
Expand Down Expand Up @@ -208,12 +208,12 @@ The value of pi is approximately 3.14
Let us look at the content inside the curly braces: `{pi_approx:.2f}`. The first part before the `:` is the variable. Nothing new here. The part after `:` is called a format specifier. `.2f` means the following:

- `.` - this signifies the decimal point.
- `2` - since this comes after the decimal point, it stipulates that there should be exactly two numbers after the decimal point. In other words, the value (`pi_approx`) should be rounded off to two decimal places.
- `2` - since this comes after the decimal point, it stipulates that there should be exactly two numbers after the decimal point. In other words, the value `pi_approx` should be rounded off to two decimal places.
- `f` - this signifies that we are dealing with a `float` value.

Let us consider a variant of this code:

```python
```python linenums="1"
pi_approx = 22 / 7
print(f'The value of pi is approximately {pi_approx:.3f}')
```
Expand Down Expand Up @@ -243,7 +243,7 @@ BSC1002: 100
BSC1003: 90.15
```

While this is not bad, we would like the marks to be right aligned and have a uniform representation for the marks. The following code helps us achieve this:This is what we wish to see:
While this is not bad, it would look a tad bit cleaner if we could get the marks to be right aligned. The following code helps us achieve this:

```python linenums="1"
roll_1, marks_1 = 'BSC1001', 90.5
Expand All @@ -256,18 +256,16 @@ print(f'{roll_3}: {marks_3:10.2f}')

The output of the above code will be:

``` linenums="1"
```
BSC1001: 90.50
BSC1002: 100.00
BSC1003: 90.15
```

This is much more neater.

As seen above we get a number of spaces before the floating point values and the values themselves are now right aligned. Let's take a closer look at the contents of the second pair of curly braces: `{marks_1:10.2f}`. The part before the `:` as before is the variable. The part after the `:` is `10.2f`. Here again, `.2f` signifies that the float value should be rounded off to two decimal places. The `10` before the decimal point is the minimum width of the column used for printing this value. If the number has fewer than 10 characters (including the decimal point), this will be compensated by adding spaces before the number.

The part that might be confusing is the second curly braces in each of the print statements. Let us take a closer look: `{marks_1:10.2f}`. The part before the `:` is the variable. The part after the `:` is `10.2f`. Here again, `.2f` signifies that the float value should be rounded off to two decimal places. The `10` before the decimal point is the minimum width of the column used for printing this value. If the number has fewer than 10 characters (including the decimal point), this will be compensated by adding spaces before the number.

For a better understanding of this concept, let us turn to printing integers with a specific formatting. This time, we will use the `#!py format()` function:
For a better understanding of this concept, let us turn to printing integers with a specific formatting. This time, we will use the `#!py format()` method:

```python linenums="1"
print('{0:5d}'.format(1))
Expand All @@ -280,7 +278,7 @@ print('{:5d}'.format(111111))

This gives the following output:

``` linenums="1"
```
1
11
111
Expand All @@ -295,5 +293,6 @@ Points to note in the code:
- First three print statements have the index of the argument — `0` in this case — before the `:`. Last three statements do not have the index of the argument. In fact there is nothing before the `:`. Both representations are valid.
- The `5d` after the `:` means that the width of the column used for printing must be at least 5.
- Lines 1 to 4 have spaces before them as the integer being printed has fewer than five characters.

- Line 5 has exactly five characters, leaving no empty room for spaces.
- And line 6 obviously has more characters than the specified minimum width and thus is printed as such.

27 changes: 17 additions & 10 deletions docs/chapter-3/lesson-3.5.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Lesson-3.5

## Library
## Library (Continued)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey, can you tell me why you added "continued" in parantheses?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There was a library lesson in some earlier chapter too where some other library was introduced.. so I thought we had to convey to the readers that this isn't the same lesson. Should I get rid of it 🫤? Or maybe we could do something like:

  • Library - Part 1
  • Library - Part 2


We will look at two more libraries — `math` and `random` — and use them to solve some fascinating problems in mathematics.

Expand All @@ -24,7 +24,7 @@ for n in range(1, 6):

If we execute the above code, we get the following output:

``` linenums="1"
```
n = 1, x_n = 1.414
n = 2, x_n = 1.848
n = 3, x_n = 1.962
Expand Down Expand Up @@ -54,7 +54,7 @@ for n in range(1, 20):
print(x)
```

After just 20 iterations, the value is so close to two: `#!py 1.9999999999910236`. But we have used trial and error to decide when to terminate the iteration. A better way to do this is to define a tolerance: if the difference between the previous value and the current value in the sequence is less than some predefined value (tolerance), then we terminate the iteration.
After just 20 iterations, the value is so close to two: `#!py 1.9999999999910236`. But we have used trial and error to decide when to terminate the iteration. A better way to do this is to define a _tolerance_: if the difference between the previous value and the current value in the sequence is less than some predefined value (tolerance), then we terminate the iteration.

```python linenums="1"
import math
Expand All @@ -64,11 +64,16 @@ while abs(x_curr - x_prev) >= tol:
x_prev = x_curr
x_curr = math.sqrt(2 + x_prev)
count += 1
print(f'Value of x at {tol} tolerance is {x_curr}')
print(f'Value of x at {tol:.5f} tolerance is {x_curr}')
print(f'It took {count} iterations')
```

The output of the above code would be:

```
Value of x at 0.00001 tolerance is 1.9999976469034038
It took 9 iterations
```

### `random`

Expand All @@ -79,11 +84,13 @@ import random
print(random.choice('HT'))
```

That is all there is to it! `random` is a library and `#!py choice()` is a function defined in it. It accepts any sequence as input and returns an element chosen at random from this sequence. In this case, the input is a string, which is nothing but a sequence of characters.
That is all there is to it! `random` is a library and `#!py choice()` is a function defined in it. It accepts any sequence as input and returns an element chosen at random from this sequence. In this case, the input is a string, which is nothing but a sequence of characters and each time the code is run we get either of these characters, `H` or `T`.

We know that the probability of obtaining a head on a coin toss is 0.5. This is the theory. Is there a way to see this rule in action? Can we computationally verify if this is indeed the case? For that, we have to set up the following experiment. Toss a coin $n$ times and count the number of heads. Dividing the total number of heads by $n$ will give the empirical probability. As $n$ becomes large, this probability must approach 0.5.
We know that the probability of obtaining a head on a coin toss is 0.5. Atleast that's what theory says. Is there a way to see this rule in action? Can we computationally verify if this is indeed the case? For this, we need to set up an experiment: toss a coin $n$ times and count the number of heads. Dividing the total number of heads by $n$ will give the _empirical probability[^1]_. As $n$ becomes larger and larger, this probability must approach 0.5.

```python
[^1]: Simply put [empirical probability](https://en.wikipedia.org/wiki/Empirical_probability) gives the likelihood of an event to occur based on past or historical data.

```python linenums="1"
import random
n = int(input())
heads = 0
Expand All @@ -107,12 +114,12 @@ Let us run the above code for different values of $n$ and tabulate our results:
| 1,000,000 | 0.499983 |
</div>

The value is approaching `#!py 0.5` as expected! `random` is quite versatile.
The value is indeed approaching `#!py 0.5` as expected!

<!-- Replace this code block with a repl -->

!!! question "Exercise"
Let us now roll a dice! `randint(a, b)` returns a random integer $N$ such that $a \leq N \leq b$.
!!! question "Practice Problem"
Let us now try to simulate a die roll! `random.randint(a, b)` returns a random integer $N$ such that $a \leq N \leq b$.

```python
import random
Expand Down
Loading