Cognitive Computing 1 and 2 -- Python programming

[Before working through this document, everyone should have read "Notes on cognitive computing" (in lecture materials) by now!]

This document is a Jupyter Notebook. It is a combination of text (like this sentence you are currently reading), Python code, and Python code output.

Basically, text and code (+ output) are organized into 'cells' which can be 'run' or executed independently.



In [2]:

print("Hello world!")




Hello world!



Hey, you just ran your first line of code, and learned your first -- and possibly most important! -- programming function, print!

The homework will also be conducted in a Jupyter Notebook. (If you really get interested in programming, we can talk off-line about what tools and programming environments besides Jupyter Notebooks are good to learn and use.)

Let's dive in!

Many people are probably wary or intimidated by programming, but...

At it's most basic level, programming is just like using a calculator

All the arithmetic that you are used to is here:



In [3]:

( (9 + 10) * (3 + 4) ) / 2




Out[3]:

66.5



Remember in algebra how you defined variables and then wrote expressions with those variables? That's key to programming as well:



In [4]:

x = 9
y = 3
z = 2

a = x * y / z
print(a)




13.5



This is really all programming is -- writing mathematical and math-like expressions, and letting a computer do the work of evaluating such expressions.

Let's now talk a little more about...

Variables and types

Variable names. Variable names can contain alphanumeric characters a-z, A-Z, and _, but cannot start with a number.

As I just demonstrated, we 'assign' a value to a variable name with =.



In [5]:

x = 2



All values have a 'type'.



In [6]:

type(x)




Out[6]:

int




In [7]:

type(2)




Out[7]:

int



Note that the type is associated with the value, not the name.



In [8]:

x = 2.1
type(x)




Out[8]:

float



That is, x was initially an int but when we assigned it a new value, it became a float. (More on what a float is in a moment.)

If we try to use a variable that has not yet been defined we get a NameError.



In [9]:

x + q




---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-9-8f60a42a2a01> in <module>()
----> 1 x + q

NameError: name 'q' is not defined



Trying to understand your error messages is key to understanding why your program isn't working as expected!!

Googling your error message will usually lead to helpful information. In fact, if you can google, you can learn to program. Any question you have about programming has been asked and answered online 100 times before!

Real quick: in case you haven't noticed...if the last line of code in a cell is simply an expression -- and not an assignment -- jupyter notebook prints the result of that expression to the screen. Contrast this...:



In [10]:

x




Out[10]:

2.1



...with this...:



In [11]:

x = 9



