Open In Colab

Intro to Python Basics

This tutorial was inspired by and adapted from Whirlwind Tour of Python [CC0 1.0 License], DartBrains [CC BY-SA 4.0 License] and the NumPy Tutorials [BSD-3-Clause License].


Types of Variables

  • Numeric types:

    • int, float, long, complex

  • string

  • boolean

    • True / False

Python’s simple types are summarized in the following table:

Python Scalar Types

Type

Example

Description

str

x = 'abc'

String: characters or text

int

x = 1

integers (i.e., whole numbers)

float

x = 1.0

floating-point numbers (i.e., real numbers)

complex

x = 1 + 2j

Complex numbers (i.e., numbers with real and imaginary part)

bool

x = True

Boolean: True/False values

NoneType

x = None

Special object indicating nulls

Use the type() function to find the type for a value or variable

# String
c = 'hello'
print(type(c))
<class 'str'>
# Integer
a = 1
print(type(a))
<class 'int'>
# Float
b = 1.0
print(type(b))
<class 'float'>
# Boolean
d = True
print(type(d))
<class 'bool'>
# None
e = None
print(type(e))
<class 'NoneType'>
# Cast integer to string
print(type(str(a)))
<class 'str'>

String Operators

Some of the arithmetic operators also have meaning for strings. E.g. for string concatenation use + sign

String repetition: Use * sign with a number of repetitions

# Assigning and combining strings
a = 'Hello'
b = 'World'
print(a + ' ' + b)
Hello World
# f-strings
firstname = 'Shawn'
print(f'My name is {firstname}')
My name is Shawn
# Combining f-strings
firstname = 'Shawn'
lastname = 'Rhoads'

print(f'My name is {firstname} {lastname}')
My name is Shawn Rhoads
# Repeat String without spaces
print(str(a)*5)
HelloHelloHelloHelloHello
# Repeat String with spaces
print(str(a+' ')*5)
Hello Hello Hello Hello Hello 

Math Operators

  • +, -, *, and /

  • Exponentiation **

  • Modulo % (Remainder)

# Addition
a = 2 + 7
print(f'a = {a}')
a = 9
# Subtraction
b = a - 5
print(f'b = a-5 = {a}-5 = {b}')
b = a-5 = 9-5 = 4
# Multiplication
print(f'b*2 = {b}*2 = {b*2}')
b*2 = 4*2 = 8
# Division
print(f'a/3 = {a}/3 = {a/3}')
a/3 = 9/3 = 3.0
# Exponentiation
print(f'b^2 = b**2 = {b}**2 = {b**2}')
b^2 = b**2 = 4**2 = 16
# Modulo 
print('(Modulo returns the remainder of dividing the left hand operand by right hand operand)')
print(f'remainder of b/9 = remainder of {b}/9 = {b%9}')
(Modulo returns the remainder of dividing the left hand operand by right hand operand)
remainder of b/9 = remainder of 4/9 = 4

Logical Operators

Perform logical comparison and return Boolean value

x == y # x is equal to y
x != y # x is not equal to y
x > y # x is greater than y
x < y # x is less than y
x >= y # x is greater than or equal to y 
x <= y # x is less than or equal to y
# Works for strings
a = 'hello'
b = 'world'
c = 'Hello'
print('a==b: ' + str(a==b))
print('a==c: ' + str(a==c))
print('a!=b: ' + str(a!=b))
print()
a==b: False
a==c: False
a!=b: True
# Works for numeric
d = 5
e = 8
print('d < e: ' + str(d < e))
print('d >= e: ' + str(d >= e))
d < e: True
d >= e: False

Conditional Logic (if…)

Unlike most other languages, Python uses tab formatting rather than closing conditional statements (e.g., end)

Syntax (note spacing):

if condition:
    do_something
elif condition:
    do_alternative
else:
    do_otherwise # often reserved to report an error
                 # after a long list of options

Implicit conversion of the value to bool() happens if condition is of a different type than bool, thus all of the following should work:

n = 2

if n:
    print("n is non-0 or not None")
n is non-0 or not None
if n is None:
    print("n is None")
    
if n is not str:
    print("n is not string")
n is not string
x = 20
y = 21

if y > x:
    print('y > x')
