---
title: "(A very brief)<br/>Introduction to Python"
subtitle: "Lecture 02"
author: "Dr. Colin Rundel"
footer: "Sta 663 - Spring 2023"
format:
  revealjs:
    theme: slides.scss
    transition: fade
    slide-number: true
    self-contained: true
execute:
  echo: true
---

```{r setup, message=FALSE, warning=FALSE, include=FALSE}
options(
  width=80
)
```

```{r hooks, include=FALSE}
local({
  hook_old <- knitr::knit_hooks$get("error")  # save the old hook
  knitr::knit_hooks$set(error = function(x, options) {
    x = sub("## \n## Detailed traceback:\n.*$", "", x)
    x = sub("Error in py_call_impl\\(.*?\\)\\: ", "", x)
    hook_old(x, options)
  })
})
```


# Basic types

## Type system basics

Like R, Python is a dynamically typed language but the implementation details are very different as it makes extensive use of an object oriented class system for implementation (more on this later)

:::: {.columns .small}
::: {.column width='50%'}
```{python}
True
1
1.0
1+1j
"string"
```
:::

::: {.column width='50%'}
```{python}
type(True)
type(1)
type(1.0)
type(1+1j)
type("string")
```
:::
::::

::: {.aside}
Note - all of these types are *scalar* values.
:::

## Dynamic types

As just mentioned, Python is dynamically typed langiage so most basic operations will attempt to coerce object to a consistent type appropriate for the operation.

:::: {.columns .small}
::: {.column width='50%'}
**Boolean operations:**
```{python}
1 and True
0 or 1
not 0
not (0+0j)
not (0+1j)
```
:::

::: {.column width='50%' .fragment}
**Comparisons:**
```{python error=TRUE}
5. > 1
5. == 5
1 > True
(1+0j) == 1
"abc" < "ABC"
```
:::
::::

## Mathematical operations

:::: {.columns}
::: {.column width='50%'}
```{python}
1 + 5
1 + 5.
1 * 5.
True * 5
(1+0j) - (1+1j)
```
:::

::: {.column width='50%' .fragment}
```{python}
5 / 1.
5 / 2
5 // 2
5 % 2
7 ** 2
```
:::
::::

## Coercion errors

Python is not quite as liberal as R when it comes to type coercion,

::: {.small}
```{python error=TRUE}
"abc" > 5
"abc" + 5
```
:::
. . .

::: {.small}
```{python}
"abc" + str(5)
```
:::

. . .

::: {.small}
```{python error=TRUE}
"abc" ** 2
```
:::

. . .

::: {.small}
```{python error=TRUE}
"abc" * 3
```
:::

. . .

More on why this happens in a little bit...


## Casting

Explicit casting between types can be achieved using the types as functions, e.g. `int()`, `float()`, `bool()`, or `str()`.

:::: {.columns .small}
::: {.column width='50%'}
```{python error=TRUE}
float("0.5")
float(True)
int(1.1)
int("2")
int("2.1")
```
:::

::: {.column width='50%' .fragment}
```{python error=TRUE}
bool(0)
bool("hello")
str(3.14159)
str(True)
```
:::
::::

## Variable assignment

When using Python it is important to think of variable assignment as the process of attaching a name to an object (literal, data structure, etc.)

:::: {.columns}
::: {.column width='50%'}
```{python}
x = 100
x

x = "hello"
x

ß = 1 + 2 / 3
ß
```
:::

