Week 3 - Dive Into Python, Part I¶
This week's Jupyter notebook introduces us to basic data types and actions in Python.
Without further ado, let's start:
# Heads-up: This is the syntax for writing comments in Python.
# Start a line with the "#" character to turn it into a comment.
# Comments are ignored by the interpreter, so you can use them for annotation purposes
# You can use the print() function to display simple data types such as numbers and strings.
print("Hello, world!")
Hello, world!
Numbers, variables and basic arithmetic¶
# Initialize a variable called "a" with a value of 5
a = 5
print(a)
5
# We don't have to use print() each time in notebook cells or using the interactive interpreter.
# Just using the name of a variable will also display its value:
a
5
Assigning a number this way will create a variable of type int. If unsure, one can always check the type of a variable with the type() built-in method.
type(a)
int
# We can change the value of a variable as follows
a = 100
a
100
b = 200
# Addition is as simple as
a + b
300
# If we want to save the result of an expression we can assign it to a variable
c = a * b - 10
c
19990
Aside from the four basic arithmetic operators +, -, *, /, there exist other operators in Python as well.
One such example is the power operator (**), where a ** b can be used for calculating $a^b$:
a ** 5
10000000000
Note: The int type is flexible in terms of representing relatively huge numbers:
a ** 100
100000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000
type(a)
int
We can also represent floating-point numbers just as easily:
a = 5.0
type(a)
float
The following syntax also works for representing float types:
5.
5.0
.5
0.5
Here's an example of a complex type, which is specified by having the j suffix after the imaginary part of the number:
c = 3 + 4j
type(c)
complex
As an example, this type comes with its own multiplication operator:
c * c
(-7+24j)
Libraries¶
(Note: we learn about libraries more in detail, later...)
Some useful functions, such as sin() and cos(), are not directly available:
cos(0)
--------------------------------------------------------------------------- NameError Traceback (most recent call last) Cell In[18], line 1 ----> 1 cos(0) NameError: name 'cos' is not defined
To be able to use these functions, we have to import a library that contains them. Such useful math functions can be accessed by importing the math library, as follows:
import math
To use a function from a library, we use the lname.fname(...) syntax, where lname represents the imported library name and fname represents the function name.
As an example:
math.pow(2, 5)
32.0
Aside from functions, can also provide special values such as pi:
math.cos(math.pi)
-1.0
Boolean values and logic¶
We can define variables to have the Boolean values of True and False to do logical operations.
a = True
b = False
The and binary operator evaluates to True if both of its operands are True, otherwise it evaluates to False.
a and b
False
b and b
False
a and a
True
The or binary operator evalues to True if any of its operands are True, otherwise it evaluates to False.
a or b
True
b or b
False
a or a
True
not negates the value of a Boolean variable.
not True
False
not False
True
Comparison operators (such as >, <, >=, <=, ==, !=) will return Boolean values:
# Equality
10 == 10
True
3 > 5
False
# Inequality
3 != 3.1
True
Container datatypes¶
Python gives us many types for storing data. We'll explore each one, starting from strings:
Strings¶
Strings represent a sequence of characters, and are created with using enclosing single (') or double (") quotation marks:
s = 'hello'
type(s)
str
Python allows us to do several operations with indexes, one such example being concatenation:
d = 'hello'
e = 'world'
d + e
'helloworld'
# If we want a space between the words, we can do this:
d + ' ' + e
'hello world'
Although both numbers and strings are able to use +, Python will not allow mixing integers with strings:
10 + '10'
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) Cell In[38], line 1 ----> 1 10 + '10' TypeError: unsupported operand type(s) for +: 'int' and 'str'
To convert a string into a number, we can use int():
10 + int(10)
20
We can also check if a string contains another string inside it, using the in keyword:
'hello' in 'hello, world'
True
Similarly, we can check if it doesn't contain such a substring, with:
'hello' not in 'hello, world'
False
Strings are case sensitive:
'Hello' == 'hello'
False
The len() function allows us to get the length of a string:
len('Hello')
5
# Don't forget, each character (including spaces and punctuation) contributes to the length:
len('Hello, world!')
13
Strings and other sequential containers allow repetition with *:
'hello' * 3
'hellohellohello'
Indexing and Slicing¶
Sequential containers such as strings and lists allow us to access elements using their position (index) in the container. Indexing and slicing use square brackets [].
Note 1: In python, indexes start at 0. This means that the first character you see in the string has index 0, the second character has index 1, and so on.
Note 2: Python allows negative indexing. Negative indexes start by counting from the end of the string.
It's best to proceed with a simple visualization of a string (ceng240) along with its positive and negative indexes:
| 0 | 1 | 2 | 3 | 4 | 5 | 6 | Positive indexing |
|---|---|---|---|---|---|---|---|
| c | e | n | g | 2 | 4 | 0 | |
| -7 | -6 | -5 | -4 | -3 | -2 | -1 | Negative indexing |
For example, the 'g' element can be accessed via positive indexing (3), as well as negative indexing (-4).
s = 'ceng240'
Simple indexing has the syntax of container[idx], which will access the element with the given index. Here are some examples with positive indexing:
# The "first" character has the index 0
s[0]
'c'
s[3]
'g'
# To access the last element, we can use
s[len(s) - 1]
'0'
Here are some examples with negative indexing:
# First element from the end
s[-1] # not s[-0], as that is just s[0]
'0'
s[-3]
'2'
One can observe that for negative indexing, we have the following equality:
s[-idx] = s[len(s) - idx]
s[-5] == s[len(s) - 5]
True
With the s[idx1:idx2] syntax, Python allows us to access a slice (i.e, multiple elements) of a container. The slice starts from the first index (inclusive), up to (but not including) the second index parameter.
Some examples of slicing:
s[0:4]
'ceng'
s[2:3]
'n'
# This will also work (if confused, check the table)
s[-3:-1]
'24'
If idx1 is left empty, the slice will start from the start of the container.
(i.e, by default, idx1 = 0)
If idx2 is left empty, the slice will continue until the end of the container.
(i.e, by default, idx2 = length of the container)
# This will skip the initial element
s[1:]
'eng240'
# And this will slice the first 5 - 1 elements of the string
s[:5]
'ceng2'
# Get the last 3 characters of the string:
s[-3:]
'240'
# Slice all the characters except the last 4:
s[:-3]
'ceng'
(you can add additional cells under this cell to practice your slicing skills)
Finally, Python allows strided indexing, with the s[idx1:idx2:stride] syntax.
The stride (or increment) parameter allows the slice to "skip over" consecutive elements of the string. After indexing the first element (with idx1), the next index will be selected by incrementing (or decrementing!) the current index with stride, and so on.
It's best to learn by example, as always:
# This is the usual behavior of slicing, where the next index is selected by incrementing by 1:
s[::1]
'ceng240'
# Now, after selecting the first element, we will select every other element:
s[::2]
'cn20'
# This will allow you to get the even numbers
'0123456789'[::2]
'02468'
# To get the odd numbers, you could do this
'0123456789'[1::2]
'13579'
# Classic trick to "invert" a string:
s[::-1]
'042gnec'
# One final example of strided slicing:
'0123456789'[::-3]
'9630'
Leaving the stride parameter empty is the same as setting it to 1:
s[::] == s[::1]
True
Special characters¶
The following will not be accepted by Python, because we are trying to use quotation marks inside a string:
# Python will complain about this
j = "I said "hello, world""
Cell In[67], line 2 j = "I said "hello, world"" ^ SyntaxError: invalid syntax
To solve this, we can use single quotes (') to define the string, and use double quotes (") inside the string:
j = 'I said "hello, world"'
j
'I said "hello, world"'
The same technique can be used for using single quotes in a string:
j = "I said 'hello, world'"
j
"I said 'hello, world'"
Alternatively, we can use the backslash (\) character to make Python understand that this character should be inside the string:
# We escape the quotes by using \"
j = "I said \"hello, world\""
j
'I said "hello, world"'
We can make multi-line strings by using the docstring syntax, where the string is enclosed by three single quotation marks ('''text''').
k = '''
hello
world
ceng240'''
Note that this will also preserve the lines:
print(k)
hello
world
ceng240
When we display k directly, we see the following:
k
'\n hello\n world\n ceng240'
Here, \n is what Python uses to represent a vertical line, as a special character (called the newline character). We can use it ourselves to print text in different lines as follows:
print("hello\nworld")
hello world
Note: Strings are immutable, which means after we create a string, it is not possible to change its content:
s = 'hello'
s[0] = 'j'
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) Cell In[75], line 2 1 s = 'hello' ----> 2 s[0] = 'j' TypeError: 'str' object does not support item assignment
Here, we would create a new string:
'j' + s[1:]
'jello'
Lists and Tuples¶
Lists (and tuples) are sequential containers that can hold data of different types inside. They are defined using square brackets [ ].
# Create a list of random stuff
l = ['a', 10, 'c', False]
type(l)
list
l
['a', 10, 'c', False]
As we saw with strings, we can do common operations on lists, such as slicing and indexing:
l[1:]
[10, 'c', False]
l[::-1]
[False, 'c', 10, 'a']
Lists can be elements of other lists, as follows:
l = ['1',
2,
'a',
['s', 0, 10]]
# Accessing an element in a list inside a list:
l[3][0]
's'
You could represent a matrix as a list of lists:
a = [
[1,2,3],
[3,4, 5],
[1, 10, 8]
]
a[2][2]
8
Lists are mutable, which means the data inside the container can be changed after initialization:
l = [1, '2', 10, [1], 5]
l[0] = 'hello'
l
['hello', '2', 10, [1], 5]
If we want the data to unchangeable after initialization, we use tuples instead, which are immutable. Tuples use parentheses () instead of square brackets.
t = (1, '2', 10, [1], 5)
type(t)
tuple
t[0] = 'hello'
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) Cell In[86], line 1 ----> 1 t[0] = 'hello' TypeError: 'tuple' object does not support item assignment
# You can create a tuple from a list via tuple(), and vice versa via list()
t2 = tuple(l)
t2
('hello', '2', 10, [1], 5)
As lists are mutable, we can add new elements, or remove elements from the list.
In particular, [] by itself represents an empty list.
len([])
0
l
['hello', '2', 10, [1], 5]
# Remove via the del keyword
del l[0]
l
['2', 10, [1], 5]
# Remove via accessing and setting to empty list
l[0:2] = []
l
[[1], 5]
Adding elements via list.append() will add an element to the end of the list:
l.append(5)
l
[[1], 5, 5]
Lists allow concatenation (like strings), so you can combine lists or add new elements like the following:
[1, 2, 3] + [4, 5, 6]
[1, 2, 3, 4, 5, 6]
l = [9, 8, 0]
l += [4]
l
[9, 8, 0, 4]
# Insertion at a particular index
l.insert(3, 'hello')
l
[9, 8, 0, 'hello', 4]
Lists allow repetition, like strings:
l * 2
[9, 8, 0, 'hello', 4, 9, 8, 0, 'hello', 4]
And finally, one can check for membership in lists using the in / not in syntax:
l = [1, 'c', 10, False, 'Hello']
1 in l
True
2 in l
False
False in l
True
10 not in l
False
Dict¶
Dictionaries are efficient containers which hold key-value pairs, where each key accesses a specific value in the dictionary. They are defined using curly braces {}.
# An empty dict
d = {}
type(d)
dict
Adding a key-value pair to a dict is done by the following syntax:
d['key'] = 'value'
d
{'key': 'value'}
# Adding a pair with an integer key
d[10] = True
d
{'key': 'value', 10: True}
We can check if a value exists for a key by the following syntax:
d['key'] # Will return 'value'
'value'
If the provided key does not exist, Python will complain with a KeyError:
d['hello']
--------------------------------------------------------------------------- KeyError Traceback (most recent call last) Cell In[105], line 1 ----> 1 d['hello'] KeyError: 'hello'
# We can use the get() method for accesing a key-value pair as well:
d.get('key')
'value'
# However, Python will not throw a KeyError if that key does not exist
d.get('hello')
Here's an example of a dictionary with different keys and values:
person = {
'name': 'Cagri Eser',
'email': 'cagrie@metu.edu.tr',
'age': 26,
'phone': 123
}
person['name']
'Cagri Eser'
We can change the value of a key using the same syntax:
person['phone'] = 456
person['phone']
456
And we can delete a key-value pair by specifying the key:
del person['age']
person
{'name': 'Cagri Eser', 'email': 'cagrie@metu.edu.tr', 'phone': 456}
We can access the individual keys and values with:
person.keys()
dict_keys(['name', 'email', 'phone'])
person.values()
dict_values(['Cagri Eser', 'cagrie@metu.edu.tr', 456])
# We haven't seen for loops, but here's a sneak peek:
for key, value in person.items():
print(key, value)
name Cagri Eser email cagrie@metu.edu.tr phone 456
# Checking for a key using the 'in' syntax
'name' in person
True
Sets¶
Sets are similar to dicts, and they also use curly brackets {}. Sets are mutable, however, elements of sets must be immutable. Each element in a set can appear only once.
s = {1, 2, 3}
type(s)
set
# Sets do not support indexing
s[0]
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) Cell In[117], line 2 1 # Sets do not support indexing ----> 2 s[0] TypeError: 'set' object is not subscriptable
Sets support addition and removal of new elements.
s.add(4)
s
{1, 2, 3, 4}
s.add('hello')
s
{1, 2, 3, 4, 'hello'}
# Lists can't be added to sets as they are mutable
s.add([1, 2])
--------------------------------------------------------------------------- TypeError Traceback (most recent call last) Cell In[120], line 2 1 # Lists can't be added to sets as they are mutable ----> 2 s.add([1, 2]) TypeError: unhashable type: 'list'
# However, tuples can be added
s.add((1, 2))
s
{(1, 2), 1, 2, 3, 4, 'hello'}
For immutable sets, you can use frozenset.
s2 = frozenset(s)
print(type(s2))
s2.add(5)
<class 'frozenset'>
--------------------------------------------------------------------------- AttributeError Traceback (most recent call last) Cell In[122], line 3 1 s2 = frozenset(s) 2 print(type(s2)) ----> 3 s2.add(5) AttributeError: 'frozenset' object has no attribute 'add'
Sets have useful functions such as union, intersection and other set operations.
s
{(1, 2), 1, 2, 3, 4, 'hello'}
s.intersection({1, 3, 4})
{1, 3, 4}
{1, 2}.union({3, 4})
{1, 2, 3, 4}
Expressions, Execution Logic, Ordering and Conversions¶
Expressions represent calculations from operations among data. These can be arithmetical operations, logical operations, indexing and membership. In terms of priority (precedence), they are ordered in the following manner:
indexing ([]) -> exponentiation (**) -> multiplication / division based operators (*, /, //, %) -> addition / subtraction (+, -) -> logical comparison (>, <=, etc.) and membership (in, not in) -> other logical operators (not > and > or)
Of course, to avoid confusion, don't forget to use parenthesis wherever necessary.
2 + 5 * 7
37
(2 + 5) * 7
49
When dealing with logical and and or, Python will take shortcuts instead of evaluating everything directly.
Consider the following examples:
# Python will throw an error when you divide an int or float by zero.
5 / 0
--------------------------------------------------------------------------- ZeroDivisionError Traceback (most recent call last) Cell In[128], line 2 1 # Python will throw an error when you divide an int or float by zero. ----> 2 5 / 0 ZeroDivisionError: division by zero
# Normally, you would expect an error to be thrown here as well:
True or 5 / 0
True
In the example above, since True or X will have the boolean value of True for any Boolean value of X, the result will be True without evaluating the value of X. Same for the example below:
False and 5 /0
False
When the result is not clear from the first operand, then the second operand will also be evaluated:
True and 5/ 0
--------------------------------------------------------------------------- ZeroDivisionError Traceback (most recent call last) Cell In[131], line 1 ----> 1 True and 5/ 0 ZeroDivisionError: division by zero
However, Python evaluates such expressions from left-to-right. The following expression, even if it would be trivially True given that X or True is always True, will evaluate the first operand and then throw an error:
# Always True?
(5/0) or True
--------------------------------------------------------------------------- ZeroDivisionError Traceback (most recent call last) Cell In[132], line 2 1 # Always True? ----> 2 (5/0) or True ZeroDivisionError: division by zero
Type conversion¶
When applying a binary operator, Python will attempt to implicitly convert one data type to another if they are different types.
a = 5
type(a)
int
b = 5.2
type(b)
float
print(a+b)
type(a+b)
10.2
float
c = 5 +4j
type(c)
complex
print(c+a)
type(c+a)
(10+4j)
complex
In the case of boolean values:
When implicitly converting a bool to a number, True will be converted to 1 and False will be converted to 0.
When implicitly converting a number to bool, 0 will be converted to False, and any non-zero number will be converted to 1.
True + 4
5
True + False
1
bool(5)
True
Python supports multiple assignment, and you can also swap values of variables in place without having to use a third temporary variable.
a, b, c = 10, 20, 30
a, b, c
(10, 20, 30)
# This is also possible
l = [8, 9]
a, b = l
a, b
(8, 9)
# Swapping values
a,b = b,a
a, b
(9, 8)
# Using a third variable:
print(a, b)
tmp = a
a = b
b = tmp
print(a, b)
9 8 8 9