(This is not a terribly important concept in programming or Python -- I just mention it to preempt possible confusion about the notebook's behavior.)

Fundamental types

Here, we'll just think of 'types' as the 'kinds of things' a programming language has. Python's fundamental types (some of which we've seen):



In [12]:

# integer
x = 1
type(x)




Out[12]:

int




In [13]:

# floating-point
x = 1.0
type(x)




Out[13]:

float



(In case it isn't clear # introduces a 'comment'. It's basically a way of telling the computer not to interpret anything after it as Python code. Instead, you (the programmer) write this or that comment to help explain the program, so someone reading it later can understand it.)

int is integer. A float (short for 'floating point' number) is not really simply a number with a decimal point, but it will suffice for now to think of them that way.

The next data type we are already familiar with from our logic lectures!



In [14]:

# boolean
x = True
y = False
type(x)




Out[14]:

bool



In a little I'll show how Python can calculate truth-values for complex statements. Later, I'll show how we use bool instances for conditionals (e.g., if x == True: print("x is True!"))

Finally, Python has a str type for strings, which you can basically think of as plain text.



In [15]:

# string
name = 'Russell Richie'
type(name)




Out[15]:

str



You can also put strings inside double-quotes. This is necessary if you want to put a single quote inside a string!



In [19]:

"Russell Richie's a grad student."




Out[19]:

"Russell Richie's a grad student."



Having different data types is important because they let you do different things. That is, different types afford different actions, or operations (which we'll cover in a minute). (Quickly, int and float let you do math-y things, bool logic-y things, and str text-y/language-y thing.)

Questions so far? We do have time to field some questions throughout!

Converting types (a.k.a. type casting)

What happens if we try to add the integer 1, and the string '1'? Before we execute this next line, any guesses what the output will be?



In [22]:

1 + '1'




---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-22-861a99da769e> in <module>()
----> 1 1 + '1'

TypeError: unsupported operand type(s) for +: 'int' and 'str'



Try to read that error message. Why did this fail?

So we need to be able to convert between types.



In [23]:

x = '1'
type(x)




Out[23]:

str




In [24]:

y = int(x) # y is x converted to an integer
type(y)




Out[24]:

int



So again, this doesn't work:



In [25]:

y + x




---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-25-3c9b764a1236> in <module>()
----> 1 y + x

TypeError: unsupported operand type(s) for +: 'int' and 'str'



But this does:



In [26]:

y + int(x)




Out[26]:

2



Operators and comparisons

I said before that different types have different operations available to them. We already saw a little bit how numerical types likes int and float have the usual arithmetic operations available to them, but to reiterate:

(Some) Mathematical operators



In [28]:

1 + 2




Out[28]:

3




In [29]:

1 - 2




Out[29]:

-1




In [30]:

3 * 4




Out[30]:

12




In [31]:

3 / 4




Out[31]:

0.75




In [32]:

# Power, aka exponentiation
2**3




Out[32]:

8




In [26]:

# Modulus. Returns the remainder when you divide one number by another
9 % 4




Out[26]:

1



Logical operators



In [33]:

1 == 2




Out[33]:

False



ATTENTION!

In Python, = is different from ==.

• = is for assigning a value to a variable!

• == is for computing equivalence between two values!

Observe:



In [34]:

1 = 2




File "<ipython-input-34-b922a9ca1612>", line 1
1 = 2
^
SyntaxError: can't assign to literal



Why does that fail?

These work, though!



In [35]:

1 != 2




Out[35]:

True




In [36]:

1 > 2




Out[36]:

False




In [37]:

1 >= 2




Out[37]:

False




In [38]:

1 < 2




Out[38]:

True




In [39]:

1 <= 2




Out[39]:

True



And we have some of the statement logic connectives that you all are familiar with!



In [40]:

(1 != 2) and (2 > 1)




Out[40]:

True




In [41]:

(1 == 2) or (1 != 2)




Out[41]:

True




In [42]:

print("x is",x)
not (x == 2)




x is 1

Out[42]:

True



Hopefully this reminds you of statement logic! We could also just define atomic letters with truth-values and compute the truth-value of complex propositions like this:



In [43]:

A = True
B = False
C = False

(A or B) and not (C)




Out[43]:

True




In [ ]:

#A->B
(not A) or B

#A<>B
(A and B) or (not A and not B)



There are no logical operators/connectives for the conditional or biconditional in base (ordinary) Python, but do you really need them? Why or why not?

String operators

You can concatenate strings:



In [48]:

first_name = 'Russell'
last_name  = 'Richie'
full_name  = first_name + ' ' + last_name # ' '  is a string with a space!
print(full_name)




Russell Richie



I'll go over more of the things you can do with strings later, in the "Objects" section below.

Questions?

Above we covered data types like bool, int, float, and str. Sometimes, you want to put a whole bunch of instances of such types into a single container. This is where we could use:

Lists

They are exactly what they sound like! A list is an ordered sequence of any combination of data types.

Lists are defined by square brackets [], with individual elements inside separated by commas.



In [51]:

the_numbers = [4, 8, 15, 16, 23, 42]
type(the_numbers)




Out[51]:

list



What do you do with lists?

For one thing, you often want to know how long a list is:



In [52]:

len(the_numbers)




Out[52]:

6



You also might want to get just certain values within the list. We can access values from lists using square brackets. This is called "indexing". Indexes start at 0 in Python.



In [53]:

the_numbers[0] # get the 0-th item from the list (intuitively, the 1st)




Out[53]:

4




In [54]:

the_numbers[3]




Out[54]:

16



So 0-based indexing means an index of n gets you the n+1th element.

If 0-based indexing seems weird, think of it like this:

Think of the indexes as 'fence posts' between the elements of the list.

Rest assured, this does become more intuitive as you use it.

Use [#:#] to get a range of elements.



In [55]:

the_numbers[2:5] # get everything between the 2 and 5 fenceposts




Out[55]:

[15, 16, 23]




In [56]:

the_numbers[:2] # get everything up to the 2 fencepost




Out[56]:

[4, 8]




In [57]:

the_numbers[2:] # get everything after the 2 fencepost




Out[57]:

[15, 16, 23, 42]



Indexing can be used to change values, too.



In [58]:

the_numbers[0] = 2
the_numbers




Out[58]:

[2, 8, 15, 16, 23, 42]



Note: indexing works similarly for strings.



In [59]:

print(name)




Russell Richie




In [60]:

name[0]




Out[60]:

'R'




In [61]:

name[:7]




Out[61]:

'Russell'



But you actually can't use indexing to change a value of a string! (For reasons we won't go into.)



In [62]:

name[0] = 'F'




---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-62-73973131ebc6> in <module>()
----> 1 name[0] = 'F'

TypeError: 'str' object does not support item assignment



The range function is useful for creating simple lists. Although it behaves like a list, it's technically not a list unless you convert it to one explicitly.



In [63]:

range(4)




Out[63]:

range(0, 4)




In [64]:

list( range(4) )




Out[64]:

[0, 1, 2, 3]



More generally, any type of data structure that we can "iterate" over can be turned into a list. (More on the notion of 'iteration' in a minute.)



In [66]:

r = 'russell'
print(len(r))
r = 'richie'
print(len(r))




7
6




In [65]:

list(name)




Out[65]:

['R', 'u', 's', 's', 'e', 'l', 'l', ' ', 'R', 'i', 'c', 'h', 'i', 'e']



Pick up here on Friday Feburary 24th!.

Operators on lists.



In [54]:

the_numbers + [1, 2] # concatenate two lists




Out[54]:

[2, 8, 15, 16, 23, 42, 1, 2]




In [55]:

n = 2
the_numbers * n # repeat a list n times




Out[55]:

[2, 8, 15, 16, 23, 42, 2, 8, 15, 16, 23, 42]



But notice that neither of these operations directly modified the_numbers!



In [56]:

the_numbers




Out[56]:

[2, 8, 15, 16, 23, 42]



If we want to add [1, 2] to the_numbers and keep the result in the_numbers, we could do this:



In [57]:

the_numbers = the_numbers + [1, 2]
the_numbers




Out[57]:

[2, 8, 15, 16, 23, 42, 1, 2]



Another very important thing you do with lists -- sometimes you want to know if something (say, an integer) is or is not in a particular list:



In [58]:

23 in the_numbers # like asking "(is) 23 in the_numbers"?




Out[58]:

True




In [59]:

23 not in the_numbers




Out[59]:

False



Modifying lists.

There are certain operations to modify lists 'in place'. Meaning, you can modify the list without having to reassign the list to the same or a new variable.



In [60]:

the_numbers.append(101)
the_numbers




Out[60]:

[2, 8, 15, 16, 23, 42, 1, 2, 101]




In [61]:

the_numbers.extend([120, 142, 179])
the_numbers




Out[61]:

[2, 8, 15, 16, 23, 42, 1, 2, 101, 120, 142, 179]



Those two operations are definitely very similar and easy to confuse. But observe:



In [62]:

the_numbers.append([1,2,3])
the_numbers




Out[62]:

[2, 8, 15, 16, 23, 42, 1, 2, 101, 120, 142, 179, [1, 2, 3]]




In [63]:

the_numbers.extend([1,2,3])
the_numbers




Out[63]:

[2, 8, 15, 16, 23, 42, 1, 2, 101, 120, 142, 179, [1, 2, 3], 1, 2, 3]



The key difference is that append simply takes its 'argument' and makes it the last element in the_numbers. extend takes the argument and adds each of its elements to the end of the_numbers.

You might be puzzled by the syntax of these operations: some_object.some_operation(some_inputs). Operations with this syntax are technically called methods, but for now you can think of them as functions just like len() or print().

Questions??

Functions

I've already referred to 'functions' without much explanation, when I showed, e.g., print() and len() and even .append(). The programming notion of 'function' more or less maps onto your everyday but also mathematical notions of 'function': functions do things, usually for some special purpose; functions map from inputs to outputs.

Commonly, functions are sequences of operations...

Quick: what technical term meaning 'sequence of operations' do we already know?

...sequences of operations that are called with parentheses, and the input to the function in parentheses.

As we've seen, Python provides many 'built-in' and useful functions. We already saw print(), len(), and range(). In addition:



In [64]:

the_numbers = [4, 8, 15, 16, 23, 42, 101, 103]
the_numbers




Out[64]:

[4, 8, 15, 16, 23, 42, 101, 103]




In [65]:

sum(the_numbers)




Out[65]:

312




In [66]:

min(the_numbers)




Out[66]:

4



That notation -- function_name(argument) -- should remind you of how functions appeared in math classes, as in f(x).

We can define our own functions with the def keyword (short for 'define').



In [67]:

def is_even(x):
# determines whether a number is an even number
return (x % 2) == 0

is_even(10)




Out[67]:

True



For the first three numbers in the_numbers, determine whether each is even, and store the result in a list.



In [68]:

is_even_list = [is_even(the_numbers[0]),
is_even(the_numbers[1]),
is_even(the_numbers[2])]

is_even_list




Out[68]:

[True, True, False]




In [69]:

def square_is_even(x):
# this function tells you whether the square of x is even!
square = x ** 2
return is_even(square)

print(square_is_even(3))
print(square_is_even(4))




False
True



Objects

We've seen the word 'object' pop up here and there. In Python, pretty much everything is an object. Every instance of some broader class is an object. That includes literals (like 1 or 'a' or ['a',2]), variables, and more. But for the purposes of this class, don't fret terribly much about the precise definition of 'object'...

What's important is this -- objects have values and functions (technically called methods) attached to them that are accessed with a . (dot). We already saw a couple instances of these functions with .append and .extend. Some other important ones:

For string objects:



In [70]:

name.endswith('ie')




Out[70]:

True




In [71]:

' '.join(['Russell','Richie'])




Out[71]:

'Russell Richie'




In [72]:

name.upper()




Out[72]:

'RUSSELL RICHIE'



For list objects:



In [73]:

the_numbers.index(23)




Out[73]:

4




In [74]:

the_numbers.reverse()
the_numbers




Out[74]:

[103, 101, 42, 23, 16, 15, 8, 4]




In [75]:

the_numbers.sort()
the_numbers




Out[75]:

[4, 8, 15, 16, 23, 42, 101, 103]



If you put your cursor after the . and press 'tab', you will see possible attributes and methods for an object. (And actually, pressing tab at many points will often give possible completions to a line.)



In [76]:

name.




File "<ipython-input-76-ee0590c242e9>", line 1
name.
^
SyntaxError: invalid syntax




In [77]:

name.capitalize?



The ? trick is actually super useful for looking up what something is or does without having to open a new tab and google it.

Control flow

In Python, whitespace -- spaces or tabs -- at the beginning of a line is significant. Some types of keywords (like if below) require that they be followed by (1) a colon :, and then (2) a group of lines indented with 4 spaces. The group of indented lines is called a block. For example, when we defined functions above, we deployed a block to indicate the content of the function.

Control flow is how we tell Python which block of code to execute.



In [78]:

first_and_last_name = name.split()
print(first_and_last_name)




['Russell', 'Richie']




In [79]:

first_name = first_and_last_name[0]
print(first_name)




Russell




In [80]:

if first_name == 'Russell':
print("That's a good name.")
else:
print("That name's okay, I guess...")




That's a good name.




In [81]:

first_name = 'Garrett'

if first_name == 'Russell':
print("That's a good name.")
else:
print("That name's okay, I guess...")




That name's okay, I guess...



If you screw up the indentation, Python will let you know.



In [82]:

x = 1
print(x)




File "<ipython-input-82-af59435001f7>", line 2
print(x)
^
IndentationError: unexpected indent




In [83]:

if x == 1:
print('yep')




File "<ipython-input-83-52e1942c0958>", line 2
print('yep')
^
IndentationError: expected an indented block



Much of the power of programming comes from...:

Loops

Like conditionals, loops can also be used in blocks. You use a loop when you want to do something over and over.

Let's say we want to print() every element in the list [0,1,2,3,4]:



In [84]:

for x in [0,1,2,3,4]:
print(x)




0
1
2
3
4



Equivalent to:



In [85]:

for x in range(5): # also equivalent to for x in list(range(5)):
print(x)




0
1
2
3
4



Let's break this down a little bit...

The for x in some_sequence: introduces a variable x which will contain a different value in some_sequence on every iteration of the loop. If some_sequence is [0,1,2,3,4], on the first run of the loop, x is 0, on the second run, x is 1, and so forth. This allows you to write a block of code just once, but run it for every element in some_sequence.

Let's try another loop:



In [86]:

the_numbers




Out[86]:

[4, 8, 15, 16, 23, 42, 101, 103]




In [87]:

for number in the_numbers:
numb_digits = len(str(number))
print("The number is", number, "which has", numb_digits, "digits")




The number is 4 which has 1 digits
The number is 8 which has 1 digits
The number is 15 which has 2 digits
The number is 16 which has 2 digits
The number is 23 which has 2 digits
The number is 42 which has 2 digits
The number is 101 which has 3 digits
The number is 103 which has 3 digits



Let's try combining a for loop and a conditional:



In [88]:

for number in the_numbers:
numb_digits = len(str(number))
if numb_digits in [1,3]:
print("The number is", number, "which has", numb_digits, "digits")




The number is 4 which has 1 digits
The number is 8 which has 1 digits
The number is 101 which has 3 digits
The number is 103 which has 3 digits



A slight tangent:

If it isn't clear, the reason the loop gives computing systems such power is that it lets the system process a large amount of information with a small amount of code/instructions if the process is repetitive in some way.

This is practically important for the "real world" (e.g., automating boring clerical work), but we could argue that it also has implications for cognitive science. That is, much of our cognition involves applying the same process to a sequence of elements. The problem set will exemplify this a little bit.

Questions??

Fin

There is, without a doubt, much, much more to Python. I haven't even covered all of what I would consider "the basics". But using just what I've given you, you really can go quite far. Certainly it will suffice for the problem set.

If you are interested in learning more programming, you could talk to me or Garrett. And/or, you might consider taking CSE classes! Many professors, graduate students, researchers, etc. elsewhere in the cognitive science community also use programming for cognitive science applications, so they might provide good learning experiences, as well.

Questions about anything we covered today?

Like much of the material in this course, to solidify your understanding of and facility with the above material, you probably need to practice. So, you might try some...

Optional exercises

For each exercise, there is a cell where you should try to fill in the function (delete the ...). The following cell acts as a test, run it to see if your function is correct.

Each exercise can be solved with just a few lines, using almost exclusively the Python functionality we covered above (when you perhaps need more stuff, I tell you what it is).

Write a function distance that takes two tuples, each containing two numbers (x, y). Compute the distance between them using the formula

$$d = \sqrt{(x_1 - x_2)^2 + (y_1 - y_2)^2}$$

As a hint I've imported the sqrt function from the math module.



In [89]:

from math import sqrt
sqrt(9)




Out[89]:

3.0




In [ ]:

def distance(a, b):
...




In [ ]:

assert distance((0, 0), (0, 0)) == 0
assert distance((0, 0), (1.5, 0)) == 1.5
assert distance((0, 0), (1, 1)) == sqrt(2)



Define a function max_of_three that takes three numbers and returns the largest of them. One way to solve this is by using if/else. But probably easier: remember the min() function? Given that that function exists, what other function probably exists?



In [ ]:

def max_of_three(a, b, c):
...




In [ ]:

assert max_of_three(1, 2, 3) == 3
assert max_of_three(3, 2, 1) == 3
assert max_of_three(1, -1, 1) == 1
assert max_of_three(100.25, 0, -1.5) == 100.25



Define a function is_palindrome that returns True for inputs that are the same backwards and forwards.

Might find the following trick helpful:



In [90]:

my_string = 'palindrome'
my_string[::-1]




Out[90]:

'emordnilap'




In [ ]:

def is_palindrome(word):
...




In [ ]:

assert is_palindrome('x')
assert is_palindrome('xx')
assert is_palindrome('')
assert not is_palindrome('abc')



A pangram is a sentence that contains every letter of the English alphabet. For example, The quick brown fox jumps over the lazy dog. Write a function is_pangram that checks whether a sentence is a pangram or not.



In [91]:

# This may be helpful.
from string import ascii_lowercase
ascii_lowercase




Out[91]:

'abcdefghijklmnopqrstuvwxyz'



There's many ways to solve this, but if you approach it like I did, you may also want to know that when you define a function, you can put return in more than one place. Which return gets called might then depend on the argument given to the function...



In [ ]:

def is_pangram(sentence):
...




In [ ]:

assert is_pangram('The quick brown fox jumps over the lazy dog.')
assert not is_pangram('The quick brown fox leaps over the lazy cat.')



Write a function factorial that computes the factorial of a number. Factorial is the ! operation. For example $4! = 4 \cdot 3 \cdot 2 \cdot 1 = 24$.

Include the special case that $0! = 1$.



In [ ]:

def factorial(x):
...




In [ ]:

assert factorial(0) == 1
assert factorial(1) == 1
assert factorial(2) == 2
assert factorial(3) == 6
assert factorial(4) == 24
assert factorial(5) == 120



Write a function digits_are_even that returns True if all the digits of a number are even.

You can call the function we already defined is_even inside your function.



In [ ]:

def digits_are_even(x):
...




In [ ]:

assert digits_are_even(22)
assert digits_are_even(2840)
assert not digits_are_even(24561)
assert not digits_are_even(52486)
assert digits_are_even(0)



Write a function char_freq that returns a dictionary containing the number of times each character appears.

We didn't talk about dictionaries, but they are basically collections of key: value pairs, like this:



In [92]:

word_freqs = {'the':100, 'a':90, 'Russell':10, 'is':50}
word_freqs




Out[92]:

{'Russell': 10, 'a': 90, 'is': 50, 'the': 100}




In [93]:

word_freqs['Russell']




Out[93]:

10




In [94]:

word_freqs['Richie'] = 9
word_freqs




Out[94]:

{'Richie': 9, 'Russell': 10, 'a': 90, 'is': 50, 'the': 100}



You can build an empty dictionary -- which you will fill in later in your program -- with:



In [96]:

new_dictionary = dict()
new_dictionary




Out[96]:

{}



Note that a function like char_freq already exists in Python, as collections.Counter, but see if you can recreate it.



In [ ]:

def char_freq(sentence):
...




In [ ]:

from collections import Counter

sentence = 'The quick brown fox jumps over the lazy dog.'.lower()
assert char_freq(sentence) == Counter(sentence)

sentence = 'aksjdhjsdbvjbfajsdhbajfgsodig'
assert char_freq(sentence) == Counter(sentence)