├── .DS_Store
├── README.md
├── images
├── .DS_Store
├── beans.jpg
├── bigo.png
├── bigographs.png
├── fibo-2.JPG
├── fibo.JPG
├── indir.jpeg
└── words-search.jpg
├── markdowns
├── big-oooh.md
├── bit-manipulation.md
├── dynamic-programming.md
├── oop-classes-objects.md
├── oop-inheritance.md
└── recursion.md
└── notebooks
├── .ipynb_checkpoints
├── oop-classes-objects-checkpoint.ipynb
├── oop-inheritance-checkpoint.ipynb
└── regex-checkpoint.ipynb
├── oop-classes-objects.ipynb
└── oop-inheritance.ipynb
/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nyandwi/PythonPro/a8bc45c498de98e02e472b4120de742afe25d658/.DS_Store
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Python Pro Features
2 |
3 | *[**Note**: Ongoing work, one commit at time. I would like creating my own version of all topics mentioned in below outline, but I due to other commitments, I can't promise I will. If I am unable to create content for a given topic, I will link some external good resources. I also compiled resources for further learning(see further learning). Feedback or suggestion for other good Python resources are welcome]*
4 |
5 | Python is a popular programming language that emphasizes efficiency, readability and simplicity ahead of anything. Due to its rich standard library, it has been called "a battery included" language.
6 |
7 | This repository is a home of Python most advanced concepts and features. It's an extension of a former repository of [Python basics](https://github.com/Nyandwi/PythonBasics).
8 |
9 | ## Outline
10 |
11 | * Basics Beyond Basics:
12 | * [Recursion](markdowns/recursion.md)
13 | * [Dynamic Programming](markdowns/dynamic-programming.md)
14 | * [Bit Manipulation](markdowns/bit-manipulation.md)
15 | * [Measuring and Understanding Program Efficiency with Big-O](markdowns/big-oooh.md)
16 | * Code Formatting
17 | * Writing Pythonic Code: Tricks and Know How to's
18 | * Decorators
19 | * Generators
20 | * Itertools
21 | * Collections
22 | * OOP - Classes and Objects: [notebook](notebooks/oop-classes-objects.ipynb), [markdown](markdowns/oop-classes-objects.md)
23 | * OOP - Inheritance: [notebook](notebooks/oop-inheritance.ipynb), [markdown](markdowns/oop-inheritance.md)
24 | * OOP - Magic Methods: [article](https://rszalski.github.io/magicmethods/)
25 | * Logging
26 | * Threading
27 | * Testing, Debugging, Exceptions, and Assertions: [video](https://www.youtube.com/watch?v=9H6muyZjms0&t=2s), [slides](https://ocw.mit.edu/courses/electrical-engineering-and-computer-science/6-0001-introduction-to-computer-science-and-programming-in-python-fall-2016/lecture-slides-code/MIT6_0001F16_Lec7.pdf)
28 | * Regular Expressions: [part 1](https://realpython.com/regex-python/), [part 2](https://realpython.com/regex-python-part-2/)
29 | * Python Modules
30 | * Working with:
31 | * Files and Command Line
32 | * Json
33 | * Google Sheets
34 | * PDF
35 |
36 |
37 | ## Further Learning
38 |
39 | - [MIT Introduction to Computer Science and Programming in Python](https://ocw.mit.edu/courses/electrical-engineering-and-computer-science/6-0001-introduction-to-computer-science-and-programming-in-python-fall-2016/index.htm)
40 | - [Automate the Boring Stuff with Python by AI Sweigart](https://automatetheboringstuff.com)
41 | - [Beyond the Basic Stuff with Python by AI Sweigart](http://inventwithpython.com/beyond/)
42 | - [Awesome Python Articles by Real Python](https://realpython.com)
43 |
44 | Thanks to Symon for suggesting some concepts to cover in this repository.
45 |
--------------------------------------------------------------------------------
/images/.DS_Store:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nyandwi/PythonPro/a8bc45c498de98e02e472b4120de742afe25d658/images/.DS_Store
--------------------------------------------------------------------------------
/images/beans.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nyandwi/PythonPro/a8bc45c498de98e02e472b4120de742afe25d658/images/beans.jpg
--------------------------------------------------------------------------------
/images/bigo.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nyandwi/PythonPro/a8bc45c498de98e02e472b4120de742afe25d658/images/bigo.png
--------------------------------------------------------------------------------
/images/bigographs.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nyandwi/PythonPro/a8bc45c498de98e02e472b4120de742afe25d658/images/bigographs.png
--------------------------------------------------------------------------------
/images/fibo-2.JPG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nyandwi/PythonPro/a8bc45c498de98e02e472b4120de742afe25d658/images/fibo-2.JPG
--------------------------------------------------------------------------------
/images/fibo.JPG:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nyandwi/PythonPro/a8bc45c498de98e02e472b4120de742afe25d658/images/fibo.JPG
--------------------------------------------------------------------------------
/images/indir.jpeg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nyandwi/PythonPro/a8bc45c498de98e02e472b4120de742afe25d658/images/indir.jpeg
--------------------------------------------------------------------------------
/images/words-search.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Nyandwi/PythonPro/a8bc45c498de98e02e472b4120de742afe25d658/images/words-search.jpg
--------------------------------------------------------------------------------
/markdowns/big-oooh.md:
--------------------------------------------------------------------------------
1 | # Measuring Algorithm Efficiency with Big-O
2 |
3 | *******
4 |
5 | *Content:*
6 | * [Introduction](#1)
7 | * [Measuring Program Efficiency with Timer and Counting Operations](#2)
8 | * [The order of Growths](#3)
9 | * [Python Time Complexity](#4)
10 | * [Summary and Tricks](#5)
11 | * [References and Further Learning](#6)
12 |
13 | *******
14 |
15 | ### Introduction
16 |
17 | * Big Oh or Big-O time is a metric used to measure the efficiency of the algorithms.
18 | * Big-O is also referred to as `algorithmic complexity`, `time complexity`, `space complexity`, and `asymptotic complexity`.
19 | * As Ned Batchelder said, Big-O is measuring *"`how code slows as data grows.` Big-O isn’t about how fast your code goes at any particular time. It’s about how the running time changes as the data size changes."*
20 | * We will use Ned talk about Big-O in some part of this tutorial. You can watch the whole talk [here](https://www.youtube.com/watch?v=duvZ-2UK0fc).
21 |
22 | * To develop faster algorithms, it is important to understand Big O.
23 | * Big O is a notation that describes the order of growth of an algorithm. The lower the order of growth, the better.
24 | * Measuring program efficiency is one of the hardest challenges in solving computational problems. Programs can be implemented in different ways, with different algorithm, and can run on different machines. How one can measure the time efficieny of the algorithm regardless of the approach, type algorithm and machine running the algorithm?
25 | * In addition to Big O, there are other two techniques for measuring the efficiency of the programs which are `timer` and `counting the operations`. Before we talk about Big O, let's see the downsides of those two techniques.
26 |
27 |
28 | ## Measuring Program Efficiency with Timer and Counting Operations
29 |
30 | ### Using Timer
31 |
32 | * Measuring the program efficiency using `timer` means timing the program from the beggining of execution to the end of program execution.
33 | * Python has two modules `time` and `timeit` that can be used to measure the runtime: the amount of time it takes to run a program.
34 | * As you can imagine, the fastest programs has low runtime. The higher the runtime, the slower the program.
35 | * With `time` module, you start the time, run the program, and stop the clock.
36 | * See the code below:
37 |
38 | ******************
39 |
40 | ```python
41 | import time
42 |
43 | def add_numbers(L):
44 | """
45 | Add all numbers in a list
46 | and return sum
47 | """
48 |
49 | num_sum = 0
50 | for i in L:
51 | num_sum += i
52 | return num_sum
53 |
54 | # Timing the program
55 |
56 | t0 = time.time_ns()
57 | num_sum = add_numbers([1,2,3,4,5,6,7,8,9,10])
58 | t1 = time.time_ns() - t0
59 | print(f"Solution: {num_sum}, function runtime in nanoseconds: {t1}ns")
60 | # Output: num_sum = 55, function runtime in nanoseconds: 34000ns
61 | # Time changes at every run!!
62 | ```
63 | ******************
64 |
65 | * Measuring the program efficiency with timer can not really tell us much about the efficiency of the program. Time varies for different inputs but it can not express the relationship between the inputs and time it takes to run the program.
66 |
67 | * Also, timing evaluates the implementation and the capacity of the machine running the program.
68 |
69 | * We need to find a better way of measuring the efficiency of the algorithms regardless of implementation and machines, but the size of the input data and the scalability.
70 | * You can also use `cProfiler` to measure the time in more nice way.
71 |
72 | ### Counting the Number of Operations
73 |
74 | * Most operations such as mathematical, comparison, assignment take constant time.
75 | * For example, assignment operator (`num = 0`) is one operation, `2*3 + 4` is two operations(addition and multiplication).
76 |
77 | * Similar to timing the program execution, operation counts is not efficient too. The number of operations depend on the implementation and there is no clear definition of which operations to count.
78 |
79 | * Although counting the operations takes into the acount of input data and is independent of computer, but still we need a better way of measuring the program efficiency. This is where Big O comes into the picture.
80 |
81 | ## Big O Again
82 |
83 | * The better way of measuring the efficiency of the program should largely depend on the algorithm, scalability and the size of the inputs.
84 | * The input could be an integer, a list, array, or multiple inputs to a function.
85 |
86 | * There are 3 cases for Big O:
87 | * **Best case**: The minimum possible running time over all inputs of a given size. Say you are searching the element in a list and by luck, such element is the first element of the list.
88 |
89 | * **Average case**: The average running time over all possible inputs of a given size. For i input of a running time ti, average case is the average time for running all possible inputs n.
90 |
91 | * **Worst case**: Maximum running time over all possible inputs of a given size. Example of worst case time: Say you are searching the element in list, and that elements turns out to be the last element. You will have to go over all elements of the list until you find the target element. The element could also not be part of the list...worst! Exhausted resources!!
92 |
93 | * The best case time is not useful. It can trick us to think that our program is faster but not actually true!
94 |
95 | * In academia, people use Big Omega, `Ω(n)` to describe the best case scenario of the algorithm and Big Theta `Θ(n)` to describe algorithm that has both same average case and worst case time. But in software engineering, where people care a lot about building things that work at scale, they merely use Big-O `O(n)` to describe the worst case scenario of the algorithm. This is why best case time is not a useful metric for people that care about scalability!
96 |
97 | * Average case and worst case are sometime similar.
98 |
99 | * In Big O time measurements, additive and multiplicative `constants` are ignored. Our interests are in finding how the time grows with the input. Let's take an example below:
100 |
101 | ******************
102 | ```python
103 | def factorial(n):
104 | """
105 | Given an integer value,
106 | return its factorial without using resursive approach
107 | """
108 |
109 | facto = 1 # Assignment Takes constant time: O(1)
110 | while n > 1: # This loop will run n times: O(n)> Because the loop has 2 lines to execute, it will be O(2n)
111 | facto *= n # Takes constant time: O(1)
112 | n -= 1 # Takes constant time: O(1)
113 | return facto
114 | print(factorial(3)) #3x2x1
115 | #Output: 6
116 | # The total O time = O(1 + 2n + 1 + 1)
117 | # All constants are neglected, so the resulting runtime is O(n)
118 | ```
119 | ******************
120 |
121 | * The above example shows that all constants time are neglected. However, it's important to note that constants affect the runtime. They do it at a small scale, but overall, in theorical analysis of algorithmic efficiency, we neglect all constants.
122 | * Also, when multiple orders of growth are present in the program, we take the dominant order of growth. It's same as what we said above. So for example, `O(2 + 3n + n^2) = O(n^2)`. The dominant term is `n^2`. The rest are neglected. Take another examples: `O(n + logn) = O(n)`, `O(nlogn + n) = O(nlogn)`.
123 |
124 | * For small input data, constants can seems like they make difference and they actually does, for large size of the input data n and high order of growth, they don't make difference. This is why we omit them!
125 |
126 | * When we say `O(n)`, `n` represents the size of the input data. So when your function input is a list and integer, pay attention to a list.
127 |
128 |
129 | ### The Order of Big O
130 |
131 | * Big O defines the order of growth of a particular program. O actually mean `Order` or `On the Order of n`, where `n` is the size of the input data to a program.
132 | * There are at least 6 orders from the lowest to the highest:
133 | * Constant time: `O(1)`
134 | * Linear Time: O(n)
135 | * Logarithmic time: `O(log n)`
136 | * Log-linear time: `O(nlogn)`
137 | * Quadratic time: `O(n^2)`
138 | * Exponential time: `O(c^n)`
139 |
140 | * `O(1)` and `O(log n)` are fast, `O(n)` and `O(nlogn)` are not bad, and the latter two are very slow. Unless it's the only possible option, you should avoid having the algorithm that has runtime of `O(n^c)` or `O(c^n)`.
141 | * `O(1)` >> `O(log n)` >> `O(n)` >>`O(nlogn)` >>`O(n^c)` >> `O(c^n)`.
142 | * Below is the illustrations of 6 different orders of growth:
143 |
144 | 
145 | Credit: MIT 6.0001, Lecture 10...More about the course in references.
146 |
147 | * Let's talk about those orders in details.
148 |
149 | #### 1. O(1), Constant time
150 |
151 | * All simple operations take O(1) constant time.
152 |
153 | * These operations include but are not limited to adding, multiplying, and subtracting numbers. Assignment operator(`=`) also takes a constant time.
154 |
155 | * Constant time does not depend on the size of the input data. Let's take an example.
156 | * Operations such as finding the length of the list `(len(list_of_numbers))` takes a constant time. It does not matter how long the list is. It's just finding its length. Another example: Given a string(or a sequence of characters), determine if the string is empty. The task of determining if the string is empty takes a constant time. The size of the string doesn't matter when it comes to finding if it's either empty or not. Recall that Big-O deals with the size of the input data.
157 |
158 | ******************
159 | ```python
160 | def stri_empty(stri):
161 | """
162 | Determining if a given string is empty takes a constant time O(1).
163 | It does not depend on the size of the string.
164 | """
165 | if stri == '':
166 | return True
167 | else:
168 | False
169 | ```
170 | ******************
171 |
172 | * To summarize a O(1) constant time: The Big-O time doesn't depend on the size of the input data `n`.
173 |
174 | #### 2. O(n), Linear Time
175 |
176 | * While the constant time doesn't depend on the size of the input data, linear time is directly proportional to the size of the input data `n`. Hence the name `linear`.
177 | * Example of linear time O(n): Given a list of unsorted values, determine if element x is part of the list. This task is actually a `linear search`, a brute way to search a list. The task will depend on the number of elements of the list, or simply the size of the list. If there are `n` elements, the list will take O(n) linear time.
178 |
179 | ```python
180 | def linear_search(L, x):
181 |
182 | n = len(L)
183 | for i in range(n):
184 | if i == x:
185 | return True
186 | ```
187 |
188 | * The reason that the runtime for the above list search function is `O(n)` is that we must look through the all elements of the list to determine if the given element `x` is there. So, the runtime will be proportional to the size of the list.
189 | * Generally, most single iterative problems have a linear time `O(n)` because you essentially have to look through the all elements of the list. The runtime for iterative problems or problems that involve loop will be proportinal to the number of iterations. Recall that if any line of code that simply perform simple mathematical operations takes constant time.
190 |
191 | * Let's take another example: Given a list of the weekdays, return days that begin with the letter `T`:
192 |
193 | ******************
194 | ```python
195 | def week_days(week_d):
196 |
197 | days_T = [] #O(1)
198 |
199 | for day in week_d: #O(n)
200 | if day[0] == 'T': #O(1)
201 | days_T.append(day) #O(1)
202 |
203 | return days_T
204 | ```
205 | ******************
206 |
207 | * The runtime for the above program is `O(n)` if we neglect all constants. Inserting or appending a value to a list would take `amortized`(or average) constant time since Python list implements `dynamic arrays` or `ArrayList`. Dynamic arrays have amortized or average time of `O(1)` for dynamic operations(inserting or deleting the element at the end of the array). Amortized time represents the average time it takes to insert or append elements to a list. Note that word average: Each insertion or append operation takes `O(n)` but since `it always doen't happen`, the `amortized` constant time is `O(1)`.
208 | * To wrap up the linear time and constant time, here is a great analogy by Ned. Counting the beans in a jar by counting one bean by bean(depend on number of beans), and counting beans by labels(just simple, count the labels). See photo below.
209 |
210 | 
211 | *Photo credit: Ned Batchelder, Pycon 2018.*
212 |
213 | #### 3. O(logn), Logarithmic Time
214 |
215 | * Linear time depends on the size of the input in proportional manner. Doubling the size of the input will double the runtime. Logarithmic time however are different. Let's see some examples.
216 |
217 | * Let's take an example of a commonly know searching algorithm called `binary search`. Binary search uses a technique called `divide and conquer`. Divide and conquer is a technique that is also used by sorting algorithms like `merge sort` and `quick sort` to sort a list or array. In divide and conquer, we evenly divide a problem and then recursively solve every subproblem. Let's explain this by a binary search algorithm.
218 |
219 | * We are given an array of values `arr` and we are asked to search a given value `x` in the array using binary search. We start with comparing the value x with the `middle` element of the array. If x == middle, we return x. If x is less than middle, we search on the left side (all elements less than middle) of the array. If x is greater than middle, we search on the right side(or elements greater than the middle) of the array.
220 |
221 | * The binary search algorithm would look like this but note that the code is not supposed to run.
222 | ******************
223 | ```python
224 | def binary_search(arr, x):
225 | n = len(arr)
226 | middle = arr[n//2]
227 | left = arr[:middle]
228 | right = arr[middle:]
229 |
230 | if x == middle:
231 | return middle
232 |
233 | if middle < x:
234 | binary_search(left, x) #recursively search in left side
235 |
236 | else:
237 | binary_search(right, x) #recursively search in left side
238 | ```
239 | ******************
240 |
241 | * In binary search, for every step, we divide the array by 2 until we have only one element. If we have a array of N values, for every step, we will have N/2 values. The number of steps required to divide the array till the end is log2N which is equal to 2k = N where `k` is the number of steps.
242 | * So, our binary search will take O(logn) time where the base of the log is 2.
243 |
244 | * In essence, nearly all computational problems that requires some sorts of halving the inputs takes O(logn) logarithmic time. Those include searching element in balanced binary search tree. When finding a element in binary search tree, for every step at a given node, we either go left or right of the node. When we do that, we are essentially dividing the height of the tree until we get to the last node of the tree(`leaf node`). The runtime for searching element in a balanced tree is proportional to the `height` of the tree and since we halve the height when traversing a node after node, the runtime is logarithmic. It is `O(logn)`.
245 | * When we divide the input data into two equal parts to separately operate on each individual part, we are essentially reducing the workload. That's why logarithmic time is better than linear time. Most works that take O(n) linear time usually means we are iterating through all elements of a list or other data structures.
246 |
247 | * To stress that, most computational problems that involve evenly splitting the elements of the input data take logarithmic runtime.
248 |
249 | * To summarize logarithmic time(in comparison with linear time), let's also take an example from Ned talk. Say you want to find a given word in a book. If you start reading the book from the first page all the way to the last page finding that particular word, the time it will take will depend on the size of the book, or simply the number of words in book. That's a linear time O(n). But say now you are searching that word in encyclopedia. It's going to be much easier than a book. You may start by looking in the middle if that word is present. If it's not in middle, you can look at pages before the middle page, and else not at the back pages. You can tell that this is a `binary search problem where we are using divide and conquer` technique. And at every word search step, we evenly divide the pages into two sides, and search on each side. This will take logarithmic time O(logn). Quoting Ned, *"in this case, doubling the size of the encyclopedia doesn’t double the time it takes. It just adds one more divide-in-half step to the work. This is known as a log-n task: the number of steps grows as the number of digits in n grows.* Here is the illustration of the example below.
250 |
251 | 
252 |
253 | #### 4. O(n log n), Log-linear time
254 |
255 | * Previously, we saw that `O(log n)` logarithmic time increase half-step the input as the size of the input data doubles. We also saw that almost all problems in which we have to halve the input for each step like binary search and binary search tree have `O(log n)` logarithmic time.
256 |
257 | * Most practical problems takes `O(n log n)` log-linear time. Examples are sorting algorithms like merge, quick sorts, and heap sorts.
258 |
259 | * O(nlogn) time is the multiplication of O(n) linear time and O(logn) logarithmic time. It's multiplying the work done in linear time and other done in logarithmic time. Or, it's like doing `O(log n)` work in `n` times.
260 |
261 | * A good example of O(nlog n) time is merge sort. Merge sort works by dividing the array into two equal sub-arrays and then recursively sort every sub-array and merge the results later. Merge sort is essentially based on divide and conquer. Dividing the array into two subparts takes `O(log n)` time. Each recursive call takes `O(n)` time. If we multiply them, we get `O(log n)` time. The reason we multiply is that for every step we divide the array into two parts, we also do one recursive call. Basically, all scenarios where you do one thing and do other thing at the same step(say iteratively), then the runtimes are multiplied. On the other hand, if you have to do a task till the end, and you start another task and complete it, the runtime are added.
262 |
263 | * Below is the algorithm of merge sort:
264 |
265 | ******************
266 | ```python
267 | def merge_sort(left, right):
268 | """
269 | left: left part of the list
270 | right: right part of the list
271 | """
272 | sorted_list = []
273 | i,j = 0, 0
274 | while i < len(left) and j < len(right): #order the left and right list
275 | if left[i] < right[j]:
276 | sorted_list.append(left[i])
277 | i += 1
278 | else:
279 | sorted_list.append(right[j])
280 |
281 | while (i < len(left)): #sort left part if right part is empty
282 | sorted_list.append(left[i])
283 | i += 1
284 |
285 | while (j < len(right)):#sort right part if right part is empty
286 | sorted_list.append(right[j])
287 | j += 1
288 |
289 | return sorted_list
290 | ```
291 | ******************
292 |
293 | * That's it for log-linear time. To summarize, log-linear time means we are doing the `O(log n)` logarithimic work in `n` times. When the size of the input n grows, it takes a bit more to complete the work.
294 |
295 | #### 5. O(n2) Quadratic Time
296 |
297 | * All algorithmic complexities we saw so far are not quite bad. If you can do something in constant time, that's really great. If you can do it in logarithmic time, that's still great. In linear time, that's good. In log-linear, that's not bad.
298 |
299 | * The things starts to be cautious when you have to do the work in `O(n^2)` quadratic time. Remember Big-O is a measure of how code slows when data grows. So, if the size of your input data is squarred, the runtime is squarred too. For small values of `n`, that can be tolerable. But when you expect the n to be large, quadratic time complexity can extremely slows down your algorithm.
300 |
301 | * Example of cases where you may have quadratic time complexity are using two nested loops and selection sort.
302 |
303 | ```
304 | Selection sort is an inplace sorting algorithm where we start with finding the minimum element of the array
305 | and swap it with the front element(element at index 0), look other minimum value between array[1:]
306 | and swap it with the element at index 1, and then continue until the array is sorted.
307 | The idea of selection sort is to iteratively keep all the elements before index i sorted.
308 | ```
309 |
310 | * Let's take an example of nested loops. Say you have two equal size lists and you want to find the intersection between them. If it's okay to have duplicates in the intersection, the algorithm can look like this:
311 |
312 | ******************
313 | ```python
314 | def list_intersect(l1, l2):
315 |
316 | intersection = []
317 | for i in l1:
318 | for j in l2:
319 | if i == j:
320 | intersection.append(i)
321 | ```
322 | ******************
323 |
324 | * In the function above, each loop will take `O(n)` time. The resulting runtime will be the multiplication of both runtimes since at every step of iteration, we are doing work in list `l1` and `l2` at the same time.
325 |
326 | * Quadratic time is also called as polynomial time. Polynomial time is a general case for all algorithms that takes `O(n^c)` time where `c` is a constant. That means we can have `O(n^3)` cubic time, `O(n^4)` quartic time, `O(n^5))` penta-time(not sure if it's the right way to say it but you get the idea).
327 |
328 | * Also recursive functions calls takes polynomial complexity.
329 |
330 | * If you can find other ways to not do a task in polynomial time, that's a good thing because as you go to the large order polynomials, your algorithm can be very slow.
331 |
332 | #### 6. O(2n) Exponential Time
333 |
334 | * An algorithm that run in exponential time is much slower than algorithm that run in polynomial time. When you have to make many exponential calls for large input data, algorithm can be very and very and very slow.
335 |
336 | * Making more than one recursive function call on the same input data takes exponential time. Remember that one recursive function call takes O(n) linear time.
337 |
338 | * In the expression `O(2^n)`, `n` is the number of times that each recursive function call run.
339 |
340 | * Problems that involves putting things in the order of combination takes exponential time. Take an example: Say you have a list of integers n, the time it would take to reorder the list in every possible combination of its elements would be exponential time. If your list have n values, it would take `O(2^n)` time.
341 |
342 | * Let's take an example of finding the fibonacci series. A fibonacci series goes like this: `0, 1, 1, 2, 3, 5, 8, 13, 21...`: Each current number is the sum of previous two numbers.
343 |
344 | * The exponential time `O(2^n)` is not to be confused with `O(n!)` factorial time. All problems that involves permutations(the number of possible order that something can be arranged) takes factorial time. A factorial of n is equal to `n*n-1*n-2...*1`. Example: `3! = 3 * 2 * 1 = 6`.
345 | *
346 | ******************
347 |
348 | ```python
349 | def fibonac(n):
350 | '''
351 | Edge cases: n = 0, return 0, 1 return 1, None return None
352 | '''
353 | if n == 0:
354 | return n
355 | elif n == 1:
356 | return n
357 | elif n == None:
358 | return None
359 |
360 | return fibonac(n - 1) + fibonac(n - 2)
361 | ```
362 | ******************
363 |
364 | * The worst case runtime for the above fibonacci series function is `O(2^n)`
365 |
366 | * For large values of n, factorial can be hard to compute. Find the factorial of 20 for example!!!
367 |
368 |
369 | ### Python Time Complexity
370 |
371 | * Below is the runtime complexity for Python list and dictionaries:
372 |
373 | * Lists:
374 | * Inserting or appending value in at the end of list with `mylist.append()`: O(1)
375 | * Removing or popping the last element of the list with `mylist.pop()`: O(1)
376 | * Getting the value(`indexing`) at index i with `mylist[i]`: O(1)
377 | * Finding the length of the list with `len(mylist)`: O(1)
378 | * Checking if value exists in list with `in` operator: O(n)
379 | * Looping or `iterating` through the list with `for loop`, `for i in mylist`: O(n)
380 | * Sorting the list with `mylist.sort()`: O(n log n)
381 | * Reversing the list with `mylist.reverse()`: O(n), all items must be rearranged.
382 | * Inserting the element x at a given index of the list(not the last index) with `mylist.insert(i, x)`: O(n).
383 | * Removing the element in a list at a given index (not last one) with `mylist.remove(i, x)`: O(n)
384 |
385 | Dictionaries:
386 | * Adding a new key-value pair to the dictionary, `mydict[key] = value`: O(1).
387 | * Getting the value at a given key, `mydict[key]`: O(1).
388 | * Checking if key exist in dictionary with `in` operator, `key in mydict`: O(1).
389 | * Looping through the keys in dictionary, `for key in mydict`: O(n).
390 | * Finding the length of dictionary with `len(mydict)`: O(1) average case, O(n) worst case.
391 |
392 | * In essence, Python list implements dynamic arrays. Dynamic arrays provides constant time for inserting and deleting the last element of the array.
393 |
394 | * Dictionaries implements hash tables. Hash tables are extremely fast for key-value pair mapping. That's why getting the value stored at a given key takes constant time. Same thing for checking if a key exists in dictionary.
395 |
396 | * For Python sets(store dictinct values): Adding new value with `add(key)` method takes O(1) constant time, checking if value is `in` set also takes constant time, but looping through the set takes linear time.
397 | * Python sets also implements hash tables with keys only rather than key-pairs.
398 | * Converting the list to set takes O(n) linear time. So, there is no gain of efficiency when you convert a list to a set to take advantage of constant time insertion for any index!
399 |
400 |
401 | ### Big O: Summary and Tricks
402 |
403 | * Big O is used to measure or to evaluate the efficiency of the algorithms. It describes the order of growth of algorithm. The lower the order, the better(if you can do something in `O(1)` constant time, that's a huge win).
404 |
405 | * The nice thing about Big-O is that it doesn't depend on the implementation of the algorithm or the machine running the algorithm. It merely measures the behavior of the algorithm at the large input data.
406 |
407 | * Big-O largely depends on the size of the input data, and it's even meaningful for large input data. As Ned put it in his fascinating [talk](https://www.youtube.com/watch?v=duvZ-2UK0fc), "Big-O is how the code slows as the data grows."
408 |
409 | * Big-O can be hard to understand, but thinking it in terms of how the increase in the input data affects the efficiency of the algorithm can make it a bit easier.
410 |
411 | * There are 6(+1) main order of growth:
412 | * **O(1) Constant time:** The runtime of the algorithm does not depend on the size of the input data `n`. All mathematical, assignment operators and almost all scalar operations takes constant time.
413 | * **O(log n) Logarithmic time:** The time complexity of the algorithm increases by one step (of input) as the size of the input data `n` doubles. Most computational problems that can be solved by dividing the input data into two parts at every step(divide and conquer) takes logarithmic time.
414 | * **O(n) Linear time:** The time complexity of the algorithm is directly proportional to the size of the input data.
415 | * **O(n log n) Log-linear time:** Log linear time is multiplication of logarithmic time `O(log n)` and linear time `O(n)`. It's like doing a `O(log n)` task `n` times. Most sorting algorithms such as merge sort and quick sort and heap sort takes `O(n log n)` time.
416 | * **`O(n^2)` Quadratic time:** The runtime increase by square when the size of the input data increase by square. Quadratic time is a kind of polynomial time. There can be cubic time `O(n^3)`, `O(n^4)`....Nested loops like to have this type of runtime.
417 | * **`O(2^n)` Exponential time:** Exponential time is the best description of most recursive function calls.
418 | * **`O(n!)` Factorial time:** Computational problems that involves some form of permutations usually have factorial time. This is the highest order of growth the algorithm can have.
419 |
420 | 
421 | *Image by Ned*
422 |
423 | * O(1), (log n), O(n log n), and O(n) are generally fast, but `O(n^2)`, **`O(2^n)`, **`O(n!)` can be very slow and extremely slow for large dataset.
424 |
425 | * Here are few things to keep in mind when evaluating the efficiency of the algorithm with Big-O:
426 | * If you are doing typical operations mostly on scalar values, it's O(1).
427 | * If you have to loop through the values of the most data structures like list(or simply input data), it's O(n).
428 | * When doing two things iteratively(you do one thing, before you finish it you do other thing...), you must multiply the runtime of both things. Example of this is two nested loops iterating through the data, they takes `O(n) * O(n) = O(n^2)`.
429 | * When doing two things, where you do one thing completely and you tackle another thing, the resulting runtime is the addition of both things. Example: Two independent for loops.
430 | * If you have to divide the input data into two part at every step all the way to the end(divide and conquer), the runtime is O(log n). If you have to do that for every element of data structure, that's O(n log n) time.
431 | * Most sorting algorithms takes O(n log n) time.
432 | * Most recursive function calls takes `O(2^n)` exponential time.
433 | * If you have to find the permutations of values in the input data, the runtime is factorial O(n!) which is the highest order.
434 |
435 |
436 | * That's it about Big-O. Always think Big-O as *"how code slows as data grows"*. For small data, it may not be helpful, but overall, Big-O is one of the core computer science concepts that every programmer should try to understand at the highest level!
437 |
438 |
439 | ### References and Further Learning
440 |
441 | Big-O is a difficult concept. Below are the resources I used to understand it a little bit:
442 | * Ned Batchelder talk: [Big-O: How Code Slows as Data Grows](https://www.youtube.com/watch?v=duvZ-2UK0fc)
443 | * Understanding Program Efficiency - MIT 6.0001, [Part 1](https://www.youtube.com/watch?v=o9nW0uBqvEo&t=8s), [Part 2](https://www.youtube.com/watch?v=7lQXYl_L28w).
444 | * [Python Time Complexity](https://wiki.python.org/moin/TimeComplexity)
445 |
446 | ### [BACK TO TOP ⬆️](#0)
--------------------------------------------------------------------------------
/markdowns/bit-manipulation.md:
--------------------------------------------------------------------------------
1 | ## Bit Manipulation
2 |
3 | * To a computer, everything is a number. On its front end, it abstract us with super intuitive and good numbers and letters, but deep inside, everything is stored in `1's` and `0's`. Computers deal with binary digits or bits.
4 |
5 | * We as humans use decimal numbers because they easier for us. We do not need to convert numbers between numbers system when counting or doing other numeric/arthmetic operations. Computers use binary system because they are easier to implement in hardware using logic gates.
6 |
7 | * Bit manipulation is a technique used to manipulate bits or other pieces of data at the lowest level. It is mostly used in error detection and correction algorithms, control devices, data compression, encryption algorithms and optimization.
8 |
9 | * Python has `bitwise operators` that are used to manipulate individual bits. The majority of programmers don't need to manipulate bits since that is taken by Python.
10 |
11 | ### Python Bitwise Operators
12 |
13 | * Bitwise operators operate on operands(the data) bit by bit, hence the name `bitwise`. The normal logical operators(`nor`, `or`, `and`) are used to evaluate expressions.
14 |
15 | * Python bitwise operators only works with integers.
16 |
17 | * If you want to operate on bit by bit, you should not use logical operators.
18 |
19 | * There are 6 main bitwise operators:
20 | * Bitwise AND: `&`
21 | * Bitwise OR: `|`
22 | * Bitwise NOT: `~`
23 | * Bitwise XOR: `^`
24 | * Bitwise left shift: `<<`
25 | * Bitwise right shift: `>>`
26 |
27 | * Bitwise operators are similar across almost all programming languages.
28 |
29 | * With the exception of `Bit wise Not ~`, all other operators require two operands (`left and right operands`). Operands are data to be operated on. Example: `a & b`, `&` is `bitwise AND`, `a` is `left operand`, `b` is `right operand`.
30 |
31 | #### Bitwise AND &
32 |
33 | * Bitwise AND performs logical AND for all corresponding bits. It returns 1 if corresponding bits are 1 and else 0.
34 |
35 | Example:
36 |
37 | ```
38 | a = 1010 #10 in decimal
39 | b = 0111 #7 in decimal
40 | #--------------
41 | a & b = 0010 #2 in decimal
42 | ```
43 | ```python
44 | a = 10
45 | b = 7
46 | print(f"a & b: {a & b}")
47 | ```
48 | ```
49 | a & b: 2
50 | ```
51 |
52 | * Bitwise AND is equivalent to multiplication. If you multiply `a` and `b`, you would get the same response.
53 |
54 | #### Bitwise OR |
55 |
56 | * Bitwise OR performs logical OR for all corresponding bits. It returns 1 either of the corresponding bits is 1 and 0 if corresponding bits are 0.
57 |
58 | Example:
59 |
60 | ```
61 | a = 1010 #10 in decimal
62 | b = 0111 #7 in decimal
63 | #--------------
64 | a | b = 1111 #15 in decimal
65 | ```
66 |
67 | ```python
68 | a = 10
69 | b = 7
70 | print(f"a | b: {a | b}")
71 | ```
72 | ```
73 | a | b: 15
74 | ```
75 |
76 | * Bitwise `a | b`) is equivalent to `a | b = a + b - (a x b)`. Note this is to be performed on individual bits.
77 |
78 | #### Bitwise NOT ~
79 |
80 | * Bitwise NOT performs negation or inversion of bits. It flips the corresponding bits where 1 become 0 and 0 become 1.
81 |
82 | * Bitwise NOT only operates on 1 operand.
83 |
84 | Example:
85 |
86 | ```
87 | a = 1010 #10 in decimal
88 | #--------------
89 | ~a = 0101 #5 in decimal
90 | ```
91 |
92 | * The inverted bits are compliment of 1. That is to say `~ a = 1 - a`(bit by bit).
93 |
94 | #### Bitwise XOR ^
95 |
96 | * Bitwise XOR `^` returns 1 if the corresponding bits are not similar and 0 otherwise.
97 | * XOR stands for exclusive OR. The corresponding bit pair must be exclusive or opposite for it to return 1.
98 |
99 | Example:
100 |
101 | ```
102 | a = 1010 #10 in decimal
103 | b = 0111 #7 in decimal
104 | #--------------
105 | a ^ b = 1101 #13 in decimal
106 | ```
107 | * Bitwise XOR is equivalent to the modular 2 (%) of the sum of the operands: ` a ^ b = (a + b) % 2`.
108 |
109 | #### Bitwise Shift Operators
110 |
111 | * Bitwise shift operators are used to shift bits left or right by a given level. We can use shift operators to multiply or divide a number by a power of 2.
112 |
113 | ##### Bitwise Left Shift Operator
114 |
115 | * Bitwise left shift operator `(<<`) shifts the bits of the number to the left by a specified places or number of shifts.
116 |
117 | * It's like multiplying the number by the power of 2. If you are only shifting for one place, it's equivalent to doubling the number. Shifting for two spaces, it's multiplying the number by 4.
118 |
119 | * The gap left pn the right after shifting on the left is filled by 0.
120 |
121 | * Shifting to the left is equivalent to multiplying the number by power of 2: a << n = a x 2n.
122 |
123 |
124 | * Examples:
125 |
126 | ```
127 | a = 1110001
128 | a << 1 = 11100010
129 | a << 2 = 111000100
130 | a << 4 = 11100010000
131 | ```
132 | ```python
133 | a = 10
134 | print(f"a << 3: {a << 3}")
135 | ```
136 | ```
137 | a << 3: 80
138 | ```
139 |
140 | ##### Bitwise Right Shift Operator
141 |
142 | * Bitwise right shift operator `(>>)` shifts the bits of the number to the right by a specified places or number of shifts.
143 |
144 | * It's like dividing the number by the power of 2. If you are only shifting for one place, it's equivalent to dividing the number by 2. Shifting for two spaces, it's dividing the number by 4.
145 |
146 | * The gap left on the left after shifting to the right can be filled with 0's but it does not change anything.
147 |
148 | * Shifting to the right is equivalent to dividing the number by power of 2: a << n = a / 2n.
149 | * Shifting to the right by one place is like performing floor(round up decimals) division by 2.
150 |
151 | * Examples:
152 |
153 | ```
154 | a = 1010
155 | a >> 1 = 101 #the right most bit is eliminated
156 | a >> 2 = 10
157 | a >> 4 = 0
158 | ```
159 |
160 | ```python
161 | a = 10
162 | print(f"a >> 1: {a >> 1}")
163 | ```
164 | ```
165 | a >> 1: 5
166 | ```
167 | ```python
168 | a = 10
169 | print(f"a >> 4: {a >> 4}")
170 | ```
171 | ```
172 | a >> 4: 0
173 | ```
174 | ### Examples of Solving Ordinary Arthimetic Problems with Bit Manipulation
175 |
176 | #### 1. Swap Two Numbers Without Using Any additional Variable
177 |
178 | ```python
179 | # Swap two numbers without using any additional variable or any arthmetic operator
180 |
181 | def swap_numbers(a, b):
182 | """
183 | Given two integers a and b, swap them without using any additional variable
184 | """
185 |
186 | a = a ^ b
187 | b = a ^ b # b = a ^ b = (a^b) ^ b = a^(b^b) = a^0 = a, now b = a
188 | a = a ^ b # a = a ^ b = (a^b) ^ a = b^(a^a) = b ^0 = b, now a = b, swapped!!
189 |
190 | return a, b
191 | ```
192 | ```python
193 | a = 12
194 | b = 32
195 |
196 | a_new, b_new = swap_numbers(a,b)
197 | print(f"Swapped A:{a_new}, Swapped B: {b_new}")
198 | ```
199 | ```
200 | Swapped A:32, Swapped B: 12
201 | ```
202 |
203 |
204 | ```python
205 | # Swapping numbers using arthmetic operators
206 |
207 | def swap_numbers_2(a, b):
208 | """
209 | Given two integers a and b, swap them without using any additional variable
210 | """
211 |
212 | a = b - a
213 | b = b - a # b = b - (b - a) = b - b + a = a, b = a, swapped!
214 | a = b + a # a = b + a =
215 |
216 | return a, b
217 | ```
218 | ```python
219 | a = 12
220 | b = 32
221 |
222 | swap_numbers_2(a, b)
223 | ```
224 | ```
225 | (32, 12)
226 | ```
227 |
228 | #### 2. Add Two Numbers Without Using Arthmetic Operations
229 |
230 | ```python
231 | # Add two numbers without using any arthmetic operator
232 |
233 | #1: Using iterative approach
234 |
235 | def add_without_arthimetic_iter(a, b):
236 | """
237 | Given 2 numbers, return their sum without using arthmetic operator.
238 |
239 | Solution:
240 | Step 1: Add two numbers without carrying the carry. This is peformed by XOR in binary.
241 | Step 2: Add them together but only carry (ith bit in carry bits is 1 if ith-1 of a and b are 1 (1 + 1 = 10, sum = 0, carry = 1)
242 | Do those 2 steps iteratively.
243 | """
244 |
245 |
246 |
247 | if b == 0:
248 | return a
249 |
250 | while b != 0:
251 | sum_no_carry = a ^ b
252 | carry = (a & b) << 1
253 |
254 | a = sum_no_carry
255 | b = carry
256 |
257 | return a
258 | ```
259 | ```python
260 | a = 12
261 | b = 13
262 |
263 | add_without_arthimetic_iter(a, b)
264 | ```
265 | ```
266 | 25
267 | ```
268 |
269 | ```python
270 | #2: Add two numbers iteratively
271 |
272 | def add_without_arthimetic_recursive(a, b):
273 | """
274 | Given 2 numbers, return their sum without using arthmetic operator.
275 |
276 | Solution:
277 | Step 1: Add two numbers without carrying the carry. This is peformed by XOR in binary.
278 | Step 2: Add them together but only carry (ith bit in carry bits is 1 if ith-1 of a and b are 1 (1 + 1 = 10, sum = 0, carry = 1)
279 | Do those 2 steps recursively.
280 | """
281 |
282 | if b == 0:
283 | return a
284 |
285 | sum_no_carry = a ^ b
286 | carry = (a & b) << 1
287 |
288 |
289 | return add_without_arthimetic_recursive(sum_no_carry, carry)
290 | ```
291 | ```python
292 | a = 12
293 | b = 13
294 |
295 | add_without_arthimetic_recursive(a, b)
296 | ```
297 | ```
298 | 25
299 | ```
300 |
301 |
302 | ### Further Learning
303 |
304 | This is fairly short given the depth of bit manipulation. For more about it, check:
305 |
306 | * [Bit Hacks, MIT OpenCourseWare, MIT 6.172](https://www.youtube.com/watch?v=ZusiKXcz_ac)
307 | * [Bitwise Operators in Python by Real Python](https://realpython.com/python-bitwise-operators/#bitwise-logical-operators)
308 | * [Bit Manipulation: Python Official](https://wiki.python.org/moin/BitManipulation)
309 |
310 |
311 | ### To do:
312 |
313 | * Revisit bitwise operators and the foundations of binary system to understand this thing more.
--------------------------------------------------------------------------------
/markdowns/dynamic-programming.md:
--------------------------------------------------------------------------------
1 | # Dynamic Programming
2 |
3 | **********
4 | **Content:**
5 | - [Dynamic Programming](#dynamic-programming)
6 | - [1. Introduction](#1-introduction)
7 | - [2. Solving Fibonacci Series With Dynamic Programming](#2-solving-fibonacci-series-with-dynamic-programming)
8 | - [3. Dynamic Programming With Bottom Up Approach](#3-dynamic-programming-with-bottom-up-approach)
9 | - [4. Summary: Dynamic Programming Vs Recursion Vs Divide and Conquer](#4-summary-dynamic-programming-vs-recursion-vs-divide-and-conquer)
10 | - [5. References and Further Learning](#5-references-and-further-learning)
11 | ************
12 |
13 |
14 | ### 1. Introduction
15 |
16 | * Dynamic programming is an efficient technique that is used to solve problems that have overlapping problems (subproblems that contains other subproblems).
17 |
18 | * A particular problem has overlapping problems if an optimal solution involves solving the same problem multiple times.
19 |
20 | * Dynamic programming solves each subproblem once, save or cache the results, and use the results later rather than recomputing the subproblem. Storing the results of all possible subproblems in a systematic way reduce the time and space required to reach to the optimal solution. The work is merely minimized.
21 |
22 | * Dynamic programming is used to solve optimization problems - Optimization problems are problems whose solutions maximizes or minimizes some function. Example of optimization: gradient descent that minimize the loss function.
23 |
24 | * Dynamic programming is typically employed when optimizing the recursive algorithm. If a recursive algorithm computes some subproblems more than once, we can save the results of those subproblems(in a table) and use them later thereby saving time and space and minimizing the work to be done.
25 |
26 | * The process of storing the computed results of subproblems and using them later wherever possible is called `memoization`.
27 |
28 | * Dynamic programming is suited for optimization problems that have an inherent `left to right` order such as string characters, rooted trees and integer sequences.
29 |
30 | * Let's efficiently solve the fibonacci series with dynamic programming.
31 |
32 |
33 | ### 2. Solving Fibonacci Series With Dynamic Programming
34 |
35 | * Fibonacci series is a hello world of recursion and dynamic programming.
36 | * A fibonacci series is a series in which a current number n is equivalent to the addition of two previous number in the sequence. Below is the expression of a fibonacci series.
37 |
38 | >`F(n) = F(n-1) + F(n-2)`,
39 | `F(0) = 0, F(1) = 1, F(2) = 1, F(3) = 2, F(4) = 3 ...`
40 |
41 | * Before we seek to optimize the fibonacci series, let's implement it with recursion.
42 |
43 | ```python
44 |
45 | def fibo(n):
46 | """
47 | Base cases: fibo(0) = 0, fibo(1) = 1
48 | """
49 |
50 | if n == 0 or n == 1:
51 | return n
52 | else:
53 | return fibo(n - 1) + fibo(n - 2)
54 |
55 | ```
56 |
57 | * Fibonacci series of large numbers are hard to compute. Take for example: Computing `fibo(120)` gives `8,670,007,398,507,948,658,051,921` and if we assume each recursive call takes a nanosecond, it would take 250,000 years to finish. The reason why it takes lots of time to compute fibonacci series is that there are multiple similar recursive calls that are computed at every step. In fact, the idea behind dynamic programming is identifying those similar recursive calls and store their results to avoid computing them more than once.
58 |
59 | * Let's take an example for `fibo(5)`. Below is the recursive tree of `fibo(5)`. As you can notice, it has `3 fibo(2)` and `2 fibo(3)`.
60 |
61 | 
62 |
63 | * If we modify the recursive tree like below, we could potentially save work needed to find the number of the series since we are not recomputing the functions that already have values.
64 |
65 | 
66 |
67 |
68 | * Let's now compute the fibonacci series efficiently using dynamic programming. The only thing we do is to store the results of the similar recursive calls and reuse them everytime we face them rather than recomputing twice or thirdth or fourth...
69 |
70 | ```python
71 |
72 | def fibo_2(n, mimo = {}):
73 | """
74 | Find the fibonnacci of n.
75 | mimo: a dictionary to save results of overlapping recursive calls....
76 | ...n as keys and their results as values.
77 | If n > 1 is already in mimo, return its value. It's the result of the series
78 | Else: find its result, save it to memo and return it
79 | """
80 |
81 | if n == 1 or n == 0:
82 | return n
83 |
84 | if n > 1:
85 | if n in mimo:
86 | return mimo[n]
87 | else:
88 | result = fibo_2(n - 1, mimo) + fibo_2(n - 2, mimo)
89 | mimo[n] = result
90 | return result
91 | ```
92 |
93 | ```
94 | fibo_2(3)
95 | 2
96 | fibo_2(120)
97 | 5358359254990966640871840
98 | ```
99 | * `fibo_2(n)` in few words: If `n` is either 1 or 0, return 1 since the fibonacci of 1 or 0 is 1. If `n` is greater than 1 and is in `mimo`(initialized as `empty dictionary` to store `overlapping recursive calls`), return the value of `n` in `mimo` (we are saving `n` as keys in `mimo` dictionary and their results as values). If `n` is not in `mimo`, it means we haven't computed it yet, and so compute it and save the results in `memo`.
100 |
101 | * Now we have a very efficient fibonacci series for computing even large series for resonably short time. Computing `fibo_2(120)` takes fraction of milliseconds when it was almost impossible to find it with normal recursive function. Indeed, normal recursive function would take `thousands of years` to find it if we assume each recursive call takes 1 nanosecond. That really demonstrates the beauty of dynamic programming.
102 |
103 | * The runtime of `fibo_2(n)` is `O(n)` since we are caching the results and there are only n values to pass to `fibo_2(n).` Looking the value in a dictionary takes `O(1)` constant time, so we don't count that. This also much better than O(2n) of normal recursive function.
104 |
105 |
106 | ### 3. Dynamic Programming With Bottom Up Approach
107 |
108 | * The approach we used to solve the above fibonacci series with dynamic programming is typically called `top-down approach` where we start solving the problem from the top, divide it into subproblems, save the results of the overlapping subproblems and reuse the results later.
109 |
110 | * In the bottom-up approach, we solve the problem by starting with simplest case. For fibonacci series, we start with finding the fibonacci of 0, then 1, then 2, then 3, etc...and we make sure to use the previous results.
111 |
112 | ```python
113 | def fib_bottom(n):
114 | if n == 1 or n == 0:
115 | return n
116 |
117 | a = 0
118 | b = 1
119 |
120 | for _ in range(n):
121 | c = a + b
122 | a = b
123 | b = c
124 |
125 | return c
126 | ```
127 | ```
128 | fib_bottom(120)
129 | 5358359254990966640871840
130 | ```
131 |
132 | * **What we are doing above:** As usual, if n is 0 or 1, its fibonacci number is n. We start with assigning 0 to variable `a` and 1 to `b` as the simplest cases. Then at every step all the way to n, we find `c` as sum of `a` and `b`(c is like the fibonacci of i but we are not using i), `update a with b` and `update b with c`.
133 |
134 | * The runtime of the algorithm is `O(n)` for looping through the list `range(n)`. Finding `fib_bottom(120)` also takes a fraction of milliseconds.
135 |
136 |
137 | ### 4. Summary: Dynamic Programming Vs Recursion Vs Divide and Conquer
138 |
139 | * Dynamic programming is an efficient technique that is used to solve problems that have overlapping subproblems (subproblems that contains other subproblems).
140 |
141 | * Recursion is a technique used to solve the kind of problems that can be defined in simpler versions of themselves. In essense, recursion is based on divide and conquer technique.
142 |
143 | * For some problems, recursive approach is not usually efficient due to the overlapping recursive calls that takes lots of space. Dynamic programming is used to optimize the recursive functions that have overlapping recursive calls.
144 |
145 | * Divide and conquer is a well-known and popular technique used to divide a given problem into different subproblems, recursively solve each subproblem and merge the results to form the final solution. Algorithms such as merge sort, quick sort, and binary search use divide and conquer technique.
146 |
147 | * Dynamic programming is not all you need. It's only helpful and efficient when the number of unique subproblems are significantly smaller than the total number of subproblems.
148 |
149 |
150 | ### 5. References and Further Learning
151 |
152 | * Introduction to Computation and Programming Using Python, John V. Guttag
153 | * Algorithm Design Manual - Skienna
154 | * Introduction to Algorithms - MIT Press
155 |
156 |
157 | [BACK TO TOP](#0)
--------------------------------------------------------------------------------
/markdowns/oop-classes-objects.md:
--------------------------------------------------------------------------------
1 | # Object Oriented Programming (OOP) - Classes and Objects
2 |
3 | Object Oriented Programming(OOP) is a programming paradigm or feature that allows developers to structure programs into classes and objects. OOP gives the developers the ability to design programs around data.
4 |
5 | Classes are new data type that you can create yourself. Objects can be created from the Class. A typical class has two components: `fields` (or variables) that store data and `methods`(or functions) that operate on those data. Python [glossary](https://docs.python.org/3/glossary.html) defines class as a template for creating user defined objects and object
6 | as any data with state(attribute or value) and defined behavior(methods).
7 |
8 | With the simplicity of Python and the thousands of features it provides that can be used on the fly, the majority of Python developers doesn't need to use OOP. However, it is important to understand OOP so that you can understand when and when not to use it. OOP provides modularity and reusability. Take an example: Python list is a built-in object. Imagine how many time you(and other millions of coders) use list with different data!! That's extreme reusability!!
9 |
10 | When should you use class or function? This is a hard questions, but generally if you found yourself using a number of functions over and over on same data, then you should consider using class for reusability. If on the otherhand you are creating a class with methods that you will use once, it's probably better to use normal functions. Here is a great talk about that: [Stop Using Classes ](https://www.youtube.com/watch?v=o9pEzgHorH0) by Jack Diederich. You can also learn more about the critism of OOP [here](https://en.wikipedia.org/wiki/Object-oriented_programming#Criticism).
11 |
12 | Also, if you plan to build an API(Application Protocol Interface) one day, it's very likely that you will need to structure data and methods in a systematic way. So, OOP is an important thing to know.
13 |
14 | In Python, everything is an `object` and has a `type`. OOP gives us the power to create our custom object of a given data type that can be manipulated. Below is an example of how everything in Python is an object of a certain type.
15 |
16 |
17 | ```python
18 | # In Python, everything is an object of a given type
19 |
20 | my_list = [1, 2, 3, 4]
21 |
22 | type(my_list)
23 | ```
24 |
25 |
26 |
27 |
28 | list
29 |
30 |
31 |
32 | The type of Python `list` is a `type` too. This is the same thing for other objects such as `str` and `dict`. The `type` of `type` object is a `type` too :)
33 |
34 |
35 | ```python
36 | type(list)
37 | ```
38 |
39 |
40 |
41 |
42 | type
43 |
44 |
45 |
46 |
47 | ```python
48 | type(str)
49 | ```
50 |
51 |
52 |
53 |
54 | type
55 |
56 |
57 |
58 |
59 | ```python
60 | type(type)
61 | ```
62 |
63 |
64 |
65 |
66 | type
67 |
68 |
69 |
70 | There is a popular notion that OOP allows developers to represent real world objects into software objects. To some extent, this is true. Real world objects contain state and behavior. Take an example of a car object: The state of a car might be price, model, car maker, speed while the behavior might be changing the gear, applying the brake, etc...).
71 |
72 | Software objects provide an abstraction of the real world objects. Software objects store their states in data attributes(or variables), and they expose their behaviors through method attributes(or functions). Methods act on the state of the objects. Java documentation has a great explaination of class and objects in the contexts of the real world objects. For more, check it [here](https://docs.oracle.com/javase/tutorial/java/concepts/object.html).
73 |
74 |
75 | Let's see how to create a class in Python.
76 |
77 | ### Creating a Class in Python
78 |
79 | Classes are user defined objects. That signal that there are built-in classes that we use day to day contained in some [standard libraries](https://docs.python.org/3/tutorial/stdlib.html). Take an example for a `list`. A list is a class. When we create a list object, we can manipulate it in different ways.
80 |
81 |
82 | ```python
83 | # Creating a list object,
84 | # The data [1,2,3] is an instance of the list
85 |
86 | my_list = [1, 2, 3]
87 |
88 | # We can manipulate the list object
89 |
90 | my_list.append(4)
91 |
92 | print(my_list)
93 | ```
94 |
95 | [1, 2, 3, 4]
96 |
97 |
98 | Below is a simple user defined class:
99 |
100 | ```python
101 | class MyClass:
102 | """A simple example class"""
103 | i = 12345
104 |
105 | def f(self):
106 | return 'hello world'
107 | ```
108 |
109 | When creating a new class:
110 | * We specity its class name. It is common to use Capital letters for defining the class like `ClassName`. This style is called `CamelCase`.
111 |
112 | ```python
113 |
114 | class ClassName:
115 | ## Class attributes
116 |
117 | ```
118 |
119 | * We define the class attributes(data and methods). Data attributes are data (or variables) that makes up the class and the methods are like functions that operate on data. Methods only work with class.
120 |
121 | * We use a special method `__init__` to initialize the data attributes. `__init__` method starts and ends with double underscore(`__`). When passing data to the init method, the first paremeter must be `self`. There are other special methods beyond `__init__`.
122 |
123 | ```python
124 |
125 | class ClassName:
126 |
127 | def __init__(self, x, y):
128 | ```
129 |
130 |
131 | * We use `self` as the first parameter for any method that we define in the class. With the exception of `self` parameter, methods are similar to normal functions. `self` parameter allows the method to use the class data attributes.
132 |
133 | * To access the data attribute and method of an object, we use `. operator`.
134 |
135 |
136 | ```python
137 |
138 | class ClassName:
139 |
140 | def __init__(self, var1, var2):
141 | self.var1 = var1
142 | self.var2 = var2
143 |
144 | def method_1(self, var3):
145 | """
146 | DOCSTRING
147 | self points to the data attributes,
148 | function can take new data attribute like var3
149 | """
150 |
151 | #statements
152 |
153 | ```
154 | Now that we understand what makes a class, let's create a real class.
155 |
156 |
157 | ```python
158 | class Car:
159 |
160 | def __init__(self, speed, gear, model, maker):
161 |
162 | self.speed = speed
163 | self.gear = gear
164 | self.model = model
165 | self.maker = maker
166 |
167 | def speed_up(self, add_speed):
168 |
169 | speed = self.speed + add_speed
170 |
171 | return speed
172 |
173 | def apply_brake(self, decre_speed):
174 | speed = self.speed - decre_speed
175 |
176 | return speed
177 |
178 | def change_gear(self, new_gear):
179 | gear = new_gear
180 |
181 | return gear
182 |
183 | def car_info(self):
184 |
185 | print(f"Car Info:\n \
186 | Speed: {self.speed}\n \
187 | Model: {self.model}\n \
188 | Maker: {self.maker}")
189 | ```
190 |
191 | That's an example of class. In the next section, let's see how to use the class we created.
192 |
193 | ### Using a Class
194 |
195 | To create the instance of the object, we simply call class we created with the values of the data fields. Note that we don't pass `self` when calling the class.
196 |
197 |
198 | ```python
199 | my_car = Car(45, 2, 'Model S', 'Tesla')
200 | ```
201 |
202 |
203 | ```python
204 | # Accessing the data attributes
205 |
206 | my_car.gear
207 | ```
208 |
209 |
210 |
211 |
212 | 2
213 |
214 |
215 |
216 |
217 | ```python
218 | my_car.speed
219 | ```
220 |
221 |
222 |
223 |
224 | 45
225 |
226 |
227 |
228 |
229 | ```python
230 | my_car.maker
231 | ```
232 |
233 |
234 |
235 |
236 | 'Tesla'
237 |
238 |
239 |
240 | To use the methods we defined in class, we do as we always do with built-in objects (ex: `my_list.append(2)`). We use `. operator` and we pass the variables that make up a particular method. Note that we don't use `self` when calling the methods defined in the class.
241 |
242 |
243 | ```python
244 | my_car.apply_brake(10)
245 |
246 | # Should return speed - 10 = 45 - 10 = 35
247 | ```
248 |
249 |
250 |
251 |
252 | 35
253 |
254 |
255 |
256 |
257 | ```python
258 | my_car.speed_up(20)
259 | ```
260 |
261 |
262 |
263 |
264 | 65
265 |
266 |
267 |
268 |
269 | ```python
270 | my_car.change_gear(4)
271 | ```
272 |
273 |
274 |
275 |
276 | 4
277 |
278 |
279 |
280 |
281 | ```python
282 | my_car.car_info()
283 | ```
284 |
285 | Car Info:
286 | Speed: 45
287 | Model: Model S
288 | Maker: Tesla
289 |
290 |
291 | When you print `my_car` object(created from class `Car`), you don't see the information about the class other than that it is an object.
292 |
293 |
294 | ```python
295 | print(my_car)
296 | ```
297 |
298 | <__main__.Car object at 0x10371c2b0>
299 |
300 |
301 | To choose what will be diplayed when we print the object, we can define a `__str__` method inside the class. The `__str__` method must return a string. Otherwise, you will get an error.
302 |
303 |
304 | ```python
305 | class Car:
306 |
307 | def __init__(self, speed, gear, model, maker):
308 |
309 | self.speed = speed
310 | self.gear = gear
311 | self.model = model
312 | self.maker = maker
313 |
314 | def speed_up(self, add_speed):
315 |
316 | speed = self.speed + add_speed
317 |
318 | return speed
319 |
320 | def apply_brake(self, decre_speed):
321 | speed = self.speed - decre_speed
322 |
323 | return speed
324 |
325 | def change_gear(self, new_gear):
326 | gear = new_gear
327 |
328 | return gear
329 |
330 | def car_info(self):
331 |
332 | print(f"Car Info:\n \
333 | Speed: {self.speed}\n \
334 | Model: {self.model}\n \
335 | Maker: {self.maker}")
336 | def __str__(self):
337 |
338 | return f"This is the object created from class Car. The car model {self.model} was made by {self.maker}"
339 | ```
340 |
341 |
342 | ```python
343 | my_car = Car(45, 2, 'Model X', 'Tesla')
344 | print(my_car)
345 | ```
346 |
347 | This is the object created from class Car. The car model Model X was made by Tesla
348 |
349 |
350 | The methods `__str__` and `__init__` are examples of special or magic methods. Python special methods allows us to overload(or to imitate) built-in operators so that we can use them in our objects just like built-in data types. Magic methods are unique to Python and they are not found in other OOP languages such as Java and C++. We will learn more about magic methods later.
351 |
352 | You can find all magics methods on [Python Documentation](https://docs.python.org/3/reference/datamodel.html#basic-customization).
353 |
354 | One last thing: we can use `isinstance()` to check if the object `my_car` is a Car type.
355 |
356 |
357 | ```python
358 | isinstance(my_car, Car)
359 | ```
360 |
361 |
362 |
363 |
364 | True
365 |
366 |
367 |
368 | ### Final Notes and Further Learning
369 |
370 | The central idea of OOP is creating user-defined objects. With OOP, you can create structured and systematic programs. OOP provides modularity and code reusability which are essential ingredients for building large scale programs.
371 |
372 | In the next parts, we will learn about inheritance and special or magic methods.
373 |
374 | You can learn more about OOP and classes and Objects at:
375 |
376 | * OOP, Lecture 8 of [MIT 6.0001 Introduction to Computer Science and Programming in Python](https://www.youtube.com/watch?v=-DP1i2ZU9gk)
377 | * [Python Doc](https://docs.python.org/3/tutorial/classes.html)
378 | * [Wikipedia, OOP](https://en.wikipedia.org/wiki/Object-oriented_programming)
379 | * [Stop Using Classes](https://www.youtube.com/watch?v=o9pEzgHorH0)
380 |
381 |
382 | ```python
383 |
384 | ```
385 |
--------------------------------------------------------------------------------
/markdowns/oop-inheritance.md:
--------------------------------------------------------------------------------
1 |
2 |
3 | # Object Oriented Programming(OOP) - Inheritance
4 |
5 | Contents:
6 |
7 | - [How to Create a Child Class from a Parent Class](#1)
8 | - [Addind New Data and Methods to a Child Class](#2)
9 | - [Method Overriding](#3)
10 | - [Checking if Child Class Belongs to Parent Class](#4)
11 | - [Class Methods, Class Attributes, Static Methods](#5)
12 | - [Final Notes¶](#6)
13 |
14 |
15 | Real world objects that are in the same class/category share similar behaviors. OOP classes can also have things in common where rather than creating new classes, classes can be inherited from other class.
16 |
17 | Also, similar to how we write a normal functions once and reuse them multiple times without having to recreate them again, we can inherit methods and attributes of the parent class using inheritance.
18 |
19 |
20 | Inheritance is an OOP based techniques that refers to `parent-child relationship` where a child class can be inherited from the parent class. The child class will have the access of data and methods of the parent class (but not the other way).
21 |
22 | Inheritance is essentially deriving a new class(child class) from an existing class(parent class). A parent class is also called base class or super class, while the child class is called derived class.
23 |
24 |
25 |
26 | ## 1. How to Create a Child Class from a Parent Class
27 |
28 | When deriving a child class, you simply define the parent class in the child class. Below is a syntax for inheritance:
29 |
30 |
31 | ```python
32 | class ChildClassName(ParentClassName):
33 | .
34 | .
35 | .
36 | .
37 | .
38 | .
39 | .
40 |
41 |
42 | ```
43 |
44 | If you simply want the child class to inherit all data and method attributes without adding or overriding any method of the parent class, simply do the following:
45 |
46 |
47 | ```python
48 | class ChildClassName(ParentClassName):
49 | pass
50 | ```
51 |
52 | Let's take a real example. But we will create a parent class first.
53 |
54 |
55 | ```python
56 | class Vehicle:
57 |
58 | def __init__ (self, brandmaker, model, speed):
59 |
60 | self.brandmaker = brandmaker
61 | self.model = model
62 | self.speed = speed
63 |
64 | def speed_up(self, add_speed):
65 |
66 | new_speed = self.speed + add_speed
67 |
68 | return new_speed
69 |
70 | def vehicle_info(self):
71 |
72 | print(f"Vehicle Info:\n \
73 | Current Speed: {self.speed}\n \
74 | Vehicle Model: {self.model}\n \
75 | Vehicle Maker: {self.brandmaker}")
76 | ```
77 |
78 | From the `Vehicle` class, we can derive a new child class `Bicycle`. If we only want to inherit all data and method attributes, all we have to do is to `pass`.
79 |
80 |
81 | ```python
82 | class Car(Vehicle):
83 | pass
84 | ```
85 |
86 |
87 | ```python
88 | car = Car('Tesla', 'Model S', 40)
89 | car.vehicle_info()
90 | ```
91 |
92 | Vehicle Info:
93 | Current Speed: 40
94 | Vehicle Model: Model S
95 | Vehicle Maker: Tesla
96 |
97 |
98 | A child class inherited from the parent class will have an access to all parent class data and method attributes but if the child class have its own data and methods attributes, they can not be accessed in the parent class. It's only one direction.
99 |
100 |
101 |
102 | ## 2. Adding New Data and Methods to a Child Class
103 |
104 | In addition to the inherited data and method attributes, it's possible to add new data and methods to the class.
105 |
106 | To automatically get the access to the data and methods of the parent class, we can use the `super()` function. Below we use `super` for accessing the initialized parent class data through `__init__` method, but it can be used for almost any method.
107 |
108 |
109 | ```python
110 | class Car(Vehicle):
111 |
112 | def __init__(self, brandmaker, model, speed): #__init__ method is overriden: more on method overriding later
113 | super().__init__(brandmaker, model, speed) #use super() to automatically get the data and methods of parent class
114 |
115 | def speed_down(self, down_speed): #speed_down is a new method, down_speed is a new data/variable
116 |
117 | speed = self.speed + down_speed
118 |
119 | return speed
120 | ```
121 |
122 |
123 | ```python
124 | car = Car('Tesla', 'Model S', 40)
125 | car.speed_down(20)
126 | ```
127 |
128 |
129 |
130 |
131 | 60
132 |
133 |
134 |
135 |
136 |
137 | ## Method Overriding
138 |
139 | A derived or child class inherits all methods of the parent class or base class. But there are times you may want to override the methods of the parent class in child class. All you have to do is to define the methods that have the same name as the parent class. Such method will be overriden!
140 |
141 | We saw that for __init__ method but let's demonstrate it for normal method.
142 |
143 |
144 | ```python
145 | class Bicycle(Vehicle):
146 |
147 | def vehicle_info(self):
148 |
149 | print(f"Bicycle Info:\n \
150 | Current Speed: {self.speed}\n \
151 | Bicycle Model: {self.model}\n \
152 | Bicycle Maker: {self.brandmaker}")
153 | ```
154 |
155 |
156 | ```python
157 | bicycle = Bicycle('Bike Manufacturer', 'Model XXXX', 20)
158 | bicycle.vehicle_info()
159 | ```
160 |
161 | Bicycle Info:
162 | Current Speed: 20
163 | Bicycle Model: Model XXXX
164 | Bicycle Maker: Bike Manufacturer
165 |
166 |
167 | You can see that the method `vehicle_info()` was overriden. A one caveat to make here is that the things we are using are only just examples that demonstrate the concepts. In the real world, inheritance is more about code reuse than following hierachies.
168 |
169 |
170 |
171 | ## 3. Checking if Child Class Belongs to Parent Class
172 |
173 | We can check if an object is a type of a given class with `isinstance()` built-in function. It will return `True` if the object belong to the class, and otherwise `False`.
174 |
175 |
176 | ```python
177 | isinstance(car, Vehicle)
178 | ```
179 |
180 |
181 |
182 |
183 | True
184 |
185 |
186 |
187 |
188 | ```python
189 | isinstance(bicycle, Vehicle)
190 | ```
191 |
192 |
193 |
194 |
195 | True
196 |
197 |
198 |
199 |
200 | ```python
201 | class TempClass:
202 | print('Hello world')
203 |
204 | class TempChild(TempClass):
205 | pass
206 | ```
207 |
208 | Hello world
209 |
210 |
211 |
212 | ```python
213 | temp_obj = TempChild()
214 | isinstance(temp_obj, TempClass)
215 | ```
216 |
217 |
218 |
219 |
220 | True
221 |
222 |
223 |
224 |
225 | ```python
226 | isinstance(temp_obj, Vehicle)
227 | ```
228 |
229 |
230 |
231 |
232 | False
233 |
234 |
235 |
236 | There is another built-in function `issubclass()` that can be used to check if the a particular child class is a subclass of a given parent class. The first argument in `issubclass()` must be a subclass and the later argument must be the parent class.
237 |
238 |
239 | ```python
240 | issubclass(Car, Vehicle)
241 | ```
242 |
243 |
244 |
245 |
246 | True
247 |
248 |
249 |
250 |
251 | ```python
252 | issubclass(Bicycle, Vehicle)
253 | ```
254 |
255 |
256 |
257 |
258 | True
259 |
260 |
261 |
262 |
263 | ```python
264 | issubclass(TempChild, Vehicle)
265 | ```
266 |
267 |
268 |
269 |
270 | False
271 |
272 |
273 |
274 |
275 |
276 | ## 4. Multiple Inheritance
277 |
278 | Python being a very unique programming language supports multiple inheritance where a new child class can be inherited from multiple parent classes.
279 |
280 |
281 | Below is the template for inheriting from several number of parent classes:
282 |
283 | ```python
284 |
285 | class ChildClass(ParentClass1, ParentClass2, ParentClass3, ParentClass4):
286 |
287 | #Statements
288 |
289 | ```
290 |
291 | It's rare that you will need to inherit from multiple classes, but if you happen to do, check the Python [doc](https://docs.python.org/3/tutorial/classes.html#multiple-inheritance) for more.
292 |
293 |
294 |
295 | ## 5. Class Methods, Class Attributes, Static Methods
296 |
297 | Class methods are type of methods that are very specific to the class instead of the objects of the class. Class methods are represented by `@classmethod` decorator above `def` definition. The first parameter of methods is `cls` that stands for `class`.
298 |
299 | Class methods can not access regular class methods or attributes.
300 |
301 | Here is a template of class method:
302 |
303 | ```python
304 |
305 | class ClassName:
306 |
307 | def regular_method(self):
308 | #statements
309 |
310 | @classmethod
311 | def class_method(cls):
312 | #statements
313 |
314 | ```
315 |
316 | Class methods can be called without creating the object of the class.
317 |
318 | ```python
319 | ClassName.class_method()
320 | ```
321 |
322 | Just like class methods, class attributes are variables that are specific to the class not the objects of the class. Class attributes are like global variables, they are defined above all methods and they are not initialized in `__init_() method`.
323 |
324 | Here is a template for class attributes:
325 |
326 | ```python
327 | class ClassName:
328 | num = 0
329 |
330 | # statements
331 | ```
332 |
333 | Static methods are type of methods that don't have `self` or `cls` parameter and have `@staticmethod` decorator above `def` definition. Just like class methods, static methods can not access the class methods and attributes(variables).
334 |
335 | Below is a template for static methods:
336 |
337 | ```python
338 | class ClassName:
339 |
340 | @staticmethod
341 | def static_method_name():
342 | #statements
343 |
344 | ```
345 | Static methods are not widely used in Python. They are common in languages like Java and C++.
346 |
347 | Inheritance, class methods and attributes, and static methods are rarely used. But it's good to know them so that you can be able to read the codes of those who still use them. Most frameworks also use them in their codebases, so it's good to know them!
348 |
349 |
350 |
351 | ## 6. Final Notes
352 |
353 | Inheritance is all about code reuse and OOP is about representing the real world complex systems into software objects. Classes are extremely helpful for organizing large piece of codes but they are probably not the right option when you are building something you just want to use once, normal functions are away better and simpler option.
354 |
355 | >Fun fact:
356 | Python language has 250 classes. That means that most Python programs should use fewer or no class at all. Interesting [watch](https://www.youtube.com/watch?v=o9pEzgHorH0) on why you should not use class!!
357 |
358 | ### [BACK TO TOP](#0)
359 |
360 |
361 | ```python
362 |
363 | ```
364 |
--------------------------------------------------------------------------------
/markdowns/recursion.md:
--------------------------------------------------------------------------------
1 | ## Recursion
2 |
3 |
4 | *******
5 |
6 | **Content:**
7 | * [Introduction](#1)
8 | * [Example of Recursive Functions](#2)
9 | * [The Downside of Recursion](#3)
10 | * [References](#4)
11 | *******
12 |
13 |
14 | ### 1. Introduction
15 |
16 | * Recursion is a powerful technique that can be used to solve complex and long tail problems.
17 |
18 | * Generally speaking, recursion is a process of defining and solving a problem in terms of simpler versions of itself(the problem).
19 |
20 | * Algorithmic speaking, recursion is a technique used to design solutions to problems by using `divide-and-conquer` or `decrease-and-conquer` to reduce a given problem to simpler versions of the same problem.
21 |
22 | * Functional speaking, recursion is a programming technique in which a function calls itself repetitively.
23 |
24 | * Mathematically speaking, a recursive function is a function which is defined in itself. Ex: `f1(n) = f1(n-1) + f1(n-2)`, `f2(n) = n * f(n-1)`.
25 |
26 | * A good description for problems that can be solved with recursion are problems that can be divided into many similar subproblems.
27 |
28 | * Recursion is essentially based on `divide and conquer` technique where we divide the problem into different subproblems, solve each subproblem recursively, and combine the results to form the final solution.
29 |
30 | * While solving a problem recursively might make use of fewer codes, it time and space inefficient. The space of recursive problems is often O(n), where n is the depth or total number of recursive calls. Also equivalent to O(2k), where k is the number of the nodes that have children in a recursive tree. Plotting the tree is the best way to find the time complexity of the recursive function.
31 |
32 | * Most recursive problems can be solved iteratively with `for` and `while` loops. While recursion might make use of few codes than iterative approach, the iterative approach might take less computation time and space. There a fair trade-off between recursion and iterative approach.
33 |
34 | * A recursive function must have at least one `base case` that defines a solution to the special and simplest case of the problem. If there is no base case, the function can run infinitely and that's not good(computer's memory is a finite resource).
35 |
36 | * In addition to the `base case`, a recursive function should have one or more `recursive (inductive) cases` that defines the solution of the problem on the other simpler versions of the problem.
37 |
38 |
39 | ### 2. Example of Recursive Functions
40 |
41 | #### 2.0 Types of Recursion
42 |
43 | * We have five general types of recursive functions typically: Tail recursion, Head recursion, Tree recursion, Indirect recursion and Nested recursion.
44 | ##### 2.0.1 Tail recursion
45 |
46 | * In the tail recursive function the recursive is the last statement to the recursive function.
47 | * This recursion can be easily implemented as a loop and algorithmic thinking you can preferably choose the loop as they can have same runtime complexity but for space complexity the loop will be much efficient over tail recursion.
48 | * Let's take an example of printing first x natural numbers from highest
49 | * Using a loop
50 | ```python
51 | def print_x_num(x: int) -> None:
52 | while x > 0:
53 | print(x)
54 | x = x -1
55 | ```
56 | * Tail recursion approach
57 | ```python
58 | def print_x_num(x: int) -> None:
59 | if x > 0:
60 | print(x)
61 | print_x_num(x-1)
62 | ```
63 | ##### 2.0.2 Head recursion
64 |
65 | * In head recursive function the recursive call is the first statement to the recursive function.
66 | * It is hard to implement it directly using a loop
67 | * Let's take an example of printing first x natural numbers from the lowest.
68 | * Using a loop
69 | ```python
70 | def print_x_num(x: int) -> None:
71 | i = 1
72 | while i <= x:
73 | print(i)
74 | i += 1
75 | ```
76 | * Head Recursion approach
77 | ```python
78 | def print_x_num(x: int) -> None:
79 | if x > 0:
80 | print_x_num(x - 1)
81 | print(x)
82 | ```
83 |
84 | ##### 2.0.3 Tree recursion
85 | * In tree recursive the function calls itself more than one times
86 | * A typical example is finding the nth Fibonacci number
87 | * Recursive approach
88 | ```python
89 | def fibo(n: int) -> int:
90 | if n == 0 or n == 1:
91 | return n
92 | else:
93 | return fibo(n-1) + fibo(n-2)
94 | ```
95 |
96 | ##### 2.0.4 Indirect Recursion
97 | * In indirect recursive function there may be more than one function calling one another in a circular fashion.
98 | * Let's take an example to print a pattern 20 19 9 8 4 3 1 if passed n as 20
99 | ```python
100 | def funA(n: int) -> None:
101 | if n > 0:
102 | print(n)
103 | funB(n-1)
104 | def funB(n: int) -> None:
105 | if n > 0:
106 | print(n)
107 | funA(int(n/2))
108 | ```
109 | * Output
110 |
111 | 
112 | ***Image: The recursion tree of `funA(20)`. Image by author.***
113 |
114 | ##### 2.0.5 Nested recursion
115 | * In nested recursion the recursive call parameter is passed as a recursive call
116 | * Example
117 | ```python
118 | def nest_rec(n: int) -> int:
119 | if n > 100:
120 | return n - 10
121 | else:
122 | return nest_rec(nest_rec(n + 11))
123 | ```
124 |
125 | #### 2.1 Multiplication of two numbers
126 |
127 | * Given two numbers, return their multiplication without using arthimetic operator of `*`.
128 | * Solution: Multiplying a and b is basically like adding a to itself b times. Ex: `5 * 4 = 5 + 5 + 5 + 5 = 5 + (4-1)`
129 |
130 | * Using iterative approach:
131 |
132 | ```python
133 | def multi_iter(a, b):
134 |
135 | ans = 0
136 | while b > 0:
137 | ans += a
138 | b -= 1
139 |
140 | return ans
141 | ```
142 |
143 | * Using recursive approach:
144 |
145 | ```python
146 | def multi(a, b):
147 | """
148 | Edge case: b = 1
149 | """
150 |
151 | if b == 1:
152 | return a
153 | else:
154 | return a + multi(a, b - 1)
155 | ```
156 |
157 | #### 2.2 Factorial
158 |
159 | * Factorial is the hello world of recursion.
160 | * The factorial of a number n is equal to `n! = n * (n - 1)!`, ex: `7! = 7 * (7-1)!`
161 | * Iterative approach:
162 |
163 | ```python
164 | def fact_iter(n):
165 |
166 | fact = 1
167 |
168 | while n > 0:
169 | fact *= n
170 | n -= 1
171 | return fact
172 | ```
173 | * Recursive approach: Base case if for `1!` and `0!`. Factorial of 1 and 0 are 1.
174 |
175 | ```python
176 | def factorial(n):
177 | """
178 | Base case: 1! = 1, 0! = 1
179 | """
180 | if n == 1 or n == 0:
181 | return 1
182 |
183 | else:
184 | return n * factorial(n - 1)
185 | ```
186 |
187 | #### 2.3 Fibonacci numbers
188 |
189 | * The basecase for fibonnaci numbers are at 1 and 0: `fib(1) = 1`, `fib(0) = 0`.
190 |
191 | * `fib(n) = fib(n - 1) + fib(n - 2)`
192 |
193 | ```python
194 | def fibo(n):
195 | if n == 0 or n == 1:
196 | return n
197 |
198 | else:
199 | return fibo(n - 1) + fibo(n - 2)
200 | ```
201 |
202 | #### 2.4 Palindrome Strings
203 |
204 | * Recursion can also be applied on strings.
205 | * Example: Given a string, check whether the string is palindrome. Palindrome string can be read same way backward and forward. Ignore all punctuations and blank spaces.
206 |
207 | * For a string to be palindrome, the first and the last characters must be similar. We can thus reduce the string each step removing the first and last characters, checking if first & last characters are equal until the string remains with one character which is the `base case`.
208 |
209 | ```python
210 | def is_palindrome(stri):
211 |
212 | # Remove the spaces and blanks
213 |
214 | stri = stri.lower()
215 | chars = 'abcdefghijklmnopqrstuvwxyz'
216 |
217 | stri_cleaned = ''
218 |
219 | for c in stri:
220 | if c in chars:
221 |
222 | stri_cleaned += c
223 |
224 | if len(stri_cleaned) <= 1:
225 | return True
226 | else:
227 | return stri_cleaned[0] == stri_cleaned[-1] and is_palindrome(stri_cleaned[1:-1])
228 |
229 | ```
230 |
231 |
232 | ### 3. The Downside of Recursion
233 |
234 | * As we saw in the beginning, recursion is a powerful technique but it takes lots of time and space. More precisely, recursive functions take O(2n) exponential time, where n is the number of nodes in a recursive tree and the total depth of the tree is 2n. The runtime of recursive function increases exponentially as n increases.
235 |
236 | * Let's take an example for the recursive function of fibonacci numbers we did before:
237 |
238 | ```python
239 | def fibo(n):
240 | if n == 0 or n == 1:
241 | return n
242 |
243 | else:
244 | return fibo(n - 1) + fibo(n - 2)
245 | ```
246 |
247 | * The fibonnaci of small values of n is not so hard to compute but for large n, it's extremely time and space exhaustive. Take an example: fibo(120) is`5358359254990966640871840`. It can take `thousands of years years` to compute that per time complexity analysis. Try running it!!
248 |
249 | * For some problems, recursion might be all you need. But for other problems, we may need something much more. We need a more optimal way.
250 |
251 | * Dynamic programming is an efficient technique used to solve problems that can be divided into similar subproblems. It's essentially taking the recursive algorithm and finding the overlapping subproblems(similar or repeated calls) and then caching the results of those repeated calls to be used later rather than recomputing them multiple times.
252 |
253 | * Taking an example: the `fibo(5)` will have `3 fibo(2)` and 2 `fibo(3)` repeated calls. How can we cache the results of those repeated calls to use them later so as to save time and space?
254 |
255 | 
256 | ***Image: The recursive tree of `fibo(5)`. Image by author.***
257 |
258 | * More about dynamic programming later!!
259 |
260 |
261 | ### 4. References
262 |
263 | * Introduction to Computation and Programming Using Python, John V. Guttag
264 | #### [BACK TO TOP](#0)
--------------------------------------------------------------------------------
/notebooks/.ipynb_checkpoints/oop-classes-objects-checkpoint.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "id": "759e8db1",
6 | "metadata": {},
7 | "source": [
8 | "# Object Oriented Programming (OOP) - Classes and Objects"
9 | ]
10 | },
11 | {
12 | "cell_type": "markdown",
13 | "id": "94457c02",
14 | "metadata": {},
15 | "source": [
16 | "Object Oriented Programming(OOP) is a programming paradigm or feature that allows developers to structure programs into classes and objects. OOP gives the developers the ability to design programs around data.\n",
17 | "\n",
18 | "Classes are new data type that you can create yourself. Objects can be created from the Class. A typical class has two components: `fields` (or variables) that store data and `methods`(or functions) that operate on those data. Python [glossary](https://docs.python.org/3/glossary.html) defines class as a template for creating user defined objects and object\n",
19 | "as any data with state(attribute or value) and defined behavior(methods).\n",
20 | "\n",
21 | "With the simplicity of Python and the thousands of features it provides that can be used on the fly, the majority of Python developers doesn't need to use OOP. However, it is important to understand OOP so that you can understand when and when not to use it. OOP provides modularity and reusability. Take an example: Python list is a built-in object. Imagine how many time you(and other millions of coders) use list with different data!! That's extreme reusability!!\n",
22 | "\n",
23 | "When should you use class or function? This is a hard questions, but generally if you found yourself using a number of functions over and over on same data, then you should consider using class for reusability. If on the otherhand you are creating a class with methods that you will use once, it's probably better to use normal functions. Here is a great talk about that: [Stop Using Classes ](https://www.youtube.com/watch?v=o9pEzgHorH0) by Jack Diederich. You can also learn more about the critism of OOP [here](https://en.wikipedia.org/wiki/Object-oriented_programming#Criticism).\n",
24 | "\n",
25 | "Also, if you plan to build an API(Application Protocol Interface) one day, it's very likely that you will need to structure data and methods in a systematic way. So, OOP is an important thing to know. "
26 | ]
27 | },
28 | {
29 | "cell_type": "markdown",
30 | "id": "ade5e5ba",
31 | "metadata": {},
32 | "source": [
33 | "In Python, everything is an `object` and has a `type`. OOP gives us the power to create our custom object of a given data type that can be manipulated. Below is an example of how everything in Python is an object of a certain type."
34 | ]
35 | },
36 | {
37 | "cell_type": "code",
38 | "execution_count": 1,
39 | "id": "a6e3559f",
40 | "metadata": {},
41 | "outputs": [
42 | {
43 | "data": {
44 | "text/plain": [
45 | "list"
46 | ]
47 | },
48 | "execution_count": 1,
49 | "metadata": {},
50 | "output_type": "execute_result"
51 | }
52 | ],
53 | "source": [
54 | "# In Python, everything is an object of a given type\n",
55 | "\n",
56 | "my_list = [1, 2, 3, 4]\n",
57 | "\n",
58 | "type(my_list)"
59 | ]
60 | },
61 | {
62 | "cell_type": "markdown",
63 | "id": "1f464326",
64 | "metadata": {},
65 | "source": [
66 | "The type of Python `list` is a `type` too. This is the same thing for other objects such as `str` and `dict`. The `type` of `type` object is a `type` too :)"
67 | ]
68 | },
69 | {
70 | "cell_type": "code",
71 | "execution_count": 2,
72 | "id": "bcc3e303",
73 | "metadata": {},
74 | "outputs": [
75 | {
76 | "data": {
77 | "text/plain": [
78 | "type"
79 | ]
80 | },
81 | "execution_count": 2,
82 | "metadata": {},
83 | "output_type": "execute_result"
84 | }
85 | ],
86 | "source": [
87 | "type(list)"
88 | ]
89 | },
90 | {
91 | "cell_type": "code",
92 | "execution_count": 3,
93 | "id": "5ed07629",
94 | "metadata": {},
95 | "outputs": [
96 | {
97 | "data": {
98 | "text/plain": [
99 | "type"
100 | ]
101 | },
102 | "execution_count": 3,
103 | "metadata": {},
104 | "output_type": "execute_result"
105 | }
106 | ],
107 | "source": [
108 | "type(str)"
109 | ]
110 | },
111 | {
112 | "cell_type": "code",
113 | "execution_count": 4,
114 | "id": "b0794603",
115 | "metadata": {},
116 | "outputs": [
117 | {
118 | "data": {
119 | "text/plain": [
120 | "type"
121 | ]
122 | },
123 | "execution_count": 4,
124 | "metadata": {},
125 | "output_type": "execute_result"
126 | }
127 | ],
128 | "source": [
129 | "type(type)"
130 | ]
131 | },
132 | {
133 | "cell_type": "markdown",
134 | "id": "9c57f9fc",
135 | "metadata": {},
136 | "source": [
137 | "There is a popular notion that OOP allows developers to represent real world objects into software objects. To some extent, this is true. Real world objects contain state and behavior. Take an example of a car object: The state of a car might be price, model, car maker, speed while the behavior might be changing the gear, applying the brake, etc...). \n",
138 | "\n",
139 | "Software objects provide an abstraction of the real world objects. Software objects store their states in data attributes(or variables), and they expose their behaviors through method attributes(or functions). Methods act on the state of the objects. Java documentation has a great explaination of class and objects in the contexts of the real world objects. For more, check it [here](https://docs.oracle.com/javase/tutorial/java/concepts/object.html).\n",
140 | "\n",
141 | "\n",
142 | "Let's see how to create a class in Python."
143 | ]
144 | },
145 | {
146 | "cell_type": "markdown",
147 | "id": "fcf31cc9",
148 | "metadata": {},
149 | "source": [
150 | "### Creating a Class in Python"
151 | ]
152 | },
153 | {
154 | "cell_type": "markdown",
155 | "id": "fe17e196",
156 | "metadata": {},
157 | "source": [
158 | "Classes are user defined objects. That signal that there are built-in classes that we use day to day contained in some [standard libraries](https://docs.python.org/3/tutorial/stdlib.html). Take an example for a `list`. A list is a class. When we create a list object, we can manipulate it in different ways."
159 | ]
160 | },
161 | {
162 | "cell_type": "code",
163 | "execution_count": 5,
164 | "id": "0049b8ed",
165 | "metadata": {},
166 | "outputs": [
167 | {
168 | "name": "stdout",
169 | "output_type": "stream",
170 | "text": [
171 | "[1, 2, 3, 4]\n"
172 | ]
173 | }
174 | ],
175 | "source": [
176 | "# Creating a list object, \n",
177 | "# The data [1,2,3] is an instance of the list\n",
178 | "\n",
179 | "my_list = [1, 2, 3]\n",
180 | "\n",
181 | "# We can manipulate the list object\n",
182 | "\n",
183 | "my_list.append(4)\n",
184 | "\n",
185 | "print(my_list)"
186 | ]
187 | },
188 | {
189 | "cell_type": "markdown",
190 | "id": "8825e2de",
191 | "metadata": {},
192 | "source": [
193 | "Below is a simple user defined class:\n",
194 | "\n",
195 | "```python\n",
196 | "class MyClass:\n",
197 | " \"\"\"A simple example class\"\"\"\n",
198 | " i = 12345\n",
199 | "\n",
200 | " def f(self):\n",
201 | " return 'hello world'\n",
202 | "```\n",
203 | "\n",
204 | "When creating a new class:\n",
205 | "* We specity its class name. It is common to use Capital letters for defining the class like `ClassName`. This style is called `CamelCase`. \n",
206 | "\n",
207 | "```python\n",
208 | "\n",
209 | "class ClassName:\n",
210 | " ## Class attributes\n",
211 | " \n",
212 | "```\n",
213 | "\n",
214 | "* We define the class attributes(data and methods). Data attributes are data (or variables) that makes up the class and the methods are like functions that operate on data. Methods only work with class. \n",
215 | "\n",
216 | "* We use a special method `__init__` to initialize the data attributes. `__init__` method starts and ends with double underscore(`__`). When passing data to the init method, the first paremeter must be `self`. There are other special methods beyond `__init__`.\n",
217 | "\n",
218 | "```python\n",
219 | "\n",
220 | "class ClassName:\n",
221 | " \n",
222 | " def __init__(self, x, y):\n",
223 | "```\n",
224 | " \n",
225 | "\n",
226 | "* We use `self` as the first parameter for any method that we define in the class. With the exception of `self` parameter, methods are similar to normal functions. `self` parameter allows the method to use the class data attributes.\n",
227 | "\n",
228 | "* To access the data attribute and method of an object, we use `. operator`.\n",
229 | "\n",
230 | "\n",
231 | "```python\n",
232 | "\n",
233 | "class ClassName:\n",
234 | " \n",
235 | " def __init__(self, var1, var2):\n",
236 | " self.var1 = var1\n",
237 | " self.var2 = var2\n",
238 | " \n",
239 | " def method_1(self, var3):\n",
240 | " \"\"\"\n",
241 | " DOCSTRING\n",
242 | " self points to the data attributes, \n",
243 | " function can take new data attribute like var3\n",
244 | " \"\"\"\n",
245 | " \n",
246 | " #statements\n",
247 | " \n",
248 | "```\n",
249 | "Now that we understand what makes a class, let's create a real class."
250 | ]
251 | },
252 | {
253 | "cell_type": "code",
254 | "execution_count": 6,
255 | "id": "c23c5413",
256 | "metadata": {},
257 | "outputs": [],
258 | "source": [
259 | "class Car:\n",
260 | " \n",
261 | " def __init__(self, speed, gear, model, maker):\n",
262 | " \n",
263 | " self.speed = speed\n",
264 | " self.gear = gear\n",
265 | " self.model = model\n",
266 | " self.maker = maker\n",
267 | " \n",
268 | " def speed_up(self, add_speed):\n",
269 | " \n",
270 | " speed = self.speed + add_speed\n",
271 | " \n",
272 | " return speed\n",
273 | " \n",
274 | " def apply_brake(self, decre_speed):\n",
275 | " speed = self.speed - decre_speed\n",
276 | " \n",
277 | " return speed\n",
278 | " \n",
279 | " def change_gear(self, new_gear):\n",
280 | " gear = new_gear\n",
281 | " \n",
282 | " return gear\n",
283 | " \n",
284 | " def car_info(self):\n",
285 | " \n",
286 | " print(f\"Car Info:\\n \\\n",
287 | " Speed: {self.speed}\\n \\\n",
288 | " Model: {self.model}\\n \\\n",
289 | " Maker: {self.maker}\")"
290 | ]
291 | },
292 | {
293 | "cell_type": "markdown",
294 | "id": "3c80c6ad",
295 | "metadata": {},
296 | "source": [
297 | "That's an example of class. In the next section, let's see how to use the class we created."
298 | ]
299 | },
300 | {
301 | "cell_type": "markdown",
302 | "id": "e8b540aa",
303 | "metadata": {},
304 | "source": [
305 | "### Using a Class"
306 | ]
307 | },
308 | {
309 | "cell_type": "markdown",
310 | "id": "fbf6935e",
311 | "metadata": {},
312 | "source": [
313 | "To create the instance of the object, we simply call class we created with the values of the data fields. Note that we don't pass `self` when calling the class."
314 | ]
315 | },
316 | {
317 | "cell_type": "code",
318 | "execution_count": 7,
319 | "id": "dccdac1a",
320 | "metadata": {},
321 | "outputs": [],
322 | "source": [
323 | "my_car = Car(45, 2, 'Model S', 'Tesla')"
324 | ]
325 | },
326 | {
327 | "cell_type": "code",
328 | "execution_count": 8,
329 | "id": "8bd9d4aa",
330 | "metadata": {},
331 | "outputs": [
332 | {
333 | "data": {
334 | "text/plain": [
335 | "2"
336 | ]
337 | },
338 | "execution_count": 8,
339 | "metadata": {},
340 | "output_type": "execute_result"
341 | }
342 | ],
343 | "source": [
344 | "# Accessing the data attributes\n",
345 | "\n",
346 | "my_car.gear"
347 | ]
348 | },
349 | {
350 | "cell_type": "code",
351 | "execution_count": 9,
352 | "id": "4f05c849",
353 | "metadata": {},
354 | "outputs": [
355 | {
356 | "data": {
357 | "text/plain": [
358 | "45"
359 | ]
360 | },
361 | "execution_count": 9,
362 | "metadata": {},
363 | "output_type": "execute_result"
364 | }
365 | ],
366 | "source": [
367 | "my_car.speed"
368 | ]
369 | },
370 | {
371 | "cell_type": "code",
372 | "execution_count": 10,
373 | "id": "cfd4dd64",
374 | "metadata": {},
375 | "outputs": [
376 | {
377 | "data": {
378 | "text/plain": [
379 | "'Tesla'"
380 | ]
381 | },
382 | "execution_count": 10,
383 | "metadata": {},
384 | "output_type": "execute_result"
385 | }
386 | ],
387 | "source": [
388 | "my_car.maker"
389 | ]
390 | },
391 | {
392 | "cell_type": "markdown",
393 | "id": "63a1c4fc",
394 | "metadata": {},
395 | "source": [
396 | "To use the methods we defined in class, we do as we always do with built-in objects (ex: `my_list.append(2)`). We use `. operator` and we pass the variables that make up a particular method. Note that we don't use `self` when calling the methods defined in the class."
397 | ]
398 | },
399 | {
400 | "cell_type": "code",
401 | "execution_count": 11,
402 | "id": "16a13fd0",
403 | "metadata": {},
404 | "outputs": [
405 | {
406 | "data": {
407 | "text/plain": [
408 | "35"
409 | ]
410 | },
411 | "execution_count": 11,
412 | "metadata": {},
413 | "output_type": "execute_result"
414 | }
415 | ],
416 | "source": [
417 | "my_car.apply_brake(10)\n",
418 | "\n",
419 | "# Should return speed - 10 = 45 - 10 = 35"
420 | ]
421 | },
422 | {
423 | "cell_type": "code",
424 | "execution_count": 12,
425 | "id": "e4d8a99e",
426 | "metadata": {},
427 | "outputs": [
428 | {
429 | "data": {
430 | "text/plain": [
431 | "65"
432 | ]
433 | },
434 | "execution_count": 12,
435 | "metadata": {},
436 | "output_type": "execute_result"
437 | }
438 | ],
439 | "source": [
440 | "my_car.speed_up(20)"
441 | ]
442 | },
443 | {
444 | "cell_type": "code",
445 | "execution_count": 13,
446 | "id": "b4aee1e3",
447 | "metadata": {},
448 | "outputs": [
449 | {
450 | "data": {
451 | "text/plain": [
452 | "4"
453 | ]
454 | },
455 | "execution_count": 13,
456 | "metadata": {},
457 | "output_type": "execute_result"
458 | }
459 | ],
460 | "source": [
461 | "my_car.change_gear(4)"
462 | ]
463 | },
464 | {
465 | "cell_type": "code",
466 | "execution_count": 14,
467 | "id": "67536218",
468 | "metadata": {},
469 | "outputs": [
470 | {
471 | "name": "stdout",
472 | "output_type": "stream",
473 | "text": [
474 | "Car Info:\n",
475 | " Speed: 45\n",
476 | " Model: Model S\n",
477 | " Maker: Tesla\n"
478 | ]
479 | }
480 | ],
481 | "source": [
482 | "my_car.car_info()"
483 | ]
484 | },
485 | {
486 | "cell_type": "markdown",
487 | "id": "84ba7cdf",
488 | "metadata": {},
489 | "source": [
490 | "When you print `my_car` object(created from class `Car`), you don't see the information about the class other than that it is an object. "
491 | ]
492 | },
493 | {
494 | "cell_type": "code",
495 | "execution_count": 15,
496 | "id": "c713f643",
497 | "metadata": {},
498 | "outputs": [
499 | {
500 | "name": "stdout",
501 | "output_type": "stream",
502 | "text": [
503 | "<__main__.Car object at 0x10371c2b0>\n"
504 | ]
505 | }
506 | ],
507 | "source": [
508 | "print(my_car)"
509 | ]
510 | },
511 | {
512 | "cell_type": "markdown",
513 | "id": "b05229e0",
514 | "metadata": {},
515 | "source": [
516 | "To choose what will be diplayed when we print the object, we can define a `__str__` method inside the class. The `__str__` method must return a string. Otherwise, you will get an error."
517 | ]
518 | },
519 | {
520 | "cell_type": "code",
521 | "execution_count": 16,
522 | "id": "a0e4c1f4",
523 | "metadata": {},
524 | "outputs": [],
525 | "source": [
526 | "class Car:\n",
527 | " \n",
528 | " def __init__(self, speed, gear, model, maker):\n",
529 | " \n",
530 | " self.speed = speed\n",
531 | " self.gear = gear\n",
532 | " self.model = model\n",
533 | " self.maker = maker\n",
534 | " \n",
535 | " def speed_up(self, add_speed):\n",
536 | " \n",
537 | " speed = self.speed + add_speed\n",
538 | " \n",
539 | " return speed\n",
540 | " \n",
541 | " def apply_brake(self, decre_speed):\n",
542 | " speed = self.speed - decre_speed\n",
543 | " \n",
544 | " return speed\n",
545 | " \n",
546 | " def change_gear(self, new_gear):\n",
547 | " gear = new_gear\n",
548 | " \n",
549 | " return gear\n",
550 | " \n",
551 | " def car_info(self):\n",
552 | " \n",
553 | " print(f\"Car Info:\\n \\\n",
554 | " Speed: {self.speed}\\n \\\n",
555 | " Model: {self.model}\\n \\\n",
556 | " Maker: {self.maker}\")\n",
557 | " def __str__(self):\n",
558 | " \n",
559 | " return f\"This is the object created from class Car. The car model {self.model} was made by {self.maker}\""
560 | ]
561 | },
562 | {
563 | "cell_type": "code",
564 | "execution_count": 17,
565 | "id": "c4f96c8f",
566 | "metadata": {},
567 | "outputs": [
568 | {
569 | "name": "stdout",
570 | "output_type": "stream",
571 | "text": [
572 | "This is the object created from class Car. The car model Model X was made by Tesla\n"
573 | ]
574 | }
575 | ],
576 | "source": [
577 | "my_car = Car(45, 2, 'Model X', 'Tesla')\n",
578 | "print(my_car)"
579 | ]
580 | },
581 | {
582 | "cell_type": "markdown",
583 | "id": "127ad7a9",
584 | "metadata": {},
585 | "source": [
586 | "The methods `__str__` and `__init__` are examples of special or magic methods. Python special methods allows us to overload(or to imitate) built-in operators so that we can use them in our objects just like built-in data types. Magic methods are unique to Python and they are not found in other OOP languages such as Java and C++. We will learn more about magic methods later. \n",
587 | "\n",
588 | "You can find all magics methods on [Python Documentation](https://docs.python.org/3/reference/datamodel.html#basic-customization)."
589 | ]
590 | },
591 | {
592 | "cell_type": "markdown",
593 | "id": "58058236",
594 | "metadata": {},
595 | "source": [
596 | "One last thing: we can use `isinstance()` to check if the object `my_car` is a Car type."
597 | ]
598 | },
599 | {
600 | "cell_type": "code",
601 | "execution_count": 18,
602 | "id": "5665c722",
603 | "metadata": {},
604 | "outputs": [
605 | {
606 | "data": {
607 | "text/plain": [
608 | "True"
609 | ]
610 | },
611 | "execution_count": 18,
612 | "metadata": {},
613 | "output_type": "execute_result"
614 | }
615 | ],
616 | "source": [
617 | "isinstance(my_car, Car)"
618 | ]
619 | },
620 | {
621 | "cell_type": "markdown",
622 | "id": "3f82aa0c",
623 | "metadata": {},
624 | "source": [
625 | "### Final Notes and Further Learning"
626 | ]
627 | },
628 | {
629 | "cell_type": "markdown",
630 | "id": "fb3907f0",
631 | "metadata": {},
632 | "source": [
633 | "The central idea of OOP is creating user-defined objects. With OOP, you can create structured and systematic programs. OOP provides modularity and code reusability which are essential ingredients for building large scale programs.\n",
634 | "\n",
635 | "In the next parts, we will learn about inheritance and special or magic methods.\n",
636 | "\n",
637 | "You can learn more about OOP and classes and Objects at:\n",
638 | "\n",
639 | "* OOP, Lecture 8 of [MIT 6.0001 Introduction to Computer Science and Programming in Python](https://www.youtube.com/watch?v=-DP1i2ZU9gk)\n",
640 | "* [Python Doc](https://docs.python.org/3/tutorial/classes.html)\n",
641 | "* [Wikipedia, OOP](https://en.wikipedia.org/wiki/Object-oriented_programming)\n",
642 | "* [Stop Using Classes](https://www.youtube.com/watch?v=o9pEzgHorH0)"
643 | ]
644 | },
645 | {
646 | "cell_type": "code",
647 | "execution_count": null,
648 | "id": "332793dd",
649 | "metadata": {},
650 | "outputs": [],
651 | "source": []
652 | }
653 | ],
654 | "metadata": {
655 | "kernelspec": {
656 | "display_name": "Python 3 (ipykernel)",
657 | "language": "python",
658 | "name": "python3"
659 | },
660 | "language_info": {
661 | "codemirror_mode": {
662 | "name": "ipython",
663 | "version": 3
664 | },
665 | "file_extension": ".py",
666 | "mimetype": "text/x-python",
667 | "name": "python",
668 | "nbconvert_exporter": "python",
669 | "pygments_lexer": "ipython3",
670 | "version": "3.9.7"
671 | }
672 | },
673 | "nbformat": 4,
674 | "nbformat_minor": 5
675 | }
676 |
--------------------------------------------------------------------------------
/notebooks/.ipynb_checkpoints/oop-inheritance-checkpoint.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "id": "0da0dfc1",
6 | "metadata": {},
7 | "source": [
8 | "\n",
9 | "\n",
10 | "# Object Oriented Programming(OOP) - Inheritance"
11 | ]
12 | },
13 | {
14 | "cell_type": "markdown",
15 | "id": "08b3943e",
16 | "metadata": {},
17 | "source": [
18 | "Contents:\n",
19 | "\n",
20 | "- [How to Create a Child Class from a Parent Class](#1)\n",
21 | "- [Addind New Data and Methods to a Child Class](#2)\n",
22 | "- [Method Overriding](#3)\n",
23 | "- [Checking if Child Class Belongs to Parent Class](#4)\n",
24 | "- [Class Methods, Class Attributes, Static Methods](#5)\n",
25 | "- [Final Notes¶](#6)\n",
26 | " "
27 | ]
28 | },
29 | {
30 | "cell_type": "markdown",
31 | "id": "c8fef721",
32 | "metadata": {},
33 | "source": [
34 | "Real world objects that are in the same class/category share similar behaviors. OOP classes can also have things in common where rather than creating new classes, classes can be inherited from other class.\n",
35 | "\n",
36 | "Also, similar to how we write a normal functions once and reuse them multiple times without having to recreate them again, we can inherit methods and attributes of the parent class using inheritance. \n",
37 | "\n",
38 | "\n",
39 | "Inheritance is an OOP based techniques that refers to `parent-child relationship` where a child class can be inherited from the parent class. The child class will have the access of data and methods of the parent class (but not the other way).\n",
40 | "\n",
41 | "Inheritance is essentially deriving a new class(child class) from an existing class(parent class). A parent class is also called base class or super class, while the child class is called derived class."
42 | ]
43 | },
44 | {
45 | "cell_type": "markdown",
46 | "id": "c3f0cee3",
47 | "metadata": {},
48 | "source": [
49 | "\n",
50 | "\n",
51 | "## 1. How to Create a Child Class from a Parent Class"
52 | ]
53 | },
54 | {
55 | "cell_type": "markdown",
56 | "id": "dfeaeea3",
57 | "metadata": {},
58 | "source": [
59 | "When deriving a child class, you simply define the parent class in the child class. Below is a syntax for inheritance:\n",
60 | "\n",
61 | "\n",
62 | "```python\n",
63 | "class ChildClassName(ParentClassName):\n",
64 | " .\n",
65 | " .\n",
66 | " .\n",
67 | " .\n",
68 | " .\n",
69 | " .\n",
70 | " .\n",
71 | " \n",
72 | " \n",
73 | "```\n",
74 | "\n",
75 | "If you simply want the child class to inherit all data and method attributes without adding or overriding any method of the parent class, simply do the following:\n",
76 | "\n",
77 | "\n",
78 | "```python\n",
79 | "class ChildClassName(ParentClassName):\n",
80 | " pass\n",
81 | "```\n",
82 | "\n",
83 | "Let's take a real example. But we will create a parent class first."
84 | ]
85 | },
86 | {
87 | "cell_type": "code",
88 | "execution_count": 8,
89 | "id": "e8cc33bd",
90 | "metadata": {},
91 | "outputs": [],
92 | "source": [
93 | "class Vehicle:\n",
94 | " \n",
95 | " def __init__ (self, brandmaker, model, speed):\n",
96 | " \n",
97 | " self.brandmaker = brandmaker\n",
98 | " self.model = model\n",
99 | " self.speed = speed\n",
100 | " \n",
101 | " def speed_up(self, add_speed):\n",
102 | " \n",
103 | " new_speed = self.speed + add_speed\n",
104 | " \n",
105 | " return new_speed\n",
106 | " \n",
107 | " def vehicle_info(self):\n",
108 | " \n",
109 | " print(f\"Vehicle Info:\\n \\\n",
110 | " Current Speed: {self.speed}\\n \\\n",
111 | " Vehicle Model: {self.model}\\n \\\n",
112 | " Vehicle Maker: {self.brandmaker}\")"
113 | ]
114 | },
115 | {
116 | "cell_type": "markdown",
117 | "id": "b56f9b99",
118 | "metadata": {},
119 | "source": [
120 | "From the `Vehicle` class, we can derive a new child class `Bicycle`. If we only want to inherit all data and method attributes, all we have to do is to `pass`."
121 | ]
122 | },
123 | {
124 | "cell_type": "code",
125 | "execution_count": 9,
126 | "id": "9f8d2a77",
127 | "metadata": {},
128 | "outputs": [],
129 | "source": [
130 | "class Car(Vehicle):\n",
131 | " pass"
132 | ]
133 | },
134 | {
135 | "cell_type": "code",
136 | "execution_count": 10,
137 | "id": "161bbd65",
138 | "metadata": {},
139 | "outputs": [
140 | {
141 | "name": "stdout",
142 | "output_type": "stream",
143 | "text": [
144 | "Vehicle Info:\n",
145 | " Current Speed: 40\n",
146 | " Vehicle Model: Model S\n",
147 | " Vehicle Maker: Tesla\n"
148 | ]
149 | }
150 | ],
151 | "source": [
152 | "car = Car('Tesla', 'Model S', 40)\n",
153 | "car.vehicle_info()"
154 | ]
155 | },
156 | {
157 | "cell_type": "markdown",
158 | "id": "db89e7dc",
159 | "metadata": {},
160 | "source": [
161 | "A child class inherited from the parent class will have an access to all parent class data and method attributes but if the child class have its own data and methods attributes, they can not be accessed in the parent class. It's only one direction."
162 | ]
163 | },
164 | {
165 | "cell_type": "markdown",
166 | "id": "5b7cbf85",
167 | "metadata": {},
168 | "source": [
169 | "\n",
170 | "\n",
171 | "## 2. Adding New Data and Methods to a Child Class"
172 | ]
173 | },
174 | {
175 | "cell_type": "markdown",
176 | "id": "b95ce625",
177 | "metadata": {},
178 | "source": [
179 | "In addition to the inherited data and method attributes, it's possible to add new data and methods to the class.\n",
180 | "\n",
181 | "To automatically get the access to the data and methods of the parent class, we can use the `super()` function. Below we use `super` for accessing the initialized parent class data through `__init__` method, but it can be used for almost any method."
182 | ]
183 | },
184 | {
185 | "cell_type": "code",
186 | "execution_count": 11,
187 | "id": "6bbe80a7",
188 | "metadata": {},
189 | "outputs": [],
190 | "source": [
191 | "class Car(Vehicle):\n",
192 | " \n",
193 | " def __init__(self, brandmaker, model, speed): #__init__ method is overriden: more on method overriding later\n",
194 | " super().__init__(brandmaker, model, speed) #use super() to automatically get the data and methods of parent class\n",
195 | " \n",
196 | " def speed_down(self, down_speed): #speed_down is a new method, down_speed is a new data/variable\n",
197 | " \n",
198 | " speed = self.speed + down_speed\n",
199 | " \n",
200 | " return speed"
201 | ]
202 | },
203 | {
204 | "cell_type": "code",
205 | "execution_count": 12,
206 | "id": "ed0fd05c",
207 | "metadata": {},
208 | "outputs": [
209 | {
210 | "data": {
211 | "text/plain": [
212 | "60"
213 | ]
214 | },
215 | "execution_count": 12,
216 | "metadata": {},
217 | "output_type": "execute_result"
218 | }
219 | ],
220 | "source": [
221 | "car = Car('Tesla', 'Model S', 40)\n",
222 | "car.speed_down(20)"
223 | ]
224 | },
225 | {
226 | "cell_type": "markdown",
227 | "id": "127fc01f",
228 | "metadata": {},
229 | "source": [
230 | "\n",
231 | "\n",
232 | "## Method Overriding"
233 | ]
234 | },
235 | {
236 | "cell_type": "markdown",
237 | "id": "634b1f92",
238 | "metadata": {},
239 | "source": [
240 | "A derived or child class inherits all methods of the parent class or base class. But there are times you may want to override the methods of the parent class in child class. All you have to do is to define the methods that have the same name as the parent class. Such method will be overriden!\n",
241 | "\n",
242 | "We saw that for __init__ method but let's demonstrate it for normal method."
243 | ]
244 | },
245 | {
246 | "cell_type": "code",
247 | "execution_count": 13,
248 | "id": "65465bfd",
249 | "metadata": {},
250 | "outputs": [],
251 | "source": [
252 | "class Bicycle(Vehicle):\n",
253 | " \n",
254 | " def vehicle_info(self):\n",
255 | " \n",
256 | " print(f\"Bicycle Info:\\n \\\n",
257 | " Current Speed: {self.speed}\\n \\\n",
258 | " Bicycle Model: {self.model}\\n \\\n",
259 | " Bicycle Maker: {self.brandmaker}\")"
260 | ]
261 | },
262 | {
263 | "cell_type": "code",
264 | "execution_count": 14,
265 | "id": "c5b1c851",
266 | "metadata": {},
267 | "outputs": [
268 | {
269 | "name": "stdout",
270 | "output_type": "stream",
271 | "text": [
272 | "Bicycle Info:\n",
273 | " Current Speed: 20\n",
274 | " Bicycle Model: Model XXXX\n",
275 | " Bicycle Maker: Bike Manufacturer\n"
276 | ]
277 | }
278 | ],
279 | "source": [
280 | "bicycle = Bicycle('Bike Manufacturer', 'Model XXXX', 20)\n",
281 | "bicycle.vehicle_info()"
282 | ]
283 | },
284 | {
285 | "cell_type": "markdown",
286 | "id": "5e613123",
287 | "metadata": {},
288 | "source": [
289 | "You can see that the method `vehicle_info()` was overriden. A one caveat to make here is that the things we are using are only just examples that demonstrate the concepts. In the real world, inheritance is more about code reuse than following hierachies. "
290 | ]
291 | },
292 | {
293 | "cell_type": "markdown",
294 | "id": "257370b0",
295 | "metadata": {},
296 | "source": [
297 | "\n",
298 | "\n",
299 | "## 3. Checking if Child Class Belongs to Parent Class"
300 | ]
301 | },
302 | {
303 | "cell_type": "markdown",
304 | "id": "0d40fb6e",
305 | "metadata": {},
306 | "source": [
307 | "We can check if an object is a type of a given class with `isinstance()` built-in function. It will return `True` if the object belong to the class, and otherwise `False`."
308 | ]
309 | },
310 | {
311 | "cell_type": "code",
312 | "execution_count": 23,
313 | "id": "f8ad7f88",
314 | "metadata": {},
315 | "outputs": [
316 | {
317 | "data": {
318 | "text/plain": [
319 | "True"
320 | ]
321 | },
322 | "execution_count": 23,
323 | "metadata": {},
324 | "output_type": "execute_result"
325 | }
326 | ],
327 | "source": [
328 | "isinstance(car, Vehicle)"
329 | ]
330 | },
331 | {
332 | "cell_type": "code",
333 | "execution_count": 24,
334 | "id": "e932f3b5",
335 | "metadata": {},
336 | "outputs": [
337 | {
338 | "data": {
339 | "text/plain": [
340 | "True"
341 | ]
342 | },
343 | "execution_count": 24,
344 | "metadata": {},
345 | "output_type": "execute_result"
346 | }
347 | ],
348 | "source": [
349 | "isinstance(bicycle, Vehicle)"
350 | ]
351 | },
352 | {
353 | "cell_type": "code",
354 | "execution_count": 25,
355 | "id": "75b531ad",
356 | "metadata": {},
357 | "outputs": [
358 | {
359 | "name": "stdout",
360 | "output_type": "stream",
361 | "text": [
362 | "Hello world\n"
363 | ]
364 | }
365 | ],
366 | "source": [
367 | "class TempClass:\n",
368 | " print('Hello world')\n",
369 | " \n",
370 | "class TempChild(TempClass):\n",
371 | " pass"
372 | ]
373 | },
374 | {
375 | "cell_type": "code",
376 | "execution_count": 26,
377 | "id": "f31b8a92",
378 | "metadata": {},
379 | "outputs": [
380 | {
381 | "data": {
382 | "text/plain": [
383 | "True"
384 | ]
385 | },
386 | "execution_count": 26,
387 | "metadata": {},
388 | "output_type": "execute_result"
389 | }
390 | ],
391 | "source": [
392 | "temp_obj = TempChild()\n",
393 | "isinstance(temp_obj, TempClass)"
394 | ]
395 | },
396 | {
397 | "cell_type": "code",
398 | "execution_count": 27,
399 | "id": "a101d6ab",
400 | "metadata": {},
401 | "outputs": [
402 | {
403 | "data": {
404 | "text/plain": [
405 | "False"
406 | ]
407 | },
408 | "execution_count": 27,
409 | "metadata": {},
410 | "output_type": "execute_result"
411 | }
412 | ],
413 | "source": [
414 | "isinstance(temp_obj, Vehicle)"
415 | ]
416 | },
417 | {
418 | "cell_type": "markdown",
419 | "id": "f8232f18",
420 | "metadata": {},
421 | "source": [
422 | "There is another built-in function `issubclass()` that can be used to check if the a particular child class is a subclass of a given parent class. The first argument in `issubclass()` must be a subclass and the later argument must be the parent class."
423 | ]
424 | },
425 | {
426 | "cell_type": "code",
427 | "execution_count": 21,
428 | "id": "236c1406",
429 | "metadata": {},
430 | "outputs": [
431 | {
432 | "data": {
433 | "text/plain": [
434 | "True"
435 | ]
436 | },
437 | "execution_count": 21,
438 | "metadata": {},
439 | "output_type": "execute_result"
440 | }
441 | ],
442 | "source": [
443 | "issubclass(Car, Vehicle)"
444 | ]
445 | },
446 | {
447 | "cell_type": "code",
448 | "execution_count": 28,
449 | "id": "851669d7",
450 | "metadata": {},
451 | "outputs": [
452 | {
453 | "data": {
454 | "text/plain": [
455 | "True"
456 | ]
457 | },
458 | "execution_count": 28,
459 | "metadata": {},
460 | "output_type": "execute_result"
461 | }
462 | ],
463 | "source": [
464 | "issubclass(Bicycle, Vehicle)"
465 | ]
466 | },
467 | {
468 | "cell_type": "code",
469 | "execution_count": 29,
470 | "id": "2f69b268",
471 | "metadata": {},
472 | "outputs": [
473 | {
474 | "data": {
475 | "text/plain": [
476 | "False"
477 | ]
478 | },
479 | "execution_count": 29,
480 | "metadata": {},
481 | "output_type": "execute_result"
482 | }
483 | ],
484 | "source": [
485 | "issubclass(TempChild, Vehicle)"
486 | ]
487 | },
488 | {
489 | "cell_type": "markdown",
490 | "id": "ad03d910",
491 | "metadata": {},
492 | "source": [
493 | "\n",
494 | "\n",
495 | "## 4. Multiple Inheritance"
496 | ]
497 | },
498 | {
499 | "cell_type": "markdown",
500 | "id": "e7a28b0b",
501 | "metadata": {},
502 | "source": [
503 | "Python being a very unique programming language supports multiple inheritance where a new child class can be inherited from multiple parent classes.\n",
504 | "\n",
505 | "\n",
506 | "Below is the template for inheriting from several number of parent classes:\n",
507 | "\n",
508 | "```python\n",
509 | "\n",
510 | "class ChildClass(ParentClass1, ParentClass2, ParentClass3, ParentClass4):\n",
511 | " \n",
512 | " #Statements\n",
513 | " \n",
514 | "```"
515 | ]
516 | },
517 | {
518 | "cell_type": "markdown",
519 | "id": "601787ae",
520 | "metadata": {},
521 | "source": [
522 | "It's rare that you will need to inherit from multiple classes, but if you happen to do, check the Python [doc](https://docs.python.org/3/tutorial/classes.html#multiple-inheritance) for more. "
523 | ]
524 | },
525 | {
526 | "cell_type": "markdown",
527 | "id": "a8c07f38",
528 | "metadata": {},
529 | "source": [
530 | "\n",
531 | "\n",
532 | "## 5. Class Methods, Class Attributes, Static Methods"
533 | ]
534 | },
535 | {
536 | "cell_type": "markdown",
537 | "id": "42046234",
538 | "metadata": {},
539 | "source": [
540 | "Class methods are type of methods that are very specific to the class instead of the objects of the class. Class methods are represented by `@classmethod` decorator above `def` definition. The first parameter of methods is `cls` that stands for `class`.\n",
541 | "\n",
542 | "Class methods can not access regular class methods or attributes.\n",
543 | "\n",
544 | "Here is a template of class method:\n",
545 | "\n",
546 | "```python\n",
547 | "\n",
548 | "class ClassName:\n",
549 | " \n",
550 | " def regular_method(self):\n",
551 | " #statements\n",
552 | " \n",
553 | " @classmethod\n",
554 | " def class_method(cls):\n",
555 | " #statements\n",
556 | " \n",
557 | "```\n",
558 | "\n",
559 | "Class methods can be called without creating the object of the class.\n",
560 | "\n",
561 | "```python\n",
562 | "ClassName.class_method()\n",
563 | "```"
564 | ]
565 | },
566 | {
567 | "cell_type": "markdown",
568 | "id": "dac88457",
569 | "metadata": {},
570 | "source": [
571 | "Just like class methods, class attributes are variables that are specific to the class not the objects of the class. Class attributes are like global variables, they are defined above all methods and they are not initialized in `__init_() method`.\n",
572 | "\n",
573 | "Here is a template for class attributes:\n",
574 | "\n",
575 | "```python\n",
576 | "class ClassName:\n",
577 | " num = 0\n",
578 | " \n",
579 | " # statements\n",
580 | " ```"
581 | ]
582 | },
583 | {
584 | "cell_type": "markdown",
585 | "id": "7293a8ea",
586 | "metadata": {},
587 | "source": [
588 | "Static methods are type of methods that don't have `self` or `cls` parameter and have `@staticmethod` decorator above `def` definition. Just like class methods, static methods can not access the class methods and attributes(variables).\n",
589 | "\n",
590 | "Below is a template for static methods:\n",
591 | "\n",
592 | "```python\n",
593 | "class ClassName:\n",
594 | " \n",
595 | " @staticmethod\n",
596 | " def static_method_name():\n",
597 | " #statements\n",
598 | " \n",
599 | "```\n",
600 | "Static methods are not widely used in Python. They are common in languages like Java and C++.\n",
601 | "\n",
602 | "Inheritance, class methods and attributes, and static methods are rarely used. But it's good to know them so that you can be able to read the codes of those who still use them. Most frameworks also use them in their codebases, so it's good to know them!"
603 | ]
604 | },
605 | {
606 | "cell_type": "markdown",
607 | "id": "8714dc75",
608 | "metadata": {},
609 | "source": [
610 | "\n",
611 | "\n",
612 | "## 6. Final Notes"
613 | ]
614 | },
615 | {
616 | "cell_type": "markdown",
617 | "id": "f7f9bdde",
618 | "metadata": {},
619 | "source": [
620 | "Inheritance is all about code reuse and OOP is about representing the real world complex systems into software objects. Classes are extremely helpful for organizing large piece of codes but they are probably not the right option when you are building something you just want to use once, normal functions are away better and simpler option.\n",
621 | "\n",
622 | ">Fun fact:\n",
623 | "Python language has 250 classes. That means that most Python programs should use fewer or no class at all. Interesting [watch](https://www.youtube.com/watch?v=o9pEzgHorH0) on why you should not use class!!"
624 | ]
625 | },
626 | {
627 | "cell_type": "markdown",
628 | "id": "c41fd8f3",
629 | "metadata": {},
630 | "source": [
631 | "### [BACK TO TOP](#0)"
632 | ]
633 | },
634 | {
635 | "cell_type": "code",
636 | "execution_count": null,
637 | "id": "4824e81a",
638 | "metadata": {},
639 | "outputs": [],
640 | "source": []
641 | }
642 | ],
643 | "metadata": {
644 | "kernelspec": {
645 | "display_name": "Python 3 (ipykernel)",
646 | "language": "python",
647 | "name": "python3"
648 | },
649 | "language_info": {
650 | "codemirror_mode": {
651 | "name": "ipython",
652 | "version": 3
653 | },
654 | "file_extension": ".py",
655 | "mimetype": "text/x-python",
656 | "name": "python",
657 | "nbconvert_exporter": "python",
658 | "pygments_lexer": "ipython3",
659 | "version": "3.9.7"
660 | }
661 | },
662 | "nbformat": 4,
663 | "nbformat_minor": 5
664 | }
665 |
--------------------------------------------------------------------------------
/notebooks/.ipynb_checkpoints/regex-checkpoint.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "id": "6c5f4c34",
6 | "metadata": {},
7 | "source": [
8 | "# Regular Expressions"
9 | ]
10 | },
11 | {
12 | "cell_type": "markdown",
13 | "id": "d0170bbe",
14 | "metadata": {},
15 | "source": [
16 | "Regular expressions (also called regex, RE, or regex patterns) is a sequence of characters that are used to determine whether the string matches a given pattern. Regex provides advanced string processing functionalities than normal [string methods](https://docs.python.org/2.5/lib/string-methods.html).\n",
17 | "\n",
18 | "Regular expressions are a small programming language implemented in other languages such as Python. Python comes with [`re` module](https://docs.python.org/3/library/re.html) for working with regular expressions. They are used in search engines, word processing applications, text editors, etc..."
19 | ]
20 | },
21 | {
22 | "cell_type": "markdown",
23 | "id": "de11e226",
24 | "metadata": {},
25 | "source": [
26 | "### Regex In Python"
27 | ]
28 | },
29 | {
30 | "cell_type": "markdown",
31 | "id": "da023703",
32 | "metadata": {},
33 | "source": [
34 | "Regular expressions are used for searching or extracting patterns from strings. To motivate them, let's take an example of searching strings."
35 | ]
36 | },
37 | {
38 | "cell_type": "code",
39 | "execution_count": 4,
40 | "id": "c2f61c07",
41 | "metadata": {},
42 | "outputs": [],
43 | "source": [
44 | "import re"
45 | ]
46 | },
47 | {
48 | "cell_type": "code",
49 | "execution_count": 13,
50 | "id": "26bcd4c0",
51 | "metadata": {},
52 | "outputs": [],
53 | "source": [
54 | "# Assume this is a text document. Search all lines that starts with `The`\n",
55 | "\n",
56 | "texts = \"\"\"\n",
57 | "Python is a programming language that emphasize simplicity and readability.\n",
58 | "The Zen of Python contains around 19 rules of Python.\n",
59 | "The first rule says that beautiful is better than ugly.\n",
60 | "The third rule says that simple is better than complex. \n",
61 | "The 17th rule says that if the implementation is hard to explain, it's a bad idea.\n",
62 | "The 18th rule says that if the implementation is easy to explain, it may be a good idea.\n",
63 | "\"\"\""
64 | ]
65 | },
66 | {
67 | "cell_type": "code",
68 | "execution_count": 15,
69 | "id": "99f7a8a9",
70 | "metadata": {},
71 | "outputs": [],
72 | "source": [
73 | "re.search('The:', texts)"
74 | ]
75 | },
76 | {
77 | "cell_type": "code",
78 | "execution_count": null,
79 | "id": "d7866380",
80 | "metadata": {},
81 | "outputs": [],
82 | "source": []
83 | }
84 | ],
85 | "metadata": {
86 | "kernelspec": {
87 | "display_name": "Python 3 (ipykernel)",
88 | "language": "python",
89 | "name": "python3"
90 | },
91 | "language_info": {
92 | "codemirror_mode": {
93 | "name": "ipython",
94 | "version": 3
95 | },
96 | "file_extension": ".py",
97 | "mimetype": "text/x-python",
98 | "name": "python",
99 | "nbconvert_exporter": "python",
100 | "pygments_lexer": "ipython3",
101 | "version": "3.9.7"
102 | }
103 | },
104 | "nbformat": 4,
105 | "nbformat_minor": 5
106 | }
107 |
--------------------------------------------------------------------------------
/notebooks/oop-classes-objects.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "id": "759e8db1",
6 | "metadata": {},
7 | "source": [
8 | "# Object Oriented Programming (OOP) - Classes and Objects"
9 | ]
10 | },
11 | {
12 | "cell_type": "markdown",
13 | "id": "94457c02",
14 | "metadata": {},
15 | "source": [
16 | "Object Oriented Programming(OOP) is a programming paradigm or feature that allows developers to structure programs into classes and objects. OOP gives the developers the ability to design programs around data.\n",
17 | "\n",
18 | "Classes are new data type that you can create yourself. Objects can be created from the Class. A typical class has two components: `fields` (or variables) that store data and `methods`(or functions) that operate on those data. Python [glossary](https://docs.python.org/3/glossary.html) defines class as a template for creating user defined objects and object\n",
19 | "as any data with state(attribute or value) and defined behavior(methods).\n",
20 | "\n",
21 | "With the simplicity of Python and the thousands of features it provides that can be used on the fly, the majority of Python developers doesn't need to use OOP. However, it is important to understand OOP so that you can understand when and when not to use it. OOP provides modularity and reusability. Take an example: Python list is a built-in object. Imagine how many time you(and other millions of coders) use list with different data!! That's extreme reusability!!\n",
22 | "\n",
23 | "When should you use class or function? This is a hard questions, but generally if you found yourself using a number of functions over and over on same data, then you should consider using class for reusability. If on the otherhand you are creating a class with methods that you will use once, it's probably better to use normal functions. Here is a great talk about that: [Stop Using Classes ](https://www.youtube.com/watch?v=o9pEzgHorH0) by Jack Diederich. You can also learn more about the critism of OOP [here](https://en.wikipedia.org/wiki/Object-oriented_programming#Criticism).\n",
24 | "\n",
25 | "Also, if you plan to build an API(Application Protocol Interface) one day, it's very likely that you will need to structure data and methods in a systematic way. So, OOP is an important thing to know. "
26 | ]
27 | },
28 | {
29 | "cell_type": "markdown",
30 | "id": "ade5e5ba",
31 | "metadata": {},
32 | "source": [
33 | "In Python, everything is an `object` and has a `type`. OOP gives us the power to create our custom object of a given data type that can be manipulated. Below is an example of how everything in Python is an object of a certain type."
34 | ]
35 | },
36 | {
37 | "cell_type": "code",
38 | "execution_count": 1,
39 | "id": "a6e3559f",
40 | "metadata": {},
41 | "outputs": [
42 | {
43 | "data": {
44 | "text/plain": [
45 | "list"
46 | ]
47 | },
48 | "execution_count": 1,
49 | "metadata": {},
50 | "output_type": "execute_result"
51 | }
52 | ],
53 | "source": [
54 | "# In Python, everything is an object of a given type\n",
55 | "\n",
56 | "my_list = [1, 2, 3, 4]\n",
57 | "\n",
58 | "type(my_list)"
59 | ]
60 | },
61 | {
62 | "cell_type": "markdown",
63 | "id": "1f464326",
64 | "metadata": {},
65 | "source": [
66 | "The type of Python `list` is a `type` too. This is the same thing for other objects such as `str` and `dict`. The `type` of `type` object is a `type` too :)"
67 | ]
68 | },
69 | {
70 | "cell_type": "code",
71 | "execution_count": 2,
72 | "id": "bcc3e303",
73 | "metadata": {},
74 | "outputs": [
75 | {
76 | "data": {
77 | "text/plain": [
78 | "type"
79 | ]
80 | },
81 | "execution_count": 2,
82 | "metadata": {},
83 | "output_type": "execute_result"
84 | }
85 | ],
86 | "source": [
87 | "type(list)"
88 | ]
89 | },
90 | {
91 | "cell_type": "code",
92 | "execution_count": 3,
93 | "id": "5ed07629",
94 | "metadata": {},
95 | "outputs": [
96 | {
97 | "data": {
98 | "text/plain": [
99 | "type"
100 | ]
101 | },
102 | "execution_count": 3,
103 | "metadata": {},
104 | "output_type": "execute_result"
105 | }
106 | ],
107 | "source": [
108 | "type(str)"
109 | ]
110 | },
111 | {
112 | "cell_type": "code",
113 | "execution_count": 4,
114 | "id": "b0794603",
115 | "metadata": {},
116 | "outputs": [
117 | {
118 | "data": {
119 | "text/plain": [
120 | "type"
121 | ]
122 | },
123 | "execution_count": 4,
124 | "metadata": {},
125 | "output_type": "execute_result"
126 | }
127 | ],
128 | "source": [
129 | "type(type)"
130 | ]
131 | },
132 | {
133 | "cell_type": "markdown",
134 | "id": "9c57f9fc",
135 | "metadata": {},
136 | "source": [
137 | "There is a popular notion that OOP allows developers to represent real world objects into software objects. To some extent, this is true. Real world objects contain state and behavior. Take an example of a car object: The state of a car might be price, model, car maker, speed while the behavior might be changing the gear, applying the brake, etc...). \n",
138 | "\n",
139 | "Software objects provide an abstraction of the real world objects. Software objects store their states in data attributes(or variables), and they expose their behaviors through method attributes(or functions). Methods act on the state of the objects. Java documentation has a great explaination of class and objects in the contexts of the real world objects. For more, check it [here](https://docs.oracle.com/javase/tutorial/java/concepts/object.html).\n",
140 | "\n",
141 | "\n",
142 | "Let's see how to create a class in Python."
143 | ]
144 | },
145 | {
146 | "cell_type": "markdown",
147 | "id": "fcf31cc9",
148 | "metadata": {},
149 | "source": [
150 | "### Creating a Class in Python"
151 | ]
152 | },
153 | {
154 | "cell_type": "markdown",
155 | "id": "fe17e196",
156 | "metadata": {},
157 | "source": [
158 | "Classes are user defined objects. That signal that there are built-in classes that we use day to day contained in some [standard libraries](https://docs.python.org/3/tutorial/stdlib.html). Take an example for a `list`. A list is a class. When we create a list object, we can manipulate it in different ways."
159 | ]
160 | },
161 | {
162 | "cell_type": "code",
163 | "execution_count": 5,
164 | "id": "0049b8ed",
165 | "metadata": {},
166 | "outputs": [
167 | {
168 | "name": "stdout",
169 | "output_type": "stream",
170 | "text": [
171 | "[1, 2, 3, 4]\n"
172 | ]
173 | }
174 | ],
175 | "source": [
176 | "# Creating a list object, \n",
177 | "# The data [1,2,3] is an instance of the list\n",
178 | "\n",
179 | "my_list = [1, 2, 3]\n",
180 | "\n",
181 | "# We can manipulate the list object\n",
182 | "\n",
183 | "my_list.append(4)\n",
184 | "\n",
185 | "print(my_list)"
186 | ]
187 | },
188 | {
189 | "cell_type": "markdown",
190 | "id": "8825e2de",
191 | "metadata": {},
192 | "source": [
193 | "Below is a simple user defined class:\n",
194 | "\n",
195 | "```python\n",
196 | "class MyClass:\n",
197 | " \"\"\"A simple example class\"\"\"\n",
198 | " i = 12345\n",
199 | "\n",
200 | " def f(self):\n",
201 | " return 'hello world'\n",
202 | "```\n",
203 | "\n",
204 | "When creating a new class:\n",
205 | "* We specity its class name. It is common to use Capital letters for defining the class like `ClassName`. This style is called `CamelCase`. \n",
206 | "\n",
207 | "```python\n",
208 | "\n",
209 | "class ClassName:\n",
210 | " ## Class attributes\n",
211 | " \n",
212 | "```\n",
213 | "\n",
214 | "* We define the class attributes(data and methods). Data attributes are data (or variables) that makes up the class and the methods are like functions that operate on data. Methods only work with class. \n",
215 | "\n",
216 | "* We use a special method `__init__` to initialize the data attributes. `__init__` method starts and ends with double underscore(`__`). When passing data to the init method, the first paremeter must be `self`. There are other special methods beyond `__init__`.\n",
217 | "\n",
218 | "```python\n",
219 | "\n",
220 | "class ClassName:\n",
221 | " \n",
222 | " def __init__(self, x, y):\n",
223 | "```\n",
224 | " \n",
225 | "\n",
226 | "* We use `self` as the first parameter for any method that we define in the class. With the exception of `self` parameter, methods are similar to normal functions. `self` parameter allows the method to use the class data attributes.\n",
227 | "\n",
228 | "* To access the data attribute and method of an object, we use `. operator`.\n",
229 | "\n",
230 | "\n",
231 | "```python\n",
232 | "\n",
233 | "class ClassName:\n",
234 | " \n",
235 | " def __init__(self, var1, var2):\n",
236 | " self.var1 = var1\n",
237 | " self.var2 = var2\n",
238 | " \n",
239 | " def method_1(self, var3):\n",
240 | " \"\"\"\n",
241 | " DOCSTRING\n",
242 | " self points to the data attributes, \n",
243 | " function can take new data attribute like var3\n",
244 | " \"\"\"\n",
245 | " \n",
246 | " #statements\n",
247 | " \n",
248 | "```\n",
249 | "Now that we understand what makes a class, let's create a real class."
250 | ]
251 | },
252 | {
253 | "cell_type": "code",
254 | "execution_count": 6,
255 | "id": "c23c5413",
256 | "metadata": {},
257 | "outputs": [],
258 | "source": [
259 | "class Car:\n",
260 | " \n",
261 | " def __init__(self, speed, gear, model, maker):\n",
262 | " \n",
263 | " self.speed = speed\n",
264 | " self.gear = gear\n",
265 | " self.model = model\n",
266 | " self.maker = maker\n",
267 | " \n",
268 | " def speed_up(self, add_speed):\n",
269 | " \n",
270 | " speed = self.speed + add_speed\n",
271 | " \n",
272 | " return speed\n",
273 | " \n",
274 | " def apply_brake(self, decre_speed):\n",
275 | " speed = self.speed - decre_speed\n",
276 | " \n",
277 | " return speed\n",
278 | " \n",
279 | " def change_gear(self, new_gear):\n",
280 | " gear = new_gear\n",
281 | " \n",
282 | " return gear\n",
283 | " \n",
284 | " def car_info(self):\n",
285 | " \n",
286 | " print(f\"Car Info:\\n \\\n",
287 | " Speed: {self.speed}\\n \\\n",
288 | " Model: {self.model}\\n \\\n",
289 | " Maker: {self.maker}\")"
290 | ]
291 | },
292 | {
293 | "cell_type": "markdown",
294 | "id": "3c80c6ad",
295 | "metadata": {},
296 | "source": [
297 | "That's an example of class. In the next section, let's see how to use the class we created."
298 | ]
299 | },
300 | {
301 | "cell_type": "markdown",
302 | "id": "e8b540aa",
303 | "metadata": {},
304 | "source": [
305 | "### Using a Class"
306 | ]
307 | },
308 | {
309 | "cell_type": "markdown",
310 | "id": "fbf6935e",
311 | "metadata": {},
312 | "source": [
313 | "To create the instance of the object, we simply call class we created with the values of the data fields. Note that we don't pass `self` when calling the class."
314 | ]
315 | },
316 | {
317 | "cell_type": "code",
318 | "execution_count": 7,
319 | "id": "dccdac1a",
320 | "metadata": {},
321 | "outputs": [],
322 | "source": [
323 | "my_car = Car(45, 2, 'Model S', 'Tesla')"
324 | ]
325 | },
326 | {
327 | "cell_type": "code",
328 | "execution_count": 8,
329 | "id": "8bd9d4aa",
330 | "metadata": {},
331 | "outputs": [
332 | {
333 | "data": {
334 | "text/plain": [
335 | "2"
336 | ]
337 | },
338 | "execution_count": 8,
339 | "metadata": {},
340 | "output_type": "execute_result"
341 | }
342 | ],
343 | "source": [
344 | "# Accessing the data attributes\n",
345 | "\n",
346 | "my_car.gear"
347 | ]
348 | },
349 | {
350 | "cell_type": "code",
351 | "execution_count": 9,
352 | "id": "4f05c849",
353 | "metadata": {},
354 | "outputs": [
355 | {
356 | "data": {
357 | "text/plain": [
358 | "45"
359 | ]
360 | },
361 | "execution_count": 9,
362 | "metadata": {},
363 | "output_type": "execute_result"
364 | }
365 | ],
366 | "source": [
367 | "my_car.speed"
368 | ]
369 | },
370 | {
371 | "cell_type": "code",
372 | "execution_count": 10,
373 | "id": "cfd4dd64",
374 | "metadata": {},
375 | "outputs": [
376 | {
377 | "data": {
378 | "text/plain": [
379 | "'Tesla'"
380 | ]
381 | },
382 | "execution_count": 10,
383 | "metadata": {},
384 | "output_type": "execute_result"
385 | }
386 | ],
387 | "source": [
388 | "my_car.maker"
389 | ]
390 | },
391 | {
392 | "cell_type": "markdown",
393 | "id": "63a1c4fc",
394 | "metadata": {},
395 | "source": [
396 | "To use the methods we defined in class, we do as we always do with built-in objects (ex: `my_list.append(2)`). We use `. operator` and we pass the variables that make up a particular method. Note that we don't use `self` when calling the methods defined in the class."
397 | ]
398 | },
399 | {
400 | "cell_type": "code",
401 | "execution_count": 11,
402 | "id": "16a13fd0",
403 | "metadata": {},
404 | "outputs": [
405 | {
406 | "data": {
407 | "text/plain": [
408 | "35"
409 | ]
410 | },
411 | "execution_count": 11,
412 | "metadata": {},
413 | "output_type": "execute_result"
414 | }
415 | ],
416 | "source": [
417 | "my_car.apply_brake(10)\n",
418 | "\n",
419 | "# Should return speed - 10 = 45 - 10 = 35"
420 | ]
421 | },
422 | {
423 | "cell_type": "code",
424 | "execution_count": 12,
425 | "id": "e4d8a99e",
426 | "metadata": {},
427 | "outputs": [
428 | {
429 | "data": {
430 | "text/plain": [
431 | "65"
432 | ]
433 | },
434 | "execution_count": 12,
435 | "metadata": {},
436 | "output_type": "execute_result"
437 | }
438 | ],
439 | "source": [
440 | "my_car.speed_up(20)"
441 | ]
442 | },
443 | {
444 | "cell_type": "code",
445 | "execution_count": 13,
446 | "id": "b4aee1e3",
447 | "metadata": {},
448 | "outputs": [
449 | {
450 | "data": {
451 | "text/plain": [
452 | "4"
453 | ]
454 | },
455 | "execution_count": 13,
456 | "metadata": {},
457 | "output_type": "execute_result"
458 | }
459 | ],
460 | "source": [
461 | "my_car.change_gear(4)"
462 | ]
463 | },
464 | {
465 | "cell_type": "code",
466 | "execution_count": 14,
467 | "id": "67536218",
468 | "metadata": {},
469 | "outputs": [
470 | {
471 | "name": "stdout",
472 | "output_type": "stream",
473 | "text": [
474 | "Car Info:\n",
475 | " Speed: 45\n",
476 | " Model: Model S\n",
477 | " Maker: Tesla\n"
478 | ]
479 | }
480 | ],
481 | "source": [
482 | "my_car.car_info()"
483 | ]
484 | },
485 | {
486 | "cell_type": "markdown",
487 | "id": "84ba7cdf",
488 | "metadata": {},
489 | "source": [
490 | "When you print `my_car` object(created from class `Car`), you don't see the information about the class other than that it is an object. "
491 | ]
492 | },
493 | {
494 | "cell_type": "code",
495 | "execution_count": 15,
496 | "id": "c713f643",
497 | "metadata": {},
498 | "outputs": [
499 | {
500 | "name": "stdout",
501 | "output_type": "stream",
502 | "text": [
503 | "<__main__.Car object at 0x10371c2b0>\n"
504 | ]
505 | }
506 | ],
507 | "source": [
508 | "print(my_car)"
509 | ]
510 | },
511 | {
512 | "cell_type": "markdown",
513 | "id": "b05229e0",
514 | "metadata": {},
515 | "source": [
516 | "To choose what will be diplayed when we print the object, we can define a `__str__` method inside the class. The `__str__` method must return a string. Otherwise, you will get an error."
517 | ]
518 | },
519 | {
520 | "cell_type": "code",
521 | "execution_count": 16,
522 | "id": "a0e4c1f4",
523 | "metadata": {},
524 | "outputs": [],
525 | "source": [
526 | "class Car:\n",
527 | " \n",
528 | " def __init__(self, speed, gear, model, maker):\n",
529 | " \n",
530 | " self.speed = speed\n",
531 | " self.gear = gear\n",
532 | " self.model = model\n",
533 | " self.maker = maker\n",
534 | " \n",
535 | " def speed_up(self, add_speed):\n",
536 | " \n",
537 | " speed = self.speed + add_speed\n",
538 | " \n",
539 | " return speed\n",
540 | " \n",
541 | " def apply_brake(self, decre_speed):\n",
542 | " speed = self.speed - decre_speed\n",
543 | " \n",
544 | " return speed\n",
545 | " \n",
546 | " def change_gear(self, new_gear):\n",
547 | " gear = new_gear\n",
548 | " \n",
549 | " return gear\n",
550 | " \n",
551 | " def car_info(self):\n",
552 | " \n",
553 | " print(f\"Car Info:\\n \\\n",
554 | " Speed: {self.speed}\\n \\\n",
555 | " Model: {self.model}\\n \\\n",
556 | " Maker: {self.maker}\")\n",
557 | " def __str__(self):\n",
558 | " \n",
559 | " return f\"This is the object created from class Car. The car model {self.model} was made by {self.maker}\""
560 | ]
561 | },
562 | {
563 | "cell_type": "code",
564 | "execution_count": 17,
565 | "id": "c4f96c8f",
566 | "metadata": {},
567 | "outputs": [
568 | {
569 | "name": "stdout",
570 | "output_type": "stream",
571 | "text": [
572 | "This is the object created from class Car. The car model Model X was made by Tesla\n"
573 | ]
574 | }
575 | ],
576 | "source": [
577 | "my_car = Car(45, 2, 'Model X', 'Tesla')\n",
578 | "print(my_car)"
579 | ]
580 | },
581 | {
582 | "cell_type": "markdown",
583 | "id": "127ad7a9",
584 | "metadata": {},
585 | "source": [
586 | "The methods `__str__` and `__init__` are examples of special or magic methods. Python special methods allows us to overload(or to imitate) built-in operators so that we can use them in our objects just like built-in data types. Magic methods are unique to Python and they are not found in other OOP languages such as Java and C++. We will learn more about magic methods later. \n",
587 | "\n",
588 | "You can find all magics methods on [Python Documentation](https://docs.python.org/3/reference/datamodel.html#basic-customization)."
589 | ]
590 | },
591 | {
592 | "cell_type": "markdown",
593 | "id": "58058236",
594 | "metadata": {},
595 | "source": [
596 | "One last thing: we can use `isinstance()` to check if the object `my_car` is a Car type."
597 | ]
598 | },
599 | {
600 | "cell_type": "code",
601 | "execution_count": 18,
602 | "id": "5665c722",
603 | "metadata": {},
604 | "outputs": [
605 | {
606 | "data": {
607 | "text/plain": [
608 | "True"
609 | ]
610 | },
611 | "execution_count": 18,
612 | "metadata": {},
613 | "output_type": "execute_result"
614 | }
615 | ],
616 | "source": [
617 | "isinstance(my_car, Car)"
618 | ]
619 | },
620 | {
621 | "cell_type": "markdown",
622 | "id": "3f82aa0c",
623 | "metadata": {},
624 | "source": [
625 | "### Final Notes and Further Learning"
626 | ]
627 | },
628 | {
629 | "cell_type": "markdown",
630 | "id": "fb3907f0",
631 | "metadata": {},
632 | "source": [
633 | "The central idea of OOP is creating user-defined objects. With OOP, you can create structured and systematic programs. OOP provides modularity and code reusability which are essential ingredients for building large scale programs.\n",
634 | "\n",
635 | "In the next parts, we will learn about inheritance and special or magic methods.\n",
636 | "\n",
637 | "You can learn more about OOP and classes and Objects at:\n",
638 | "\n",
639 | "* OOP, Lecture 8 of [MIT 6.0001 Introduction to Computer Science and Programming in Python](https://www.youtube.com/watch?v=-DP1i2ZU9gk)\n",
640 | "* [Python Doc](https://docs.python.org/3/tutorial/classes.html)\n",
641 | "* [Wikipedia, OOP](https://en.wikipedia.org/wiki/Object-oriented_programming)\n",
642 | "* [Stop Using Classes](https://www.youtube.com/watch?v=o9pEzgHorH0)"
643 | ]
644 | },
645 | {
646 | "cell_type": "code",
647 | "execution_count": null,
648 | "id": "332793dd",
649 | "metadata": {},
650 | "outputs": [],
651 | "source": []
652 | }
653 | ],
654 | "metadata": {
655 | "kernelspec": {
656 | "display_name": "Python 3 (ipykernel)",
657 | "language": "python",
658 | "name": "python3"
659 | },
660 | "language_info": {
661 | "codemirror_mode": {
662 | "name": "ipython",
663 | "version": 3
664 | },
665 | "file_extension": ".py",
666 | "mimetype": "text/x-python",
667 | "name": "python",
668 | "nbconvert_exporter": "python",
669 | "pygments_lexer": "ipython3",
670 | "version": "3.9.7"
671 | }
672 | },
673 | "nbformat": 4,
674 | "nbformat_minor": 5
675 | }
676 |
--------------------------------------------------------------------------------
/notebooks/oop-inheritance.ipynb:
--------------------------------------------------------------------------------
1 | {
2 | "cells": [
3 | {
4 | "cell_type": "markdown",
5 | "id": "0da0dfc1",
6 | "metadata": {},
7 | "source": [
8 | "\n",
9 | "\n",
10 | "# Object Oriented Programming(OOP) - Inheritance"
11 | ]
12 | },
13 | {
14 | "cell_type": "markdown",
15 | "id": "08b3943e",
16 | "metadata": {},
17 | "source": [
18 | "Contents:\n",
19 | "\n",
20 | "- [How to Create a Child Class from a Parent Class](#1)\n",
21 | "- [Addind New Data and Methods to a Child Class](#2)\n",
22 | "- [Method Overriding](#3)\n",
23 | "- [Checking if Child Class Belongs to Parent Class](#4)\n",
24 | "- [Class Methods, Class Attributes, Static Methods](#5)\n",
25 | "- [Final Notes¶](#6)\n",
26 | " "
27 | ]
28 | },
29 | {
30 | "cell_type": "markdown",
31 | "id": "c8fef721",
32 | "metadata": {},
33 | "source": [
34 | "Real world objects that are in the same class/category share similar behaviors. OOP classes can also have things in common where rather than creating new classes, classes can be inherited from other class.\n",
35 | "\n",
36 | "Also, similar to how we write a normal functions once and reuse them multiple times without having to recreate them again, we can inherit methods and attributes of the parent class using inheritance. \n",
37 | "\n",
38 | "\n",
39 | "Inheritance is an OOP based techniques that refers to `parent-child relationship` where a child class can be inherited from the parent class. The child class will have the access of data and methods of the parent class (but not the other way).\n",
40 | "\n",
41 | "Inheritance is essentially deriving a new class(child class) from an existing class(parent class). A parent class is also called base class or super class, while the child class is called derived class."
42 | ]
43 | },
44 | {
45 | "cell_type": "markdown",
46 | "id": "c3f0cee3",
47 | "metadata": {},
48 | "source": [
49 | "\n",
50 | "\n",
51 | "## 1. How to Create a Child Class from a Parent Class"
52 | ]
53 | },
54 | {
55 | "cell_type": "markdown",
56 | "id": "dfeaeea3",
57 | "metadata": {},
58 | "source": [
59 | "When deriving a child class, you simply define the parent class in the child class. Below is a syntax for inheritance:\n",
60 | "\n",
61 | "\n",
62 | "```python\n",
63 | "class ChildClassName(ParentClassName):\n",
64 | " .\n",
65 | " .\n",
66 | " .\n",
67 | " .\n",
68 | " .\n",
69 | " .\n",
70 | " .\n",
71 | " \n",
72 | " \n",
73 | "```\n",
74 | "\n",
75 | "If you simply want the child class to inherit all data and method attributes without adding or overriding any method of the parent class, simply do the following:\n",
76 | "\n",
77 | "\n",
78 | "```python\n",
79 | "class ChildClassName(ParentClassName):\n",
80 | " pass\n",
81 | "```\n",
82 | "\n",
83 | "Let's take a real example. But we will create a parent class first."
84 | ]
85 | },
86 | {
87 | "cell_type": "code",
88 | "execution_count": 8,
89 | "id": "e8cc33bd",
90 | "metadata": {},
91 | "outputs": [],
92 | "source": [
93 | "class Vehicle:\n",
94 | " \n",
95 | " def __init__ (self, brandmaker, model, speed):\n",
96 | " \n",
97 | " self.brandmaker = brandmaker\n",
98 | " self.model = model\n",
99 | " self.speed = speed\n",
100 | " \n",
101 | " def speed_up(self, add_speed):\n",
102 | " \n",
103 | " new_speed = self.speed + add_speed\n",
104 | " \n",
105 | " return new_speed\n",
106 | " \n",
107 | " def vehicle_info(self):\n",
108 | " \n",
109 | " print(f\"Vehicle Info:\\n \\\n",
110 | " Current Speed: {self.speed}\\n \\\n",
111 | " Vehicle Model: {self.model}\\n \\\n",
112 | " Vehicle Maker: {self.brandmaker}\")"
113 | ]
114 | },
115 | {
116 | "cell_type": "markdown",
117 | "id": "b56f9b99",
118 | "metadata": {},
119 | "source": [
120 | "From the `Vehicle` class, we can derive a new child class `Bicycle`. If we only want to inherit all data and method attributes, all we have to do is to `pass`."
121 | ]
122 | },
123 | {
124 | "cell_type": "code",
125 | "execution_count": 9,
126 | "id": "9f8d2a77",
127 | "metadata": {},
128 | "outputs": [],
129 | "source": [
130 | "class Car(Vehicle):\n",
131 | " pass"
132 | ]
133 | },
134 | {
135 | "cell_type": "code",
136 | "execution_count": 10,
137 | "id": "161bbd65",
138 | "metadata": {},
139 | "outputs": [
140 | {
141 | "name": "stdout",
142 | "output_type": "stream",
143 | "text": [
144 | "Vehicle Info:\n",
145 | " Current Speed: 40\n",
146 | " Vehicle Model: Model S\n",
147 | " Vehicle Maker: Tesla\n"
148 | ]
149 | }
150 | ],
151 | "source": [
152 | "car = Car('Tesla', 'Model S', 40)\n",
153 | "car.vehicle_info()"
154 | ]
155 | },
156 | {
157 | "cell_type": "markdown",
158 | "id": "db89e7dc",
159 | "metadata": {},
160 | "source": [
161 | "A child class inherited from the parent class will have an access to all parent class data and method attributes but if the child class have its own data and methods attributes, they can not be accessed in the parent class. It's only one direction."
162 | ]
163 | },
164 | {
165 | "cell_type": "markdown",
166 | "id": "5b7cbf85",
167 | "metadata": {},
168 | "source": [
169 | "\n",
170 | "\n",
171 | "## 2. Adding New Data and Methods to a Child Class"
172 | ]
173 | },
174 | {
175 | "cell_type": "markdown",
176 | "id": "b95ce625",
177 | "metadata": {},
178 | "source": [
179 | "In addition to the inherited data and method attributes, it's possible to add new data and methods to the class.\n",
180 | "\n",
181 | "To automatically get the access to the data and methods of the parent class, we can use the `super()` function. Below we use `super` for accessing the initialized parent class data through `__init__` method, but it can be used for almost any method."
182 | ]
183 | },
184 | {
185 | "cell_type": "code",
186 | "execution_count": 11,
187 | "id": "6bbe80a7",
188 | "metadata": {},
189 | "outputs": [],
190 | "source": [
191 | "class Car(Vehicle):\n",
192 | " \n",
193 | " def __init__(self, brandmaker, model, speed): #__init__ method is overriden: more on method overriding later\n",
194 | " super().__init__(brandmaker, model, speed) #use super() to automatically get the data and methods of parent class\n",
195 | " \n",
196 | " def speed_down(self, down_speed): #speed_down is a new method, down_speed is a new data/variable\n",
197 | " \n",
198 | " speed = self.speed + down_speed\n",
199 | " \n",
200 | " return speed"
201 | ]
202 | },
203 | {
204 | "cell_type": "code",
205 | "execution_count": 12,
206 | "id": "ed0fd05c",
207 | "metadata": {},
208 | "outputs": [
209 | {
210 | "data": {
211 | "text/plain": [
212 | "60"
213 | ]
214 | },
215 | "execution_count": 12,
216 | "metadata": {},
217 | "output_type": "execute_result"
218 | }
219 | ],
220 | "source": [
221 | "car = Car('Tesla', 'Model S', 40)\n",
222 | "car.speed_down(20)"
223 | ]
224 | },
225 | {
226 | "cell_type": "markdown",
227 | "id": "127fc01f",
228 | "metadata": {},
229 | "source": [
230 | "\n",
231 | "\n",
232 | "## Method Overriding"
233 | ]
234 | },
235 | {
236 | "cell_type": "markdown",
237 | "id": "634b1f92",
238 | "metadata": {},
239 | "source": [
240 | "A derived or child class inherits all methods of the parent class or base class. But there are times you may want to override the methods of the parent class in child class. All you have to do is to define the methods that have the same name as the parent class. Such method will be overriden!\n",
241 | "\n",
242 | "We saw that for __init__ method but let's demonstrate it for normal method."
243 | ]
244 | },
245 | {
246 | "cell_type": "code",
247 | "execution_count": 13,
248 | "id": "65465bfd",
249 | "metadata": {},
250 | "outputs": [],
251 | "source": [
252 | "class Bicycle(Vehicle):\n",
253 | " \n",
254 | " def vehicle_info(self):\n",
255 | " \n",
256 | " print(f\"Bicycle Info:\\n \\\n",
257 | " Current Speed: {self.speed}\\n \\\n",
258 | " Bicycle Model: {self.model}\\n \\\n",
259 | " Bicycle Maker: {self.brandmaker}\")"
260 | ]
261 | },
262 | {
263 | "cell_type": "code",
264 | "execution_count": 14,
265 | "id": "c5b1c851",
266 | "metadata": {},
267 | "outputs": [
268 | {
269 | "name": "stdout",
270 | "output_type": "stream",
271 | "text": [
272 | "Bicycle Info:\n",
273 | " Current Speed: 20\n",
274 | " Bicycle Model: Model XXXX\n",
275 | " Bicycle Maker: Bike Manufacturer\n"
276 | ]
277 | }
278 | ],
279 | "source": [
280 | "bicycle = Bicycle('Bike Manufacturer', 'Model XXXX', 20)\n",
281 | "bicycle.vehicle_info()"
282 | ]
283 | },
284 | {
285 | "cell_type": "markdown",
286 | "id": "5e613123",
287 | "metadata": {},
288 | "source": [
289 | "You can see that the method `vehicle_info()` was overriden. A one caveat to make here is that the things we are using are only just examples that demonstrate the concepts. In the real world, inheritance is more about code reuse than following hierachies. "
290 | ]
291 | },
292 | {
293 | "cell_type": "markdown",
294 | "id": "257370b0",
295 | "metadata": {},
296 | "source": [
297 | "\n",
298 | "\n",
299 | "## 3. Checking if Child Class Belongs to Parent Class"
300 | ]
301 | },
302 | {
303 | "cell_type": "markdown",
304 | "id": "0d40fb6e",
305 | "metadata": {},
306 | "source": [
307 | "We can check if an object is a type of a given class with `isinstance()` built-in function. It will return `True` if the object belong to the class, and otherwise `False`."
308 | ]
309 | },
310 | {
311 | "cell_type": "code",
312 | "execution_count": 23,
313 | "id": "f8ad7f88",
314 | "metadata": {},
315 | "outputs": [
316 | {
317 | "data": {
318 | "text/plain": [
319 | "True"
320 | ]
321 | },
322 | "execution_count": 23,
323 | "metadata": {},
324 | "output_type": "execute_result"
325 | }
326 | ],
327 | "source": [
328 | "isinstance(car, Vehicle)"
329 | ]
330 | },
331 | {
332 | "cell_type": "code",
333 | "execution_count": 24,
334 | "id": "e932f3b5",
335 | "metadata": {},
336 | "outputs": [
337 | {
338 | "data": {
339 | "text/plain": [
340 | "True"
341 | ]
342 | },
343 | "execution_count": 24,
344 | "metadata": {},
345 | "output_type": "execute_result"
346 | }
347 | ],
348 | "source": [
349 | "isinstance(bicycle, Vehicle)"
350 | ]
351 | },
352 | {
353 | "cell_type": "code",
354 | "execution_count": 25,
355 | "id": "75b531ad",
356 | "metadata": {},
357 | "outputs": [
358 | {
359 | "name": "stdout",
360 | "output_type": "stream",
361 | "text": [
362 | "Hello world\n"
363 | ]
364 | }
365 | ],
366 | "source": [
367 | "class TempClass:\n",
368 | " print('Hello world')\n",
369 | " \n",
370 | "class TempChild(TempClass):\n",
371 | " pass"
372 | ]
373 | },
374 | {
375 | "cell_type": "code",
376 | "execution_count": 26,
377 | "id": "f31b8a92",
378 | "metadata": {},
379 | "outputs": [
380 | {
381 | "data": {
382 | "text/plain": [
383 | "True"
384 | ]
385 | },
386 | "execution_count": 26,
387 | "metadata": {},
388 | "output_type": "execute_result"
389 | }
390 | ],
391 | "source": [
392 | "temp_obj = TempChild()\n",
393 | "isinstance(temp_obj, TempClass)"
394 | ]
395 | },
396 | {
397 | "cell_type": "code",
398 | "execution_count": 27,
399 | "id": "a101d6ab",
400 | "metadata": {},
401 | "outputs": [
402 | {
403 | "data": {
404 | "text/plain": [
405 | "False"
406 | ]
407 | },
408 | "execution_count": 27,
409 | "metadata": {},
410 | "output_type": "execute_result"
411 | }
412 | ],
413 | "source": [
414 | "isinstance(temp_obj, Vehicle)"
415 | ]
416 | },
417 | {
418 | "cell_type": "markdown",
419 | "id": "f8232f18",
420 | "metadata": {},
421 | "source": [
422 | "There is another built-in function `issubclass()` that can be used to check if the a particular child class is a subclass of a given parent class. The first argument in `issubclass()` must be a subclass and the later argument must be the parent class."
423 | ]
424 | },
425 | {
426 | "cell_type": "code",
427 | "execution_count": 21,
428 | "id": "236c1406",
429 | "metadata": {},
430 | "outputs": [
431 | {
432 | "data": {
433 | "text/plain": [
434 | "True"
435 | ]
436 | },
437 | "execution_count": 21,
438 | "metadata": {},
439 | "output_type": "execute_result"
440 | }
441 | ],
442 | "source": [
443 | "issubclass(Car, Vehicle)"
444 | ]
445 | },
446 | {
447 | "cell_type": "code",
448 | "execution_count": 28,
449 | "id": "851669d7",
450 | "metadata": {},
451 | "outputs": [
452 | {
453 | "data": {
454 | "text/plain": [
455 | "True"
456 | ]
457 | },
458 | "execution_count": 28,
459 | "metadata": {},
460 | "output_type": "execute_result"
461 | }
462 | ],
463 | "source": [
464 | "issubclass(Bicycle, Vehicle)"
465 | ]
466 | },
467 | {
468 | "cell_type": "code",
469 | "execution_count": 29,
470 | "id": "2f69b268",
471 | "metadata": {},
472 | "outputs": [
473 | {
474 | "data": {
475 | "text/plain": [
476 | "False"
477 | ]
478 | },
479 | "execution_count": 29,
480 | "metadata": {},
481 | "output_type": "execute_result"
482 | }
483 | ],
484 | "source": [
485 | "issubclass(TempChild, Vehicle)"
486 | ]
487 | },
488 | {
489 | "cell_type": "markdown",
490 | "id": "ad03d910",
491 | "metadata": {},
492 | "source": [
493 | "\n",
494 | "\n",
495 | "## 4. Multiple Inheritance"
496 | ]
497 | },
498 | {
499 | "cell_type": "markdown",
500 | "id": "e7a28b0b",
501 | "metadata": {},
502 | "source": [
503 | "Python being a very unique programming language supports multiple inheritance where a new child class can be inherited from multiple parent classes.\n",
504 | "\n",
505 | "\n",
506 | "Below is the template for inheriting from several number of parent classes:\n",
507 | "\n",
508 | "```python\n",
509 | "\n",
510 | "class ChildClass(ParentClass1, ParentClass2, ParentClass3, ParentClass4):\n",
511 | " \n",
512 | " #Statements\n",
513 | " \n",
514 | "```"
515 | ]
516 | },
517 | {
518 | "cell_type": "markdown",
519 | "id": "601787ae",
520 | "metadata": {},
521 | "source": [
522 | "It's rare that you will need to inherit from multiple classes, but if you happen to do, check the Python [doc](https://docs.python.org/3/tutorial/classes.html#multiple-inheritance) for more. "
523 | ]
524 | },
525 | {
526 | "cell_type": "markdown",
527 | "id": "a8c07f38",
528 | "metadata": {},
529 | "source": [
530 | "\n",
531 | "\n",
532 | "## 5. Class Methods, Class Attributes, Static Methods"
533 | ]
534 | },
535 | {
536 | "cell_type": "markdown",
537 | "id": "42046234",
538 | "metadata": {},
539 | "source": [
540 | "Class methods are type of methods that are very specific to the class instead of the objects of the class. Class methods are represented by `@classmethod` decorator above `def` definition. The first parameter of methods is `cls` that stands for `class`.\n",
541 | "\n",
542 | "Class methods can not access regular class methods or attributes.\n",
543 | "\n",
544 | "Here is a template of class method:\n",
545 | "\n",
546 | "```python\n",
547 | "\n",
548 | "class ClassName:\n",
549 | " \n",
550 | " def regular_method(self):\n",
551 | " #statements\n",
552 | " \n",
553 | " @classmethod\n",
554 | " def class_method(cls):\n",
555 | " #statements\n",
556 | " \n",
557 | "```\n",
558 | "\n",
559 | "Class methods can be called without creating the object of the class.\n",
560 | "\n",
561 | "```python\n",
562 | "ClassName.class_method()\n",
563 | "```"
564 | ]
565 | },
566 | {
567 | "cell_type": "markdown",
568 | "id": "dac88457",
569 | "metadata": {},
570 | "source": [
571 | "Just like class methods, class attributes are variables that are specific to the class not the objects of the class. Class attributes are like global variables, they are defined above all methods and they are not initialized in `__init_() method`.\n",
572 | "\n",
573 | "Here is a template for class attributes:\n",
574 | "\n",
575 | "```python\n",
576 | "class ClassName:\n",
577 | " num = 0\n",
578 | " \n",
579 | " # statements\n",
580 | " ```"
581 | ]
582 | },
583 | {
584 | "cell_type": "markdown",
585 | "id": "7293a8ea",
586 | "metadata": {},
587 | "source": [
588 | "Static methods are type of methods that don't have `self` or `cls` parameter and have `@staticmethod` decorator above `def` definition. Just like class methods, static methods can not access the class methods and attributes(variables).\n",
589 | "\n",
590 | "Below is a template for static methods:\n",
591 | "\n",
592 | "```python\n",
593 | "class ClassName:\n",
594 | " \n",
595 | " @staticmethod\n",
596 | " def static_method_name():\n",
597 | " #statements\n",
598 | " \n",
599 | "```\n",
600 | "Static methods are not widely used in Python. They are common in languages like Java and C++.\n",
601 | "\n",
602 | "Inheritance, class methods and attributes, and static methods are rarely used. But it's good to know them so that you can be able to read the codes of those who still use them. Most frameworks also use them in their codebases, so it's good to know them!"
603 | ]
604 | },
605 | {
606 | "cell_type": "markdown",
607 | "id": "8714dc75",
608 | "metadata": {},
609 | "source": [
610 | "\n",
611 | "\n",
612 | "## 6. Final Notes"
613 | ]
614 | },
615 | {
616 | "cell_type": "markdown",
617 | "id": "f7f9bdde",
618 | "metadata": {},
619 | "source": [
620 | "Inheritance is all about code reuse and OOP is about representing the real world complex systems into software objects. Classes are extremely helpful for organizing large piece of codes but they are probably not the right option when you are building something you just want to use once, normal functions are away better and simpler option.\n",
621 | "\n",
622 | ">Fun fact:\n",
623 | "Python language has 250 classes. That means that most Python programs should use fewer or no class at all. Interesting [watch](https://www.youtube.com/watch?v=o9pEzgHorH0) on why you should not use class!!"
624 | ]
625 | },
626 | {
627 | "cell_type": "markdown",
628 | "id": "c41fd8f3",
629 | "metadata": {},
630 | "source": [
631 | "### [BACK TO TOP](#0)"
632 | ]
633 | },
634 | {
635 | "cell_type": "code",
636 | "execution_count": null,
637 | "id": "4824e81a",
638 | "metadata": {},
639 | "outputs": [],
640 | "source": []
641 | }
642 | ],
643 | "metadata": {
644 | "kernelspec": {
645 | "display_name": "Python 3 (ipykernel)",
646 | "language": "python",
647 | "name": "python3"
648 | },
649 | "language_info": {
650 | "codemirror_mode": {
651 | "name": "ipython",
652 | "version": 3
653 | },
654 | "file_extension": ".py",
655 | "mimetype": "text/x-python",
656 | "name": "python",
657 | "nbconvert_exporter": "python",
658 | "pygments_lexer": "ipython3",
659 | "version": "3.9.7"
660 | }
661 | },
662 | "nbformat": 4,
663 | "nbformat_minor": 5
664 | }
665 |
--------------------------------------------------------------------------------