Skip to content

Commit

Permalink
Fixing Typos, Examples and Explanations
Browse files Browse the repository at this point in the history
  • Loading branch information
estebansolo committed Aug 1, 2020
1 parent 1ffc2d5 commit 1dd4def
Show file tree
Hide file tree
Showing 18 changed files with 90 additions and 69 deletions.
29 changes: 22 additions & 7 deletions docs/07_lambdas.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
# Day 07 - Lambdas

Lambda functions are also known as anonymous functions, they can, like a normal function, take different arguments but lambdas are one line functions.
Lambda functions are also known as anonymous functions, they can, as a normal function, take different arguments and return a single value, but lambdas are one-line functions.

The syntax is as follows:

```python
lambda arguments: expresion
Expand All @@ -9,16 +11,24 @@ lambda arguments: expresion
## Example

```python
multiply = lambda a, b: a * b
print(multiply(5, 2))
def normal_multiply(num_a, num_b):
return num_a * num_b

lambda_multiply = lambda num_a, num_b: num_a * num_b

print(normal_multiply(5, 2))
print(lambda_multiply(5, 2))

# Output:
# 10
# 10
```

As you can see both `normal_multiply` and `lambda_multiply` functions, another difference you can notice is that **lambda** functions return a value without explicitly using the `return` keyword.

## Why use Lambda functions?

We can create functions that perform a simple action using **lambda functions**, so that we can make our code more compact and simple.
We can create functions that perform a simple actions or operations using **lambda functions**, so that we can make our code more compact and simple.

Let's try using the lambda functions with one of the previous examples.

Expand All @@ -29,15 +39,20 @@ def multiple(number):
return number % 2 == 0

numbers = range(1, 101)
print(list(filter(multiple, numbers)))
filter_numbers = filter(multiple, numbers)
print(list(filter_numbers))
```

### Using lambda function
#### Using lambda function

```python
numbers = range(1, 101)
print(list(filter(lambda number: number % 2 == 0, numbers)))
filter_numbers = filter(lambda number: number % 2 == 0, numbers)
print(list(filter_numbers))
```

When you understand the syntax of these functions, you will see that they are really easy to use and can be very useful.