::: {.column width='50%' .fragment}
```{python}
a = b = 5
```{python}
a
b
```
:::
::::

. . .

::: {.aside}
Python variable names can be of any length, and must only contain letters, numbers and underscores. They may not begin with a number nor conflict with language keywords. Python 3 supports a subset of unicode for variable names.
:::

## string literals

Strings can be defined using a couple of different approaches,

::: {.small}
```{python}
'allows embedded "double" quotes'
"allows embedded 'single' quotes"
```
:::

. . .

strings can also be triple quoted, using single or double quotes, which allows the string to span multiple lines.

::: {.small}
```{python}
"""line one
line two
line three"""
```
:::

. . .

a `\` can also be used to span a long string over multiple lines without including the newline

::: {.small}
```{python}
"line one \
not line two \
not line three"
```
:::

## f strings

As of Python 3.6 you can use f strings for string interpolation formatting (as opposed to `%`-formatting and the `format()` method).

```{python}
x = [0,1,2,3,4]
f"{x[::2]}"
f'{x[0]}, {x[1]}, ...'
f"From {min(x)} to {max(x)}"
f"{x} has {len(x)} elements"
```

::: {.aside}
See [PEP 498](https://peps.python.org/pep-0498/)
:::


## raw strings

One other special type of string literal you will come across are raw strings (prefixed with `r`) - these are like regular strings except that `\` is treated as a literal character rather than an escape character.

:::: {.columns}
::: {.column width='50%'}
```{python}
print("ab\\cd")
print("ab\ncd")
print("ab\tcd")
```
:::

::: {.column width='50%'}
```{python}
print(r"ab\\cd")
print(r"ab\ncd")
print(r"ab\tcd")
```
:::
::::



## Special values

By default Python does not support missing values and non-finite floating point values are available but somewhat awkward to use. There is also a `None` type which is similar in spirit and functionality to `NULL` in R.

:::: {.columns .small}
::: {.column width='50%'}
```{python error=TRUE}
1/0
1./0
float("nan")
float("-inf")
```
:::

::: {.column width='50%' .fragment}
```{python}
5 > float("inf")
5 > float("-inf")
None
type(None)
```
:::
::::


::: {.aside}
We will not be using these values much currently, but they will be relevant when discussing pandas in a couple of weeks
:::


# Sequence types

## lists

Python lists are a *heterogenous*, *ordered*, *mutable* containers of objects (they behave very similarly to lists in R).

::: {.small}
```{python}
[0,1,1,0]
[0, True, "abc"]
[0, [1,2], [3,[4]]]
```
:::

. . .

::: {.small}
```{python}
x = [0,1,1,0]
type(x)

y = [0, True, "abc"]
type(y)
```
:::


::: {.aside}
More on the underlying data structure and its properties next time
:::

## Common operations

:::: {.columns}
::: {.column width='50%'}
```{python}
x = [0,1,1,0]

2 in x
2 not in x
x + [3,4,5]
x * 2
```
:::

::: {.column width='50%' .fragment}
```{python}
len(x)
max(x)
x.count(1)
x.count("1")
```
:::
::::

::: {.aside}
See [here](https://docs.python.org/3/library/stdtypes.html#common-sequence-operations) and [here](https://docs.python.org/3/library/stdtypes.html#mutable-sequence-types) for a more complete listing of functions and methods.
:::

## list subsetting 

Elements of a list can be accessed using the `[]` method, element position is indicated using 0-based indexing, and ranges of values can be specified using slices (`start:stop:step`).

::: {}
```{python}
x = [1,2,3,4,5,6,7,8,9]
```
:::

. . .

:::: {.columns}
::: {.column width='50%'}
```{python}
x[0]
x[3]
x[0:3]
```
:::

::: {.column width='50%' .fragment}
```{python}
x[3:]
x[-3:]
x[:3]
```
:::
::::

::: {.aside}
When using a slice the default values are `0:length(x):1`
:::

## slice w/ step

::: {}
```{python}
x = [1,2,3,4,5,6,7,8,9]
```
:::

. . .

:::: {.columns}
::: {.column width='50%'}
```{python}
x[0:5:2]
x[0:6:3]
x[0:len(x):2]
```
:::

::: {.column width='50%' .fragment}
```{python}
x[0::2]
x[::2]
x[::-1]
```
:::
::::

## Exercise 1

Come up with a slice that will subset the following list to obtain the elements requested:

```{python}
d = [0, 1, 2, 3, 4, 5, 6, 7, 8, 9]
```

* Select only the odd values in this list

* Select every 3rd value starting from the 2nd element.

* Select every other value, in reverse order, starting from the 9th element.

* Select the 3rd element, the 5th element, and the 10th element



## mutability

Since lists are mutable the stored values can be changed,

```{python}
x = [1,2,3,4,5]
```

:::: {.columns}
::: {.column width='50%'}
```{python}
x[0] = -1
x

del x[0]
x

x.append(7)
x
```
:::


::: {.column width='50%' .fragment}
```{python}
x.insert(3, -5)
x

x.pop()
x

x.clear()
x
```
:::
::::

## lists, assignment, and mutability

When assigning an object a name (`x = ...`) you do not necessarily end up with an entirely new object, see the example below where both `x` and `y` are names that are attached to the same underlying object in memory.

```{python}
x = [0,1,1,0]
y = x

x.append(2)
```

. . .

What are the values of `x` and `y` now?

. . .

```{python}
x
y
```

## lists, assignment, and mutability

To avoid this we need to make an explicit copy of the object pointed to by `x` and point to it with the name `y`.

```{python}
x = [0,1,1,0]
y = x.copy()

