Python Basics
Collections - Lists
Lists are mutable sequences, typically used to store collections of homogeneous items (where the precise degree of similarity will vary by application).
They are one of the most commonly used collection in python. They are extremely flexible, intuitive and easy to use.
So much so that other more specialized collections tend to be overlooked and lists drawbacks too easly forgotten.
Tristan
contact-datartichaut@pm.me
Python projects list
Libraries.
import copy
import itertools
from collections import deque
Beginner
What are lists and why use them.
In more technical termes, lists are defined as :
Lists are a good "go-to" collection, it works in a lot of situation, and can be used if you are not sure what type of collection to use in a part of your code yet.
As a general rule, lists are good when no other collection fit the use case.
Lists are extremly flexible, but in return it is not the most effecient collection.
ToC
l1 = list()
l1 = []
They can be initialized directly with values.
l1 = [1, 2, 3]
Note the list
method needs an iterable as its only argument.
Writing l1 = list([1, 2, 3])
would be redundant because we would be creating a list from
another list. This method is usefull when handling an iterable that we need to use as a list.
Lists can contain almost any type of object and several different types at the same time.
l1 = [1, 2, 3]
l2 = [1, "Hello", 3.4]
l3 = ["mouse", [8, 4, 6], ['a']]
Lists can also contain other lists or other iterables.
Every one of these list are of type list but its elements are of different types.
for i, list_i in enumerate([l1, l2, l3]):
print(f"\nType of l{i+1} : {type(list_i)}")
for j, elt in enumerate(list_i):
print(f"element n°{j+1} : {elt} / type : {type(elt)}")
Beginner
Accessing values in a list.
There are several ways to access elements from a list. The simplest way is to access an element by index.
To do that, we use the []
notation.
ToC
l1 = [1, 2, 3, 4, 5]
print(f"3rd element of the list : {l1[2]}.")
Same for nested lists.
# Nested List
l1 = [[2, 0, 1, 5], "Word."]
elt = l1[0][1]
print(f"The first element of l1 is a list, its second element is : {elt}")
elt = l1[1][3]
print(f"\nThe second element of l1 is a string, its 4th element is : {elt}")
Strings can be considered lists of caracters to some extent.
Negative indices can be used to access elements from the end of a list.
# Indexes negatifs (partant de la fin)
l1 = ['a', 'b', 'c', 'd', 'e']
print(f"The last element of the list is : {l1[-1]}")
It is possible to get a subset of a list with slices.
A slice is declared as follows :
Slices work with negative indices.
Here are some more examples of slices.
l1 = ['a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i']
print("Original list :")
print(l1)
print("3rd to 5th elements :")
print(l1[2:5])
print("1st to 6th from the end :")
print(l1[:-5])
print("6th to last :")
print(l1[5:])
print("first to last :")
print(l1[:])
print("2nd to 2nd to last :")
print(l1[1:-2])
With slices, the second index is always excluded.
print("Last element :")
print(l1[-1])
print("Everything except the last :")
print(l1[:-1])
A step can be added to a slice.
l1 = [1, 2, 3, 4, 5, 6, 7]
print("Element from index 0 to 6 with a step of 2 :")
print(l1[0:7:2])
print("Elements from index 6 to 0 with a step of -3 :")
print(l1[7::-3])
Beginner
Adding items to a list.
There are several method to add items to a list, some simpler, some with more handy features.
ToC
l1 = [1, 3, 5]
The list.append
method adds an item to the end of a list.
l1.append(7)
print(l1)
The list.extend
method adds multiples items from another collection.
l1.extend([9, 11, 13])
print(l1)
Beware of unordered collections, such as sets, results can be unexpected.
l1.extend({15, 17})
print(l1)
The list.insert
method adds an item at the specified index.
l1.insert(1, 2)
print(l1)
Not that any type of object can be inserted.
When inserting a list, it is inserted as a whole and not only its content.
Same goes for append.
l1.insert(2, [19, 21])
print(l1)
Finaly, items can also be added by concatenation.
l2 = [19, 21]
print(l1 + l2)
The *
can also be used to duplicate a list.
print([1, 2] * 3)
Intermediate
The list.append
, list.extend
and list.insert
methods allows to
keep the same
object whereas the +
operator creates a third object.
Advanced
Demonstration.
First with the three specific methods.
l1 = [1, 2, 3]
print(f"Address of l1 : {hex(id(l1))}")
l1.append(4)
print(f"\nAddress of l1 after append : {hex(id(l1))}")
l1.extend([5, 6])
print(f"\nAddress of l1 after extend : {hex(id(l1))}")
l1.insert(0, 0)
print(f"\nAddress of l1 after insert : {hex(id(l1))}")
Now with a concatenation.
l1 = [1, 2, 3]
l2 = [4, 5]
print(f"Address of l1 : {hex(id(l1))}")
l1 = l1 + l2
print(f"\nAddress of l1 after concatenation : {hex(id(l1))}")
There, l1 + l2
is run and the result is being stored in a third variable, this result
is then stored in l1.
Note this does not happen with the +=
operator since it implicitely calls the
list.extend
method.
Beginner
Modifying items in a list.
Just like other mutable sequence types it is possible to access items from a list by index or with a slice and change its value.
ToC
l1 = [1, 3, 5, 7, 9]
l1[1] = 2
print(l1)
Slices can be used to insert an item in place of a part of a list.
Their size do not have to match.
l1[0:4] = [1, 3]
print(l1)
Beginner
Deleting items from a list.
Just like adding, there are several ways to delete items from a list, with slightly different behaviour.
ToC
l1 = ['a', 'b', 'c', 'd', 'e', 'f', 'g']
The list.pop(index)
method removes the element at the given index.
The value is returned.
print('Item return by pop :')
print(l1.pop(2))
print('\nState of the list :')
print(l1)
The list.remove(elem)
method removes the element given as a parameter.
Nothing is returned.
print('Item return by remove :')
print(l1.remove('a'))
print('\nState of the list :')
print(l1)
The del
statement removes the element at the given index without returning it.
print("Deleting the third element.")
del l1[2]
print('\nState of the list :')
print(l1)
the list.clear
method removes every items from a list.
print("Clearing the list.")
l1.clear()
print('\nState of the list :')
print(l1)
Beginner
Copying a list.
Copying a list is not as simple as one might think and can lead to unexpected results.
ToC
Creating a copy that works in every situation without thinking about it.
# A basic list ...
l1 = [1, 2, 3]
# ... copied to another variable.
l2 = copy.deepcopy(l1)
Now let's look at aa basic example.
First we create a list l1
.
l1 = [1, 2, 3]
Then we create a list l2
and assign it l1
directly.
l2 = l1
Now let's change the first element of l1
and observe the effect on l2
.
l1[0] = 4
print(l2)
The change affected l2
.
This is because l2 = l1
affect to l2
the address of the list referenced by
l1
and not the list itself. So if change are made to the list, both variables reference the same
object and therefor, both variables are "changed".
To create an actual copy of a list both list.copy()
and copy.copy(list)
can be used.
l1 = [1, 2, 3]
l2 = l1.copy()
l1[0] = 4
print(l2)
Here change to the first item of l1
does not affect l1
, they are distinct objects.
Intermediate
Note the problem reappears with nested lists. The copy
method makes a
copy of every items in the list.
If an item in the list is a list itself (or another collection) the reference to this object is copyied
and not the object itself.
Let's see an example.
l1 = [1, 2, [3, 4]]
This time we use the list.copy
method.
l2 = l1.copy()
We can then change values in l2
and observe the changes.
l2[0] = 0
l2[2][0] = 5
print(l2)
Finally let's see how it impacted l1.
print(l1)
The first element is not impacted, as expected because l2
is a copy.
But the nested list is changed.
To avoid that, we can use the copy.deepcopy
function that recursively copies every object
in a collection.
Advanced
We can illustrate that by accessing the memories addresses of these objects.
l1 = [1, 2, [3, 4]]
l2 = l1
l3 = copy.copy(l1)
l4 = copy.deepcopy(l1)
print(f"l1 : {hex(id(l1))}")
print(f"l2 : {hex(id(l2))}")
print(f"l3 : {hex(id(l3))}")
print(f"l4 : {hex(id(l4))}")
As expected, l1
and l2
are the same object, but
l3
and l4
are distinct from l1
.
Now, how about the second list, nested in l1
.
print(f"Nested list accessed from l1 : {hex(id(l1[2]))}")
print(f"Nested list accessed from l2 : {hex(id(l2[2]))}")
print(f"Nested list accessed from l3 : {hex(id(l3[2]))}")
print(f"Nested list accessed from l4 : {hex(id(l4[2]))}")
Again, as expected, only the secondary list from l4
is really a copy.
Beginner
Iterating through a list.
for
loops are built in python to iterate over every item in a collection.
ToC
l1 = ['word', 'apple', 'red', 'bicyle']
The for ELEM in LIST:
syntax is the most common.
for str_word in l1:
print(str_word)
The enumerate
built-in function can be use to get an iterator over a collection.
It provides every item in order along with its index.
The start
argument allow to offset the returned index.
for i, elem in enumerate(l1, start=1):
print(f'{elem:6} is the element n°{i} of the list.')
Beginner
Logic tests with lists.
In Python, every object can be tested for truth value.
Testing a list for its trush value is simple.
ToC
Examples of Falsy and Truthy lists.
l1 = []
print(bool(l1))
l1 = [0]
print(bool(l1))
Use-case : a list can be used as a condition for a while
statement. It is
truthy until it becomes empty.
l1 = [1, 2, 3, 4, 5]
res = 0
while l1:
res += l1.pop()
print(res)
Here we cummulatively add elements of the list by removing the first element until it is empty.
Beginner
Unpacking a list.
With pattern matching, python is able map multiple variables on the left of an equal sign and assign the right value from an iterable on the right side.
ToC
A simple unpacking.
a, b, c = [1, 2, 3]
print(f"a : {a}")
print(f"b : {b}")
print(f"c : {c}")
Intermediate
Upacking can be a bit more complex.
The *
notation indicate that this variable should receive multiple values (as a list)
if possible.
a, *b, c = [1, 2, 3, 4, 5]
print(f"a : {a}")
print(f"b : {b}")
print(f"c : {c}")
Here, b is in second place. the *
indicate that b should receive every starting from the
second one.
The c tells us that values should stop being given to b (still as a list) so c can take the last value.
Unpacking can only have one starred expression.
Intermediate
Concrete example.
def test(string_to_split):
"""Test function."""
return string_to_split.split('-', maxsplit=3)
val1, val2, val3 = test('one-two-three')
print(val1)
print(val2)
print(val3)
Intermediate
List comprehension.
List comprehensions provide a concise way to create lists. Common applications are to make new lists where each element is the result of some operations applied to each member of another sequence or iterable, or to create a subsequence of those elements that satisfy a certain condition.
ToC
Simple list comprehension.
# Getting powers of two from a list from 0 to 9.
pow2 = [2 ** x for x in range(10)]
print(pow2)
List comprehension with a logic test.
# Getting powers of two from a list from 0 to 9 from elements greater than 5.
pow2 = [2 ** x for x in range(10) if x > 5]
print(pow2)
List comprehension with a logic test and an else statement.
Note that the syntax is different than with a simple test.
pow2 = [2 ** x if x > 5 else 0 for x in range(10)]
print(pow2)
List comprehension with two lists.
l1 = [x + y for x in ['Python ', 'C '] for y in ['Language', 'Programming']]
print(l1)
List comprehension with a lag.
l1 = [1, 2, 4, 2]
roll_sum = [y + x for x, y in zip(l1, l1[1:])]
print(roll_sum)
Beginner
Ranges
The range type represents an immutable sequence of numbers and is commonly used for looping a specific number of times in for loops.
While they share some similarities with lists, they are very different in terms of how python handle them.
A range is a much lighter object thant its list equivalent.
A range only needs to know :
ToC
r1 = range(0, 10, 2)
In this example, r1
starts at 0, ends at 9 (10 is exluded) and goes through element by 2.
Ranges have an array of interesting features.
start
attribute.
print(f"Starting point of the range : {r1.start}")
stop
attribute.
print(f"End point of the range (excluded) : {r1.stop}")
step
attribute.
print(f"Step of the range : {r1.step}")
index
method.
print(f"6th index of the range : {r1.index(6)}")
A list (or other collection) can be created from a range if it is absolutly needed but for iterating a range is often much prefered.
l1 = list(r1)
print(l1)
Intermediate
Deques.
Juste like ranges, deques are a more specialized collection. It is made to work as a stack or a queue.
A stack is a collection of elements where any new element is added on top and any removed element is taken from the top.
A queue is a collection of elements where any new element is added on top and any removed element is taken from the bottom.
ToC
dq1 = deque([1, 5, 8])
Adding elements can be done with deque.append
, deque.appendleft
,
deque.extend
or deque.extendleft
. Any of these methods work in a similar
fashion as they do with lists.
dq1.append(0)
print("Inserting 0 :")
print(dq1)
dq1.appendleft(12)
print("Inserting 12 to the left :")
print(dq1)
dq1.extend([2, 4])
print("Inserting [2, 4] :")
print(dq1)
dq1.extendleft([6, 5])
print("Inserting [6, 5] to the left :")
print(dq1)
Note the deque.insert
method allows to insert elements at specific indices.
Removing elements is as easy with the deque.pop
, deque.popleft
,
deque.remove
or deque.clear
methods.
dq1.pop()
print("Popping to the right :")
print(dq1)
dq1.popleft()
print("Popping to the left :")
print(dq1)
dq1.remove(2)
print("Remove the first occurence of 2 :")
print(dq1)
dq1.clear()
print("Purging the deque :")
print(dq1)
The deque.rotate(n)
method can be interesting when you have to shift
priorities in a stack or a queue.
dq1 = deque([1, 2, 3, 4, 5])
print("Before rotation :")
print(dq1)
dq1.rotate(2)
print("After rotating by 2 :")
print(dq1)
Intermediate
The itertools library.
The itertools library contains a large array of really interesting features helpful when working with list and iterables.
ToC
Flatten a nested list.
l1 = [[1, 2], [3, 4], [5, 6]]
print(list(itertools.chain.from_iterable(l1)))
Pairwise product of lists.
for p in itertools.product([1, 2, 3], [4, 5]):
print(p)
Permutations of elements of lists.
# Permutations à partir de listes.
for p in itertools.permutations([1, 2, 3, 4]):
print(p)
Combinations of elements of lists.
# Combinaisons.
for p in itertools.combinations([1, 2, 3, 4], 2):
print(p)
Cummulative min.
list(itertools.accumulate([9, 21, 17, 5, 11, 12, 2, 6], min))
The itertools.takewhile
method.
list(itertools.takewhile(lambda x: x < 3, [0, 1, 2, 3, 4]))
l1 = [1, 2, 3, 4, 2, 2, 3, 1, 4, 4, 4]
Most frequent element of a list.
print(max(set(l1), key=l1.count))
Index of min or max.
print(max(enumerate(l1), key=lambda x: x[1])[0])