[Go to the Challenge](https://github.com/estebansolo/Python30/blob/master/exercises/07_lambdas.py)

[Go to the Solution](https://github.com/estebansolo/Python30/blob/master/solutions/07_lambdas.py)
23 changes: 12 additions & 11 deletions docs/11_classmethod_static_method.md
Original file line number Diff line number Diff line change
@@ -1,18 +1,18 @@
# Day 11 - Classmethod and Staticmethod

We have not yet started to create our decorators but this time we will try to use two that comes with python, these are `staticmethod` and `classmethod`
We haven't talked about decorators yet, however this time we will use two which are provided by Python, these are `staticmethod` and `classmethod`.

In order to explain how to make use of a decorator right now, we must put it over the definition of the method/function with the prefix `@`.
In order to use the decorators we must know that these are put over the definition of the method or function with an `@` as a prefix.

The methods of a class are a kind of function which is defined in a class and can be of three types:
The methods are a kind of function which are defined in a class and can be of three types:

- [Instance Methods](#Instance%20Methods)
- [Class Methods](#Classmethod)
- [Static Methods](#Staticmethod)

## Instance Methods

Instance methods are the ones we usually create when working with classes, these methods must have an argument that is always the first one and as a convention, it is called **self**, these methods are part of the instance that we create.
Instance methods are the ones we usually create when working with classes, these methods must have an argument that is always the first one and as a convention, it is called **self**, these methods are part of the instance we create.

```python
class MyClass:
Expand All @@ -26,11 +26,11 @@ instance.instance_method()
# Instance Method Called
```

We define a method called `instance_method` which as we indicated must have `self` as the first parameter which refers to the instance created and we can use it to read or create attributes or use other methods in this instance of the class.
We defined a method called `instance_method` which as we indicated must have `self` as the first parameter which refers to the instance created and we can use it to read or create attributes or use other methods in this instance of the class.

## Classmethod

In order to define a class method, we must use the `classmethod` decorator. When working with instance methods, the method must have an argument which is self, which refers to the instance created, in this case, we will not define `self` because we cannot use anything related to the instance, however, we must use an argument which by convention is `cls`.
In order to define a class method, we must use the `classmethod` decorator. When working with instance methods, the method must have an argument which is self, in this case, we will not define `self` because we don't use anything related to the instance, however, we must use an argument which by convention is `cls`.

```python
class MyClass:
Expand All @@ -51,7 +51,7 @@ MyClass.class_method()

## Staticmethod

The static methods are defined the same as the class methods but this time the decorator `staticmethod` is used. Unlike the other methods, these do not receive any mandatory argument (besides that our logic requires).
Static methods are defined the same as the class methods but this time the decorator `staticmethod` is used. Unlike the other methods, these do not receive any mandatory argument (besides that our logic requires).

So these methods can not access attributes or other methods of the class or instance.

Expand All @@ -78,15 +78,15 @@ instance = MyClass()
# 2500
```

Static methods can be called from both instance and class.
Static methods can be called from both instance and class methods.

## What's it for?

We may never have used them, but we probably needed them.

The static methods are useful to separate the logic that doesn't have to make use of the instance or the class, and it' s also good practice to separate this kind of logic.
The static methods are useful to separate the logic that doesn't have to make use of the instance or the class, and it's also good practice to separate this kind of logic.

Let's see an example using both decorators
Let's see an example using both decorators:

```python
class FileManager:
Expand All @@ -109,7 +109,8 @@ instance_from_json = FileManager(json_content)
instance_from_csv = FileManager.from_csv("path/to/the/file")
```

Static method we use them for features that do not require the instance or the class, the class method allows us to create a new instance of a different format than the one the class receives when it is instantiated which is json.
Static method are used for features that do not require the instance or the class, the class method allows us to create a new instance of a different format than the one the class receives when it is instantiated which is json.

[Go to the Challenge](https://github.com/estebansolo/Python30/blob/master/exercises/11_classmethod_static_method.py)

[Go to the Solution](https://github.com/estebansolo/Python30/blob/master/solutions/11_classmethod_static_method.py)
11 changes: 6 additions & 5 deletions docs/12_property.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ In object-oriented programming, the getter and setter methods help us access pri
Although in Python we can't define methods or attributes as private, we can use a convention that indicates to other developers the level of accessibility of these.

- Protected: For protected attributes or methods, we must set an underscore before the definition. e.g. `self._name = "My name"`
- Private: Private attributes or methods have 2 underscores. e.g. `def __private_method(self):`
- Private: Private attributes or methods have two underscores. e.g. `def __private_method(self):`

## Getter / Setter

Expand All @@ -32,15 +32,15 @@ print(f"Name: {user.get_name()}")
# Name: Cristian
```

The traditional way or as it is usually done in other programming languages is using normal methods.
The traditional way or as it is normally used in other programming languages is to use normal methods.

This preamble is intended to give you an understanding of each of these levels and to help you understand how **Property** can help you create getters and setters in a Pythonic way.

## Property

`Property` is a built-in decorator that allows us to define the getter and setter.

Let's see with the same code as above how to use the property decorator.
Let's see with the same code as above how to use the property decorator:

```python
class User:
Expand Down Expand Up @@ -71,11 +71,11 @@ print(f"Name: {user.name}")
# Name: Cristian
```

As you can see, the property decorator did not change much in the way we do the getter/setter however this allows it to be more intuitive and easy to read each of its attributes, also like the methods defined above, allow us to control how we access the attributes of the class.
As you can see, the `property` decorator did not change much in the way we do the getter/setter, however this allows it to be more intuitive and easy to read, also like the methods defined above, allow us to control how we access the attributes of the class.

The property decorator is defined in a method that will return the value of the attribute, to define the setter we must use the property created, `@<property>.setter`.

We don't have to define each of these methods for each property, as we only do this for those we're going to access, or they can be read-only attributes so we'll only need the getters.
We don't have to define each of these methods for every property in our classes, as we only do this for those we're going to access, or if they can be read-only attributes we'll only need the getters.

Remember that we can implement these methods as we need them so that we won't affect the implementation of the code.

Expand Down Expand Up @@ -107,4 +107,5 @@ user.age = "26"
```

[Go to the Challenge](https://github.com/estebansolo/Python30/blob/master/exercises/12_property.py)

[Go to the Solution](https://github.com/estebansolo/Python30/blob/master/solutions/12_property.py)
13 changes: 7 additions & 6 deletions docs/13_dunder_methods.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

"Everything in Python is an object." You've probably heard this before, that's because it is, every data type we use is an object that has been enriched to increase its performance.

Dunder methods, or magic methods, are methods that we can define in our classes to give them more functionality than they have. These methods allow us to simulate the behavior of different data types from those we can find in the language.
Dunder methods, or magic methods, are methods we can define in our classes to give them more functionality than they have. These methods allow us to simulate the behavior of different data types from those we can find in the language.

The name dunder methods come from double underscores as a prefix and suffix of the method, e.g. `__init__`.

Expand All @@ -18,11 +18,11 @@ print(new_user)
# <__main__.User object at 0x7fafdc06cdd0>
```

I have created a basic User class which receives the name at the time of instantiation when printing this instance, we can see the address in memory although now it is not something we need to know, how about improving this?
I've created a basic class `User` which receives the name at the moment of its instantiation. When printing this instance, we can see the address in memory although now this is not what we need, how about improving this?

## Object Representation

**__repr__** is a method that allows us to define what information is going to represent our class, in this case, to see clear information of the created instance.
`__repr__` is a method that allows us to define what information is going to represent our class, in this case, to see clear information of the created instance.

```python
class User:
Expand All @@ -41,7 +41,7 @@ print(new_user)

## Count

You have probably already used the `len()` function more than once, but did you know that we can also implement it on our objects?
You have probably used the `len()` function more than once, but did you know we can also implement this in our objects?

```python
class User:
Expand All @@ -63,11 +63,12 @@ print(len(new_user))
# 11
```

The `len()` function allows us to know the number of characters in a string, however, we can use the magic method __len__ to know from our object the number of characters that our User represents.
The `len()` function allows us to know the number of characters in a string, however, we can use the magic method `__len__` to know from our object the number of characters that our `User` represents.

## More

There are quite a few methods that we can probably use more than we need, but we can practice with each one of them and enrich our code like never before. [Dunder Methods](https://docs.python.org/3/reference/datamodel.html)
There are quite a few methods that we probably won't need, however, we can practice with each of them to enrich our code like never before. [Dunder Methods](https://docs.python.org/3/reference/datamodel.html)

[Go to the Challenge](https://github.com/estebansolo/Python30/blob/master/exercises/13_dunder_methods.py)

[Go to the Solution](https://github.com/estebansolo/Python30/blob/master/solutions/13_dunder_methods.py)
7 changes: 4 additions & 3 deletions docs/14_operator_overloading.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ print(36 - 23)
# 13
```

Commonly, we think about comparing or making numerical calculations since it is something basic that we know since we were children, but this type of comparison can be performed with any class using some magic methods.
Usually, we think about comparing or making numerical calculations since it is something basic that we know since we were children, but this type of comparison can be performed with any class using some magic methods.

For now we will create a Product class with some of its basic attributes.
For now we will create a `Product` class with some of its basic attributes:

```python
class Product:
Expand Down Expand Up @@ -87,7 +87,7 @@ print(pizza == hotdog)

These magic methods allow us to compare our items, in my case I had their prices compared, however, you can define these methods as you need them.

As you can notice the objects we are comparing, they receive as an argument another one which relates to the value with which we are going to make the comparison.
These methods receive as an argument another one, which is related to the value with which we are going to make the comparison.

## Operations

Expand Down Expand Up @@ -118,4 +118,5 @@ print(total)
```

[Go to the Challenge](https://github.com/estebansolo/Python30/blob/master/exercises/14_operator_overloading.py)

[Go to the Solution](https://github.com/estebansolo/Python30/blob/master/solutions/14_operator_overloading.py)
20 changes: 9 additions & 11 deletions docs/15_else.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Day 15 - Else

As you can see from ***Error Handling***, we can use the 'else' clause not only with conditionals, in this case we'll see that we can use this clause with other statements.
As you saw in ***Error Handling***, we can use the `else` clause not only with conditionals, in this case we'll see that we can use this clause with other statements.

## For/Else

Expand All @@ -10,10 +10,10 @@ Before we explain how we can include the else, we'll create a loop like the one
numbers = range(10, 21)
for number in numbers:
print(number)

# The previous loop will print the numbers from 10 to 20.
```

The previous loop will print the numbers from 10 to 20.

If you are familiar with loops, you will know that the **break** sentence will interrupt the execution and continue outside of it.

```python
Expand All @@ -22,12 +22,10 @@ for number in numbers:
if number == 17:
break
print(number)

# The previous loop will print the numbers from 10 to 16, when the
# number 17 continues, the conditional will validate it and the loop
# will be interrupted, so the execution will continue.
```

The previous loop will print the numbers from 10 to 16, when the number 17 continues, the conditional will validate it and the loop will be interrupted, so the execution will continue.

However, there are cases when we are going through some kind of data we will realize that the loop ended without finding a **break**, in these cases is when we can make use of the `else` clause.

```python
Expand All @@ -37,14 +35,13 @@ for number in numbers:
break
else:
print("Number 60 was not found")

# The else clause is executed when a loop is not interrupted,
# and will only execute the block of code when the loop is over.
```

The else clause is executed when a loop is not interrupted, and will only execute the block of code when the loop is over.

## While/Else

The while statement also allows the use of the `else` clause, and like the for loop it will only be executed if no break is found in the while block.
The while statement also allows the use of the `else` clause, and like the for loop, it will only be executed if no break is found in the while block.

```python
while conditional:
Expand All @@ -54,4 +51,5 @@ else:
```

[Go to the Challenge](https://github.com/estebansolo/Python30/blob/master/exercises/15_else.py)

[Go to the Solution](https://github.com/estebansolo/Python30/blob/master/solutions/15_else.py)
7 changes: 4 additions & 3 deletions docs/16_recursion.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
# Day 16 - Recursion

Recursive functions are created when a function calls itself, this process will repeat indefinitely if we don't stop it in some condition, and only when it concludes, it will return a value to the place where it started its process.
Recursive functions are created when a function calls itself, this process will repeat indefinitely if we don't stop it at some point, and only when it concludes, it will return a value to the place where it started the process.

We can turn any loop into a recursion.
**We can turn any loop into a recursion.**

```python
total = 0
Expand Down Expand Up @@ -32,7 +32,7 @@ print(f"The sum of the numbers from 1 to {times} is equals to {total}")
# The sum of the numbers from 1 to 5 is equals to 15
```

the above function is a basic example of what we can do, it starts by receiving the number 5 as an argument.
The above function is a basic example of what we can do, it starts by receiving the number 5 as an argument.

The first condition is used to end the recursion once the value of the argument is 1 and it will go in a chain by returning the total and adding it to the previous number. In other words:

Expand All @@ -59,4 +59,5 @@ The first condition is used to end the recursion once the value of the argument
When we call the function for the first time, a value is passed to it and it calls itself but passing a lower value and so on until the end (when the value is 1 and therefore it stops making more calls). At that point they start to return. The returned values are added to the original parameter in each one of the iterations until arriving at the beginning of everything in which the first call returns the final value.

[Go to the Challenge](https://github.com/estebansolo/Python30/blob/master/exercises/16_recursion.py)

[Go to the Solution](https://github.com/estebansolo/Python30/blob/master/solutions/16_recursion.py)
5 changes: 4 additions & 1 deletion docs/17_memoization.md
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ timeit("fibonacci(35)", setup="from __main__ import fibonacci", number=1)

This process was performed once and took about 4 seconds, if you can see it is a recursive function which is called itself several times per execution and sometimes performs the same operation for the same arguments.

This process can be optimized because we will always get the same result if we pass the same argument, in other words, it is a pure function.
This process can be optimized because we will always get the same result if we pass the same argument, in other words, it is a **pure function**.

```python
from timeit import timeit
Expand All @@ -43,12 +43,15 @@ history = {}
def fibonacci(n):
if n in history:
return history[n]

if n == 0:
return 0
elif n == 1:
return 1

fibonacci_result = fibonacci(n - 1) + fibonacci(n - 2)
history[n] = fibonacci_result

return fibonacci_result

timeit("fibonacci(35)", setup="from __main__ import fibonacci", number=1)
Expand Down

0 comments on commit 1dd4def

Please sign in to comment.