[Data, the Humanist's New Best Friend](index.ipynb)
*Class 03*

In this class you are expected to learn:

  1. Boolean logic
  2. Define functions
  3. Debug problems
  4. Get data from the keyboard

*Dumbledore's logic*

Comparisions and Logical Operators


In [ ]:
if someone is unicorn_killer:
    send_kids_to_catch(someone)
# Doesn't sound like a good idea

Numerical operators are kind of intuitive. String operators just a bit less. But for logical operators we need to know some rules in advance on how those are evaluated.

First we have comparisions, that are just evaluated to the true or false, in Python True or False

  • Greater than

In [2]:
4 > 7


Out[2]:
False
  • Greater than or equal to

In [3]:
10 >= 6


Out[3]:
True

In [4]:
2 => 3  # That '=>' doesn't exist in Python!


  File "<ipython-input-4-4d26c315c03c>", line 1
    2 => 3  # That '=>' doesn't exist in Python!
       ^
SyntaxError: invalid syntax
  • Less than

In [5]:
78 < 5


Out[5]:
False
  • Less than or equal to

In [6]:
5 < 78


Out[6]:
True
  • Equals

In [7]:
a = 3
a == 3  # We use '==' to check if two things are equal


Out[7]:
True

In [8]:
5 == "5"  # Comparing numbers with strings


Out[8]:
False
  • Distinct

In [9]:
23 != "23"


Out[9]:
True

In [10]:
5 != 5.0  # Automatic casting here!


Out[10]:
False

So once we have logical values, this is, values that are evaluated as true or false, we can start using the logical operators.

The basic rules are shown below. Other than that, it's just using precedence rules.

  • The and operator, basically makes the whole expression True if all the parts are also True

In [11]:
True and True


Out[11]:
True

In [12]:
True and False


Out[12]:
False

In [13]:
False and True


Out[13]:
False

In [14]:
False and False


Out[14]:
False
  • The or operator, on the other hand, makes the whole expression True if any of the parts is also True

In [15]:
True or True


Out[15]:
True

In [16]:
True or False


Out[16]:
True

In [17]:
False or True


Out[17]:
True

In [18]:
False or False


Out[18]:
False
  • The not operator gives the opposite logical value

In [19]:
not True


Out[19]:
False

In [20]:
not False


Out[20]:
True

And how for something a little bit more complicated, are you ready? Before hitting Shif-Enter, think about what the outcome will be.


In [ ]:
a = 2
b = 10
(a / b <= 5) and not (b ** a > b * b)

Activity

Write an expression that evaluates to `True` if the a number in the variable `x` is even, and `False` otherwise.
*Hint*: The operator modulo, `%`, gives the reminder of a division. So `5 % 2` returns `1`.


In [21]:
x = 27
# Write your expression here


Out[21]:
False

Errors

Curious about that SyntaxError a few examples above? Every time Python doesn't understand something, it shouts it out pretty clearly in the form of Exceptions and Errors.

There are many kinds of errors and exceptions, so you can have a clue on what is going on in your code. The difference between errors and exceptions is so subtle, that for our purposes they are all the same. That means that if you see something is not working, is your turn to fix it! That fixing process is what we call debugging.


*It's not a bug, it's a feature!*

Functions

You will eventually find yourself writing the same series of statements over and over (or cutting and pasting in your editor). Surely there is a cleaner way to do this? We want a way to group together sequence of statements that we frequently reuse.

In Python, we do this with a function. Here’s one now:


In [23]:
def my_function(a_parameter):
        b = a_parameter * 2
        print(b)

Once you've defined a function, you can call it or invoke it from the Python interpreter or from IPython in exactly the same way you'd call a built-in function like print.

So let’s use our function:


In [24]:
my_function(2)


4

In [25]:
my_function(7)


14

In [26]:
my_function(a_parameter=7)  # Call that uses the name of the argument


14

When we call my_function, the Python interpreter executes the statements that make up the function, in order.

Functions make code easy to reuse, and easy to read. More importantly they facilitate abstraction. The general format for defining a function is:

def function_name(p1,p2,p3,p4, ... ):
        statement 1
        statement 2
        ...
        statement m
  • function_name is... the name of the function, duh!
  • p1, p2, etc. are called the parameters, you can have as many as you like.

You tell Python which statements make up the body of the function by using indentation (our magic tabs!).


*$f(x)$*

Furthermore, you can call a function inside a function.


In [27]:
def my_2nd_function(param):
    my_function(param)
    my_function(param)
    
my_2nd_function(5)


10
10

Probably, the most useful thing about functions is that they can return values. To do so, we just need to add one or several return statements, but every time a function reaches a return statement, it returns whatever expression is accompanying and quits que function, leaving the rest of the statements without executing.


In [28]:
def another_function(a):
    b = a + 5
    return b
    c = b + 5
    return c

In [29]:
another_function(0)


Out[29]:
5

In a function definition, the keyword in the header is def, which is followed by the name of the function (following snake_case naming) and a list of parameters enclosed in parentheses. These parameters become variables inside the function, and exist only there. Whatever you define inside a function, is local to that function and therefore not accesible from the outside. Thats what we call scope.


In [30]:
variable = 20
def func1(a):
    value = a + 10

func1(10)
print(value)


---------------------------------------------------------------------------
NameError                                 Traceback (most recent call last)
<ipython-input-30-4527bd0c7321> in <module>()
      4 
      5 func1(10)
----> 6 print(value)

NameError: name 'value' is not defined

Instead, you want to use the return statement and compound new expressions. This is called composition, no surprise here. So you can even have functions that are called using another function invokation as a paremeter, and so on...


In [31]:
def mind_blowing(param):
    return "Mind, " + param

mind_blowing(mind_blowing(mind_blowing(mind_blowing(mind_blowing("blowing!")))))


Out[31]:
'Mind, Mind, Mind, Mind, Mind, blowing!'

*Careful! It's not that easy!*

The parameter list may be empty, or it may contain any number of parameters. In either case, the parentheses are required.

Paremeters can be optional, so they can have default values, in which case they must put in the end of the list of parameters.


In [32]:
def func_defaults(optional=5):
    return optional + 10

In [33]:
func_defaults(10)


Out[33]:
20

In [34]:
func_defaults()


Out[34]:
15

In [35]:
func_defaults() == func_defaults(5)


Out[35]:
True

Activity

Write a function, `grade(number)`, that receives an number between 0.0 and 100.0 and returns the proper grading following the [Smith College numerology](http://cs.smith.edu/~orourke/Grading.html). For values outside the range, just print "N/A".
For example, `grade(88.75)` returns `B+`.

Inputs

There is one (two in older versions of Python) built-in function in Python for getting keyboard input (we say built-in and we mean included in the Python standard library):


In [ ]:
n = input("Enter something: ")
print(n)

Activity

Why the next code doesn't work? Do the needed modifications to make it work.


In [ ]:
age = input("Please enter your age: ")
print("You were born in ", 2014 - age)

For the next class