x.append(2)
```

. . .

What are the values of `x` and `y` now?

. . .

```{python}
x
y
```

::: {.aside}
More on `.copy()` and `.deepcopy()` methods later on in the course.
:::


## Value unpacking

lists (and other sequence types) can be unpacking into multiple variables when doing assignment,

:::: {.columns .small}
::: {.column width='50%'}
```{python}
x, y = [1,2]
x
y

x, y = [1, [2, 3]]
x
y
```
:::

::: {.column width='50%' .fragment}
```{python}
x, y = [[0,1], [2, 3]]
x
y

(x1,y1), (x2,y2) = [[0,1], [2, 3]]
x1
y1
x2
y2
```
:::
::::

## Extended unpacking


It is also possible to use extended unpacking via the `*` operator (in Python 3)

:::: {.columns}
::: {.column width='50%'}
```{python error=TRUE}
x, *y = [1,2,3]
x
y
```
:::

::: {.column width='50%' .fragment}
```{python error=TRUE}
*x, y = [1,2,3]
x
y
```
:::
::::

. . .

<br/>

If `*` is not used here, we get an error:

```{python error=TRUE}
x, y = [1,2,3]
```

## tuples

Python tuples are a *heterogenous*, *ordered*, *immutable* containers of values. 

They are nearly identical to lists except that their values cannot be changed - you will most often encounter them as a tool for packaging multiple objects when returning from a function.


```{python}
(1, 2, 3)
(1, True, "abc")
(1, (2,3))
(1, [2,3])
```

## tuples are immutable

```{python error=TRUE}
x = (1,2,3)
```

```{python error=TRUE}
x[2] = 5

del x[2]

x.clear()
```

## Casting sequences 

It is possible to cast between sequence types

```{python}
x = [1,2,3]
y = (3,2,1)
```

```{python}
tuple(x)
list(y)
```

```{python}
tuple(x) == x
list(tuple(x)) == x
```


## Ranges

These are the last common sequence type and are a bit special - ranges are a *homogenous*, *ordered*, *immutable* "containers" of **integers**.

:::: {.columns}
::: {.column width='50%'}
```{python}
range(10)
range(0,10)
range(0,10,2)
range(10,0,-1)
```
:::

::: {.column width='50%' .fragment}
```{python}
list(range(10))
list(range(0,10))
list(range(0,10,2))
list(range(10,0,-1))
```
:::
::::

. . .

What makes ranges special is that `range(1000000)` does not store 1 million integers in memory but rather just three 3$^*$.


## Strings as sequences

In most of the ways that count we can think about Python strings as being ordered, immutable, containers of unicode characters and so much of the functionality we just saw can be applied to them.

::: {.small}
```{python}
x = "abc"
```
:::

:::: {.columns .small}
::: {.column width='50%'}
```{python}
x[0]
x[-1]
x[2:]
x[::-1]
```
:::

::: {.column width='50%' .fragment}
```{python}
len(x)
"a" in x
"bc" in x
x[0] + x[2] 
```
:::
::::

. . .

::: {.small}
```{python error=TRUE}
x[2] = "c"
```
:::

## String Methods

Because string processing is a common and important programming task, the class implements a number of specific methods for these tasks. Review the page linked on the previous slide for help.

```{python}
x = "Hello world! 1234"
```

. . .

:::: {.columns}
::: {.column width='50%'}
```{python}
x.find("!")
x.isalnum()
x.isascii()
x.lower()
```
:::

::: {.column width='50%' .fragment}
```{python}
x.swapcase()
x.title()
x.split(" ")
"|".join(x.split(" "))
```
:::
::::


::: {.aside}
Find a more complete list of functions  [here](https://docs.python.org/3/library/stdtypes.html#string-methods)
:::

## Exercise 2

String processing - take the string given below and apply the necessary methods to create the target string.

**Source:**
```{python eval=FALSE}
"the quick  Brown   fox Jumped  over   a Lazy  dog"
```

**Target:**
```{python eval=FALSE}
"The quick brown fox jumped over a lazy dog."
```

::: {.aside}
Hardcoding w/ magic numbers is perfectly acceptable here.
:::


## Set and Mapping types

We will discuss sets (`set`) and dictionaries (`dict`) in more detail next week.

Specifically we will discuss the underlying data structure behind these types (as well as lists and tuples) and when it is most appropriate to use each.