elif y < x:
    print('y < x')
y > x
if x==20 and y==21:
    print('True')
else:
    print('False')
True
if x==20 or y==100:
    print('True')
else:
    print('False')
True
if x==200 or y==100:
    print('True')
else:
    print('False')
False

It’s also possible to use a while loop to repeat statements while condition remains True:

while condition do:
    do_statements
x = 0
end = 10

csum = 0
while x < end:
    csum += x
    print(x, csum)
    x += 1
print("Exited with x==%d" % x )
0 0
1 1
2 3
3 6
4 10
5 15
6 21
7 28
8 36
9 45
Exited with x==10

Python Containers

There are 4 main types of builtin containers for storing data in Python:

  • list \(\checkmark\)

  • dict \(\checkmark\)

  • set

  • tuple

Lists

In Python, a list is a mutable sequence of values. Mutable means that we can change separate entries within a list.

  • Each value in the list is an element or item

  • Elements can be any Python data type

  • Lists can mix data types

  • Lists are initialized with [] or list()

l = [1,2,3]
  • Elements within a list are indexed (starting with 0)

l[0]
  • Elements can be nested lists

nested = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
  • Lists can be sliced.

l[start:stop:stride]
  • Like all python containers, lists have many useful methods that can be applied

a.insert(index,new element)
a.append(element to add at end)
len(a)
list_a = [1,2,3]
print(list_a)
[1, 2, 3]
list_of_strings = ['hello', 'bye', 'hi']
print(list_of_strings)
['hello', 'bye', 'hi']
# Indexing and Slicing
list_b = ['list','of','things']
print(list_b[0])
print(list_b[1:3])
list
['of', 'things']
list = [1,2,3,4,5]
list[0::2]
[1, 3, 5]
# List methods (insert)
list_b.insert(2,'python')
print(list_b)
['list', 'of', 'python', 'things']
# List methods (append)
list_b.append('.')
print(list_b)
['list', 'of', 'python', 'things', '.']
# Methods could be applied to other objects (like strings)
example_string = 'PSYC 347 - Computational Models of Human Social Behavior and Neuroscience'
print(example_string)
print(example_string.upper())
PSYC 347 - Computational Models of Human Social Behavior and Neuroscience
PSYC 347 - COMPUTATIONAL MODELS OF HUMAN SOCIAL BEHAVIOR AND NEUROSCIENCE

Dictionaries

  • In Python, a dictionary (or dict) is mapping between a set of indices (keys) and a set of values

  • The items in a dictionary are key-value pairs

  • Keys can be any Python data type

  • Dictionaries are unordered

# Dictionaries
eng2sp = {}
eng2sp['one'] = 'uno'
print(eng2sp)
{'one': 'uno'}
eng2sp = {'one': 'uno', 'two': 'dos', 'three': 'tres'}
print(eng2sp)
{'one': 'uno', 'two': 'dos', 'three': 'tres'}
# keys vs values
print(eng2sp.keys())
print(eng2sp.values())
dict_keys(['one', 'two', 'three'])
dict_values(['uno', 'dos', 'tres'])

Loops

for loop is probably the most popular loop construct in Python:

for target in sequence:
    do_statements
for i in [0, 1, 2, 3, 4, 5]:
    print(i)
0
1
2
3
4
5
for i in range(6):
    print(i)
0
1
2
3
4
5
for i in range(2,5):
    print(i)
2
3
4
# indexing using for loops
list_a = ['P','S','Y','C','H']

for index, value in enumerate(list_a):
    print(index, value)
0 P
1 S
2 Y
3 C
4 H
example_string = "Python is making science easier"
for c in example_string:
    print(c)
P
y
t
h
o
n
 
i
s
 
m
a
k
i
n
g
 
s
c
i
e
n
c
e
 
e
a
s
i
e
r
# appending to lists in for loop
print(list_b)

print('\n...running for loop\n')
for value in ['a','b','c']:
    list_b.append(value)
    
print(list_b)
['list', 'of', 'python', 'things', '.']

...running for loop

['list', 'of', 'python', 'things', '.', 'a', 'b', 'c']
# manipulating lists using for loops
for value in ['m','o','d','e','l','s']:
    print(value.upper())
