# Notes on Python ## Mutable and Immutable Objects One important distinction to remember when learning Python is that between mutable and immutable objects. In short, this can be interpreted as editable and uneditable objects. ### Strings An example of an immutable object is a string. Once a string object is created, its value can never be changed. This is a confusing concept to many people when learning Python as their L1, as they tend to confuse the value of the object with the value of the variable to which it is assigned. I can imagine this is already getting confusing so I'll lift your spirits by giving an example: `string = 'string'` The value of the variable `string` is now `'string'`. Let's assume we want to change that value to `'strings'`. The first thing that comes to mind is: ```python=3.6 string + 's' print(string) >> 'string' ``` What happened there? Even though we added 's' by writing `+ 's'` the value is unchanged? Well, that's a good example of what it means to be an immutable object! "But wait," you say. "I've done something similar before and that _did_ work!" Good point. You're probably thinking of: ```python string += 's' print(string) >> 'strings' ``` "Yes, exactly! So we _can_ change the value!" Well, actually no... While `+=` is a fantastic short-hand, it makes learning harder as it obscures what is going on behind the scenes. If we were to write it out, this is what `+=` would look like: ```python=3.6 string = string + 's' ``` See the difference? We __reassign__ the value of `string` by adding together its old value and 's'. > ### A short note on variable "inception": > In the course of your python adventures, you'll surely come across a few examples of the above, and eventually write them yourself; cases in which you assign a variable while referencing the variable itself. While it may seem confusing, it is an absolutely legitimate way to write code. To help overcome this confusion, we can break down what happens into three steps: > 1. The data is interpreted > `string + 's'` >> `'string' + 's'` > 2. The computations are run > `'string'+ 's'` >> `'strings'` > 3. The data is returned and assigned to the variable > `string = 'strings'` > > It can either help or hurt at this point to write it out as a function, so look at the example below and decide for yourself whether it clarifies or not: > ```python > def add_strings(first_string, second_string): > new_string = first_string + second_string > > return new_string > > string = add_strings(string, 's') > ``` > The above should provide you with some intuition on what constitutes an immutable object and the implications thereof. To further clarify the distinction, let's now have a look at a _mutable_ object! ### Lists Lists are a good example of mutable objects. They are a type of object that come with a large set of in-built functions that you can call on the object itself to change its value. Let's first create a list that we can use in our examples below! ```python=3.6 our_list = ['my', 'favourite', 'list'] ``` #### Appending If we feel that the items in our list are getting a bit lonely, we can simply add, or _append_ items to it! ```python=3.6 our_list.append('!') print(our_list) >> ['my', 'favourite', 'list', '!'] ``` Wasn't that simple? Important here is to note the difference between how we added something to our list and how we previously "added" to the value of our string. The word "added" is wrapped in the most sarcastic of quotes as of course we didn't _actually_ add anything to the value of our string: we created a new string, and assigned its value to our previous variable. So the question is: did we do the same to our list? The answer: no! As you can see above we simply called the `.append()` method on our list without ever reassigning the value of the variable. > ### A short analogy > If this is still confusing, here's an analogy that might help: Try to think of immutable objects, such as strings, as files, and mutable objects as folders. Let's assume we have a folder named `cat_pictures` which contains a picture of a cat, called `cat.jpg`. We're happy at this point, but everything is not perfect quite yet, because we would like to edit the picture such that it looks like the cat is shooting laser beams out of its eyes. We open up Photoshop, and, after far more time than we can justify spending on this, our new picture is done! We hit save, and try to save it as `cat_pictures/cat.jpg`, but Photoshop tells us the file already exists. We hit overwrite, because who cares about boring pictures of cats without laser beams? Now `cat.jpg` still refers to a picture of a cat, however, it is not the same picture as before. We _cannot_ change a picture and still have it be the same picture, however we _can_ store it using the same name by overwriting the old picture. As for our folder, when we started out it was called `cat_pictures`. After editing the contents of our folder, it still has the same name, and it still refers to the same folder, despite the change of contents. #### (Various ways of) Removing We've seen how to _add_ data to our list, but what about removing data? As the header indicates, the options are plentiful here! To understand our options better, let's first talk about one important characteristic of lists: __List Indexes and Values__ An important feature of a list is that each value has an index and vice versa. When we want to access the values of our list, we have to use the index: ```python=3.6 print(our_list[0]) >> 'my' ``` The same goes for changing data in our list: ```python=3.6 our_list[0] = 'your' print(our_list) >> ['your', 'favourite', 'list', '!'] ``` However, for removing we can choose between using the index or the value. __Removing by Index__ The options are once again plentiful. If we want to remove an item from a list using the index, we can choose to do so using either `del` or `.pop()`. _note:_ For the examples below, please assume that after each removal we restore the list to its old state. _del_ Del is the simpler of the two. Simply write: `del our_list[3]` and the resulting list will be: `['your', 'favourite', 'list']` _pop_ This function is well-named as it literally _pops_ items from the list. If you should choose to, you won't even notice this behaviour: ```python=3.6 our_list.pop() print(our_list) >> ['your', 'favourite', 'list'] ``` Notice that, if no index is provided, `.pop()` by default removes the last object from the list. In the example above it looks like pop does exactly the same as del, however, there's one important difference that makes it _pop_: namely, it returns the value of the item that is removed from the list. Let's see how that works: ```python=3.6 while len(our_list): print(our_list.pop()) >> '!' >> 'list' >> 'favourite' >> 'your' ``` So as the value is removed from the list, it is also returned, allowing you to use it, reassign it, or whatever your heart desires. The usefulness might not be immediately apparent, but if you're a bit more perceptive you will have noticed at least _one_ simple, useful thing it do: _Note:_ Please read the info box below if you're confused by the line `while len(our_list)` > #### Conditionals Behind the Screens > When reading other people's code you may occasionally come across expressions like these: > ```python > while len(some_list): > > if some_string: > > if some_integer: > ``` > It might confuse you at first why these statements even work. To figure out _why_ the work, it's important to look at _how_ they work, behind the screens. First of all, you could say (don't quote me on this) that the only datatypes that can be evaluated in conditionals are booleans (i.e. `True` or `False`). So how does this work behind the screens? A simple way to understand this is to first understand that any datatype in a conditional needs to first be converted into a boolean. > This of course begs the question: what becomes `True` and what becomes `False`. Simply put, any datatype with a value becomes `True`, any empty datatype becomes `False`. It's a bit of an oversimplification but we'll review all the possible options below: > ```python > # FALSE > # empty string > bool('') > >> False > # an empty list > bool([]) > >> False > # an empty dictionary > bool({}) > >> False > # an integer with value 0 > bool(0) > >> False > # an empty set > bool(set()) > >> False > # None > bool(None) > >> False > > # TRUE > # a non-empty string > bool('this is a string') > >> True > # an non-zero integer > bool(1) > >> True > # a non-empty list > bool([1]) > >> True > # a non-empty dictionary > bool({'key': 'value'}) > >> True > # a non-empty set > bool({'value'}) > >> True > # not None > bool(not None) > >> True > > # Some examples that might be surprising > # a negative integer > bool(-1) > >> True > # a string containing 0 > bool('0') > >> True > # or False > bool('False') > >> True > # or None > bool('None') > >> True > # a list containing an empty string > bool(['']) > >> True > # a list containing False > bool([False]) > >> True > # I'll suffice that to show that any non-empty datatype (except of course False and None) evaluate to True > ``` > This should give you some intuition on how different datatypes are determined to be `True` or `False`. > As for the conditional part of it, they can simply be read as: > `if bool(condition) == True` > and > `while bool(condition) == True` > Knowing that, you should see how the below is simply terrible code: > ```python > # always evaluates as True > if True: > # always evaluates as False > if False: > # Infinite loop: > while True: > # loop that never starts: > while False: > ``` To reiterate, below is an example of the usefulness of `.pop()`: ```python forwards = [1,2,3,4] reverse = [] while len(forwards): reverse.append(forwards.pop()) print(reverse) >> [4, 3, 2, 1] ``` This is an easy way to create a list with the reverse order of another list. Note however that the original list, `forwards`, is now empty. _pssst... hey... you could also just call `forwards.reverse()`_ This should cover how to remove items from a list using the index (and a few other topics...). Let's move on to see how we can do... __Removing by Value__ To remove an item from a list using it's value, the `.remove()` method is at the ready. Simply pass in the value you want to remove as the argument of the function, and it will disappear from the list! ```python our_list.remove('!') print(our_list) >> ['your', 'favourite', 'list'] ``` It should be noted, however, that `.remove()` only removes the first item it can find that matches the value: ```python some_list = ['a', 'b', 'c', 'a'] some_list.remove('a') print(some_list) >> ['b', 'c', 'a'] ``` If this is not what you want to achieve, there's plenty of ways to work around it: ```python some_list = ['a', 'b', 'c', 'a'] while 'a' in some_list: some_list.remove('a') print(some_list) >> ['b', 'c'] ``` __Filtering Lists__