M
O
D
E
L
S
# only capitalize every other letter
for index, value in enumerate(['m','o','d','e','l','s']):
    if index % 2:
        print(value.upper())
    else:
        print(value)
m
O
d
E
l
S

As an alternative, list comprehension is a very powerful technique allowing for efficient construction of new lists.

[a for a in l]
# list comprehension
list_example = [10, 11, 12, 13, 14, 15]

print([x*9 for x in list_example])
[90, 99, 108, 117, 126, 135]
# List Comprehension with methods
list_example2 = ['words', 'to', 'change']

print([x.upper() for x in list_example2])
['WORDS', 'TO', 'CHANGE']

Functions

A function is a named sequence of statements that performs a computation. You define the function by giving it a name, specify a sequence of statements, and optionally values to return. Later, you can “call” the function by name.

def make_upper_case(text):
    return (text.upper())
  • The expression in the parenthesis is the argument.

  • It is common to say that a function “takes” an argument and “returns” a result.

  • The result is called the return value.

The first line of the function definition is called the header; the rest is called the body.

The header has to end with a colon and the body has to be indented. It is a common practice to use 4 spaces for indentation, and to avoid mixing with tabs.

Function body in Python ends whenever statement begins at the original level of indentation. There is no end or fed or any other identify to signal the end of function. Indentation is part of the the language syntax in Python, making it more readable and less cluttered.

string = "Python is making science easier"
string_upper = string.upper()
print(string_upper)
PYTHON IS MAKING SCIENCE EASIER
make_upper_case(string)
'PYTHON IS MAKING SCIENCE EASIER'

Classes

(from: https://realpython.com/python3-object-oriented-programming/)

"The primitive data structures available in Python, like numbers, strings, and lists are designed to represent simple things like the cost of something, the name of a poem, and your favorite colors, respectively.

What if you wanted to represent something much more complicated?

For example, let’s say you wanted to track a number of different animals. If you used a list, the first element could be the animal’s name while the second element could represent its age.

How would you know which element is supposed to be which? What if you had 100 different animals? Are you certain each animal has both a name and an age, and so forth? What if you wanted to add other properties to these animals? This lacks organization, and it’s the exact need for classes.

Classes are used to create new user-defined data structures that contain arbitrary information about something. In the case of an animal, we could create an Animal() class to track properties about the Animal like the name and age.

It’s important to note that a class just provides structure—it’s a blueprint for how something should be defined, but it doesn’t actually provide any real content itself. The Animal() class may specify that the name and age are necessary for defining an animal, but it will not actually state what a specific animal’s name or age is.

It may help to think of a class as an idea for how something should be defined.”

Let’s create a Cat class! What are some attributes of a Cat? It should have a name and age! It can also be tired and hungry. What are some actions (or methods) that the Cat can do? It feed and sleep and meow! Should feeding and sleeping update specific attributes of the Cat? Now that we have our Cat, let’s code it up!

class Cat:

    # Initializer / Instance Attributes
    def __init__(self, name, age):
        self.name = name
        self.age = age
        self.hungry = True
        self.tired = True

    # instance method
    def description(self):
        __name_age__ = "{} is {} years old. ".format(self.name, self.age)
        
        if self.tired == True:
            __is_tired__ = "{} is tired. ".format(self.name)
        else:
            __is_tired__ = "{} is not tired. ".format(self.name)
        
        if self.hungry == True:
             __is_hungry__ = "{} is hungry. ".format(self.name)
        else:
            __is_hungry__ = "{} is not hungry. ".format(self.name)
        
        return str(__name_age__+__is_tired__+__is_hungry__)

    # instance method
    def meow(self):
        return "{} says {}".format(self.name, "Meow! Meow!")
    
    def feed(self):
        self.hungry = False
        print(self.meow())
        
    def sleep(self):
        self.tired = False
# Instantiate the Cat object
kitty = Cat("Fat Cat", 6)
# call our instance methods
print(kitty.description())
print(kitty.meow())
Fat Cat is 6 years old. Fat Cat is tired. Fat Cat is hungry. 
Fat Cat says Meow! Meow!
kitty.feed()
Fat Cat says Meow! Meow!
print(kitty.description())
Fat Cat is 6 years old. Fat Cat is tired. Fat Cat is not hungry. 
kitty.sleep()
print(kitty.description())
Fat Cat is 6 years old. Fat Cat is not tired. Fat Cat is not hungry. 
kitty.name = 'Shawn'
kitty.meow()
'Shawn says Meow! Meow!'

Modules

A Module is a python file that contains a collection of related definitions. Python has hundreds of standard modules. These are organized into what is known as the Python Standard Library. You can also create and use your own modules. To use functionality from a module, you first have to import the entire module or parts of it into your namespace

To import the entire module: import module_name

You can also import a module using a specific name: import module_name as new_module_name

To import specific definitions (e.g. functions, variables, etc) from the module into your local namespace: from module_name import name1, name2

import os
from glob import glob

To get the curent directory, you can use: os.path.abspath(os.path.curdir)

Let’s use glob, a pattern matching function. We will use an if statement to print:

  • A list of directories if we are using Google Colab

  • A list of ipynb files in the current folder if we are using Jupyter Notebook locally

if 'google.colab' in str(get_ipython()):
    print('Running on Colab... Printing list of directories')
    data_file_list = glob('/*')
else:
    print('Not running on Colab... Printing list of files')
    data_file_list = glob(os.path.join(os.path.curdir,'*ipynb'))
print(data_file_list)
Not running on Colab... Printing list of files
['./module-01-03_Python-Exercises.ipynb', './module-01-02_Working-with-Data.ipynb', './module-04-01_Prosocial-RL-Exercises.ipynb', './module-01-00_Jupyter-Notebooks-Colab.ipynb', './module-03-00_Two-Armed-Bandit.ipynb', './module-01-00_Jupyter-Notebooks.ipynb', './module-04-00_Social-Learning.ipynb', './module-02-00_Linear-Modeling.ipynb', './module-01-01_Intro-to-Python.ipynb', './module-03-02_RL-Exercises.ipynb', './module-02-02_Modeling-Exercises.ipynb', './module-03-01_Models-of-Learning.ipynb', './module-02-01_Nonlinear-Modeling.ipynb']

This gives us a list of the files including the relative path from the current directory. What if we wanted just the filenames? There are several different ways to do this. First, we can use the the os.path.basename function. We loop over every file, grab the base file name and then append it to a new list.

file_list = []
for f in data_file_list:
    file_list.append(os.path.basename(f))

print(file_list)
['module-01-03_Python-Exercises.ipynb', 'module-01-02_Working-with-Data.ipynb', 'module-04-01_Prosocial-RL-Exercises.ipynb', 'module-01-00_Jupyter-Notebooks-Colab.ipynb', 'module-03-00_Two-Armed-Bandit.ipynb', 'module-01-00_Jupyter-Notebooks.ipynb', 'module-04-00_Social-Learning.ipynb', 'module-02-00_Linear-Modeling.ipynb', 'module-01-01_Intro-to-Python.ipynb', 'module-03-02_RL-Exercises.ipynb', 'module-02-02_Modeling-Exercises.ipynb', 'module-03-01_Models-of-Learning.ipynb', 'module-02-01_Nonlinear-Modeling.ipynb']

It is also sometimes even cleaner to do this as a list comprehension

[os.path.basename(x) for x in data_file_list]
['module-01-03_Python-Exercises.ipynb',
 'module-01-02_Working-with-Data.ipynb',
 'module-04-01_Prosocial-RL-Exercises.ipynb',
 'module-01-00_Jupyter-Notebooks-Colab.ipynb',
 'module-03-00_Two-Armed-Bandit.ipynb',
 'module-01-00_Jupyter-Notebooks.ipynb',
 'module-04-00_Social-Learning.ipynb',
 'module-02-00_Linear-Modeling.ipynb',
 'module-01-01_Intro-to-Python.ipynb',
 'module-03-02_RL-Exercises.ipynb',
 'module-02-02_Modeling-Exercises.ipynb',
 'module-03-01_Models-of-Learning.ipynb',
 'module-02-01_Nonlinear-Modeling.ipynb']

NumPy

NumPy is the fundamental package for scientific computing with Python.

import numpy as np

NumPy’s main object is the homogeneous multidimensional array. It is a table of elements (usually numbers), all of the same type, indexed by a tuple of non-negative integers. In NumPy dimensions are called axes.

NumPy’s array class is called ndarray. It is also known by the alias array. The more important attributes of an ndarray object are:

  • ndarray.ndim: the number of axes (dimensions) of the array.

  • ndarray.shape: the dimensions of the array. This is a tuple of integers indicating the size of the array in each dimension. For a matrix with n rows and m columns, shape will be (n,m). The length of the shape tuple is therefore the number of axes, ndim.

  • ndarray.size: the total number of elements of the array. This is equal to the product of the elements of shape.

  • ndarray.dtype: an object describing the type of the elements in the array. One can create or specify dtype’s using standard Python types. Additionally NumPy provides types of its own. numpy.int32, numpy.int16, and numpy.float64 are some examples.

  • ndarray.itemsize: the size in bytes of each element of the array. For example, an array of elements of type float64 has itemsize 8 (=64/8), while one of type complex32 has itemsize 4 (=32/8). It is equivalent to ndarray.dtype.itemsize.

  • ndarray.data: the buffer containing the actual elements of the array. Normally, we won’t need to use this attribute because we will access the elements in an array using indexing facilities.

a = np.arange(15) #array of numbers 0 to 14
a
array([ 0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11, 12, 13, 14])
a.dtype
dtype('int64')
print(a)
[ 0  1  2  3  4  5  6  7  8  9 10 11 12 13 14]
print(a.shape)
(15,)
print(a.ndim)
1
print(a.dtype.name)
int64
print(a.itemsize)
8
print(a.size)
15
print(type(a))
<class 'numpy.ndarray'>

Creating arrays

You can create an array from a regular Python list or tuple using the array function. The type of the resulting array is deduced from the type of the elements in the sequences.

A frequent error consists in calling array with multiple numeric arguments, rather than providing a single list of numbers as an argument.

a = np.array(1,2,3,4)    # WRONG
a = np.array([1,2,3,4])  # RIGHT
b = np.array([6, 7, 8])
print(b)
[6 7 8]
print(type(b))
<class 'numpy.ndarray'>

array transforms sequences of sequences into two-dimensional arrays, sequences of sequences of sequences into three-dimensional arrays, and so on.

c = np.array([(1.5, 2 ,3), (4, 5, 6), (7.1, 7.2, 7.3)])
print(c)
[[1.5 2.  3. ]
 [4.  5.  6. ]
 [7.1 7.2 7.3]]
print(c.shape)
(3, 3)

The function zeros creates an array full of zeros, the function ones creates an array full of ones, the function random.rand creates an array of random floats from a uniform distribution over [0, 1], and the function empty creates an array whose initial content is random and depends on the state of the memory. By default, the dtype of the created array is float64.

np.zeros((3,4))
array([[0., 0., 0., 0.],
       [0., 0., 0., 0.],
       [0., 0., 0., 0.]])
test = np.ones((2,3,4), dtype=np.int16)
print(test)
[[[1 1 1 1]
  [1 1 1 1]
  [1 1 1 1]]

 [[1 1 1 1]
  [1 1 1 1]
  [1 1 1 1]]]
np.random.rand(3,2)
array([[0.90508547, 0.71195178],
       [0.72020388, 0.16838038],
       [0.47611324, 0.5349403 ]])
np.empty((2,3)) # uninitialized, output may vary
array([[0.90508547, 0.71195178, 0.72020388],
       [0.16838038, 0.47611324, 0.5349403 ]])

To create sequences of numbers, NumPy provides a function analogous to range that returns arrays instead of lists.

np.arange( 10, 30, 5 ) # array from 10 to 30 in increments of 5
array([10, 15, 20, 25])

Shape Manipulation

Three main functions include:

  • ravel() flattens an array

  • reshape() changes the shape of arrays

  • transpose() transposes the array

example = np.random.rand(4,4)
print(example)
[[0.14101142 0.52124336 0.04065392 0.36218001]
 [0.2233788  0.0250243  0.12087429 0.96521804]
 [0.75529428 0.98640117 0.64910287 0.74553599]
 [0.23558827 0.09218231 0.80696487 0.45230181]]
example*10
array([[1.41011423, 5.21243359, 0.40653922, 3.62180012],
       [2.23378805, 0.25024297, 1.20874287, 9.65218044],
       [7.55294279, 9.86401174, 6.49102866, 7.45535994],
       [2.35588273, 0.92182308, 8.06964873, 4.5230181 ]])
example_flat = example.ravel()  # returns the array, flattened
print(example_flat)
[0.14101142 0.52124336 0.04065392 0.36218001 0.2233788  0.0250243
 0.12087429 0.96521804 0.75529428 0.98640117 0.64910287 0.74553599
 0.23558827 0.09218231 0.80696487 0.45230181]
example_flat.reshape(2,8) # returns the array with a modified shape 2x8
array([[0.14101142, 0.52124336, 0.04065392, 0.36218001, 0.2233788 ,
        0.0250243 , 0.12087429, 0.96521804],
       [0.75529428, 0.98640117, 0.64910287, 0.74553599, 0.23558827,
        0.09218231, 0.80696487, 0.45230181]])
example_flat.reshape(4,4) # returns the array back to original shape
array([[0.14101142, 0.52124336, 0.04065392, 0.36218001],
       [0.2233788 , 0.0250243 , 0.12087429, 0.96521804],
       [0.75529428, 0.98640117, 0.64910287, 0.74553599],
       [0.23558827, 0.09218231, 0.80696487, 0.45230181]])
test = np.random.rand(2,3,6)
print(test)
[[[0.47246735 0.93497331 0.6176843  0.24898456 0.03014371 0.97996762]
  [0.63009135 0.45392435 0.9490089  0.58247366 0.74585935 0.36274748]
  [0.22745285 0.93103471 0.46541396 0.43484719 0.65265412 0.65606734]]

 [[0.41280335 0.13149874 0.1255207  0.39640958 0.40324838 0.97229183]
  [0.33352982 0.74574888 0.98623868 0.26142712 0.08086071 0.36000286]
  [0.71168076 0.93815648 0.77675183 0.86574354 0.21302474 0.78735483]]]
print(test.ravel())
[0.47246735 0.93497331 0.6176843  0.24898456 0.03014371 0.97996762
 0.63009135 0.45392435 0.9490089  0.58247366 0.74585935 0.36274748
 0.22745285 0.93103471 0.46541396 0.43484719 0.65265412 0.65606734
 0.41280335 0.13149874 0.1255207  0.39640958 0.40324838 0.97229183
 0.33352982 0.74574888 0.98623868 0.26142712 0.08086071 0.36000286
 0.71168076 0.93815648 0.77675183 0.86574354 0.21302474 0.78735483]
print(example.transpose())
[[0.14101142 0.2233788  0.75529428 0.23558827]
 [0.52124336 0.0250243  0.98640117 0.09218231]
 [0.04065392 0.12087429 0.64910287 0.80696487]
 [0.36218001 0.96521804 0.74553599 0.45230181]]

“The Zen of Python”

from Whirlwind Tour of Python

“Python aficionados are often quick to point out how “intuitive”, “beautiful”, or “fun” Python is. While I tend to agree, I also recognize that beauty, intuition, and fun often go hand in hand with familiarity, and so for those familiar with other languages such florid sentiments can come across as a bit smug. Nevertheless, I hope that if you give Python a chance, you’ll see where such impressions might come from. And if you really want to dig into the programming philosophy that drives much of the coding practice of Python power-users, a nice little Easter egg exists in the Python interpreter: simply close your eyes, meditate for a few minutes, and import this

import this
The Zen of Python, by Tim Peters

Beautiful is better than ugly.
Explicit is better than implicit.
Simple is better than complex.
Complex is better than complicated.
Flat is better than nested.
Sparse is better than dense.
Readability counts.
Special cases aren't special enough to break the rules.
Although practicality beats purity.
Errors should never pass silently.
Unless explicitly silenced.
In the face of ambiguity, refuse the temptation to guess.
There should be one-- and preferably only one --obvious way to do it.
Although that way may not be obvious at first unless you're Dutch.
Now is better than never.
Although never is often better than *right* now.
If the implementation is hard to explain, it's a bad idea.
If the implementation is easy to explain, it may be a good idea.
Namespaces are one honking great idea -- let's do more of those!