# ---
# jupyter:
#   jupytext:
#     text_representation:
#       extension: .py
#       format_name: light
#       format_version: '1.5'
#       jupytext_version: 1.14.4
#   kernelspec:
#     display_name: Python 3
#     language: python
#     name: python3
# ---

# # Python
#
# [**Python**](https://www.python.org/) is an easy to learn but versatile and powerful programming language. Therefore it's perfectly suited for programming beginners. Python is friendly to the beginning programmer - it makes things that should be easy to express easy to express by taking care of some technical details for them - but it also gives the professional programmer many tools to build more complex systems in an elegant way.
#
# With an already extensive standard library and a vivid data science community, Python attracts more and more 	practitioners. Python is a multi-paradigm language, which means it allows you to use and mix different approaches to programming, including procedural, object-oriented, and functional programming. Don't worry, you will be introduced to these concepts (control flow, objects, functions...) soon.
#
# For many active users in scientific and academic circles as well as in for-profit and non-profit organisations Python is the favorite programming language. The ecosystem of libraries for numerical calculations, statistical methods, and data visualization make Python a powerful tool for data analytics.

# ### Python 2 or Python 3?
#
# Due to reasons beyond the scope of this course, there are two parallel major Python versions in use: Python 2 and Python 3. Python 3 is the current and improved version of Python. The differences between Python 2 and 3 are not huge, but big enough so that a program written for Python 2 needs some changes to run it with Python 3.
#
# All material in this course is build for **Python 3.7+**!.
#
# Let's quickly check which Python version is loaded:

import sys
sys.version

# ## Working with Documentation
#
# Being able to find and work with documentation is an essential skill for any programmer. Throughout this course there will be a number of exercises. Keep in mind that not only information from the course materials, but also all of the internet is available for you for reference. 
#
# For example, documentation for Python and its built-in libraries can be found at **https://docs.python.org/3/index.html**

# ## Python Introduction
#
# We will now go through some basic Python concepts. If you know any other programming language, many concepts will be familiar - try to notice the differences though.

# ### Comments
#
# It is good practice to write not only code, but also comments to explain your program where it is not obvious. Code comments will be ignored by the Python interpreter. To create a single line comment start the line with **`#`**.

# +
# This is a single line comment
# -

# ### Variables and Data Types
#
# It's easy to create a variable, that is, a name pointing to an object which can then be used by the program. Python is using **dynamic typing**, i.e. the variable type will not be stated explicitly (you may give a [hint](https://docs.python.org/3/library/typing.html#module-typing)). In general a variable can hold any object type. Here are some variables, holding some of the most basic data types:

n = 1  # a integer variable
s = 'python'  # a string variable
f = 12.34  # a float variable

print(type(n), type(s), type(f))

# You can directly perform arithmetic operations using variables.

a = 1
b = 2
c = a + b

print(a, b, c)

print(f'The sum of {a} and {b} is {c}.')

# In the cell above we used the `print` function to output a string. Because we started the string with `f'`, we can directly use the names of variabes in curly braces - they are automatically replaced with the values of the variables. 

# Python also uses **strong typing**. That means that the objects holding data have a clear type for which only the operations that make sense are allowed. For example, trying to add a string and an integer gives you an error in Python (it does not in some other languages):

# exercise: try adding variables containing different types here:


# ### Containers

# Containers are data types that can contain others. The most important ones are tuples, lists, and dictionaries.

# ### Tuples

# Tuples are sequences of objects. Creating a tuple is as simple as putting different comma-separated values:

numbers = (a, b, c)

print(numbers)

coordinates = (x, y, z) = (1.0, 0.5, 0.25)

print(coordinates)

# ### Lists
#
# [**Lists**](https://docs.python.org/3/tutorial/datastructures.html) are another container data type to store sequences of objects.

names = ['Tim', 'Elon', 'Jenny', 'Bill', 'Melanie', 'Melissa']  # a list of strings
ages = [19, 37, 40, 62, 31, 33]  # a list of integer numbers

sorted(ages)

sum(ages)

sorted(names)

', '.join(names) # join all list entries with a comma

avg_age = sum(ages) / len(ages)
print(f'The average age is {avg_age} years.')

# A list can contain lists.

[3, [1, 2.67, 3], 'python', ['a', 'b']]

# Lists and tuples seem quite similar, so what is the difference between them? The main difference is that tuples are **immutable** while lists are **mutable**. That means that after you have created a tuple, you cannot change it in place - you have to create a new tuple object if you want to make changes. Lists on the other hand can be edited - you can change an element, append or remove elements etc., all with the same list object.

# Change of an element within a list
test_list = [1, 3, 3]
print(test_list)
test_list[2] = '42'
print(test_list)

# ### Dictionaries
#
#  [**Dictionaries**](https://docs.python.org/3/tutorial/datastructures.html#dictionaries) are a very useful data type to store a mapping between some objects. For each (**_key_**) a (**_value_**) is assigned. Dictionary can be nested as well.

{'a': 1, 'b': 2}

d = {'python': 3.6, 62: 'Bill'}

d['python']

d[62]

{'a': {'b': 1, 'c': 3}}

# +
# Use this cell to try creating several variable types









# -

# ### Control flow
#
# Programming is all about giving the computer clear rules when to perform which calculation. To control the order (or the flow) statements are executed different statements are provided.
#
# #### Side note: Syntax
#
# In contrast to many other programming languages where the space character is ignored by the compiler or interpreter, spaces play an important role in the Python syntax.
#
# As we will see in the following examples, some code will be indented inside of control statements. The typical identation of 4 spaces is imported. Otherwise the code cannot be interpreted.
#
# The error message in this case will look something like this:
#
# ```python
#   File "<ipython-input-4-7de823a6cc3a>", line 2
#     ...<some code>
#         ^
# IndentationError: expected an indented block
# ```
#
# #### The for-Loop

for name in names:
    print(name)

# Using enumerate to add an index/counter
for idx, name in enumerate(names):
    print(f'{name} at position {idx}.')

# **Important:** Lists in Python always start with 0 as first index position!

# Using zip to aggregate entries from different lists
for age, name in zip(ages, names):
    print(f'{name} is {age} years old.')

# #### The if-Statement
#
# To test for logical conditions the **if ... else** statement can be used.
#
# **Important**: Please keep in mind that e.g. an empty list `[]`, an empty dictionary `{}`, the integer number `0`, or the special type `None` are also evaluated as `False`!

# +
color_one = 'green'
color_two = 'red'

is_same = color_one == color_two
# -

if color_one == color_two:
    print('The colors are equal.')
else:
    print('The colors are different.')

if not is_same:  # The actual comparison is saved in `is_same` and can be negated
    print('The colors are different.')


# #### Functions
#
# One of the most important lessons that a programmer learns is that you do not want to write down the same piece of code several times in different places. Not only are programmers naturally lazy and don't want to repeat themselves - what if you need to fix an error in that piece of code? What if you forget to fix it everywhere? Programming by copy & paste is almost always a bad idea. Therefore, many programming language concepts are all about defining a piece of code once and use it several times in different places. One of those concepts is a **function**. 
#
# A Python function enables you to write a piece of code that accepts arguments and returns values. If you need some program logic again and again, try to make a function out of it.

def compare_colors(c_1, c_2):
    if c_1 == c_2:
        print('The colors are equal.')
    else:
        print('The colors are different.')


compare_colors('green', 'red')

compare_colors('yellow', 'yellow')


def addition(n, m):
    return n + m


addition(12, 30)


def oldest_person(names, ages):
    max_age = 0
    max_name = ''
    
    for name, age in zip(names, ages): 
        if age > max_age:
            max_name = name
            max_age = age
    
    return max_name, max_age


name, age = oldest_person(names, ages)
print(f'The oldest person is {name} with and age of {age} years.')

# #### Special: List comprehensions
#
# A unique concept are [**list comprehensions**](https://docs.python.org/2/tutorial/datastructures.html#list-comprehensions). These can be used to construct lists in a very efficient way.
#
# An example is the best way to show how it works. From a list of the first `10000` positive integer numbers only the even numbers will be squared and collected in a new list.

my_list = list(range(10000))  # A list with all integer numbers from 0 to 9999

# +
# %%timeit -r 10  # Average over ten repetitions
new_list_1 = []

for item in my_list:
    if item % 2 == 0:  # modulo 2
        new_list_1.append(item * item)
# -

# %%timeit -r 10 # Average over ten repetitions
new_list_2 = [item * item for item in my_list if item % 2 == 0]


# ### Classes and Objects
#

# Object-oriented programming is an approach to programming that bundles together data and logic - you will see an example shortly. Object-oriented programming is everywhere in Python, and almost everything is an object.
#

class Contact:
    
    def __init__(self, first_name, last_name, street, city):
        self.first_name = first_name
        self.last_name = last_name
        self.street = street
        self.city = city
    
    def get_full_name(self):
        return f'{self.first_name} {self.last_name}'
    
    def get_address(self):
        address = f'{self.get_full_name()}\n{self.street}\n{self.city}'
        return address


# Above, we have defined a **class** called `Contact` - a blueprint for a new type of object, here for example to represent an entry in an address book. We have added three functions to the class - they are called **methods** because they are attached to an object now. Every method has `self` as the first argument, a variable that points to the object itself. 
#
# The `__init__` method is a special method called the **constructor** that defines how an object of type `Contact` is created and initialized.  

tim = Contact("Tim", "Cook", "1 Infinite Loop", "Cupertino, CA")

# We have now created a new object which is an **instance** of the class `Contact`, and set its attributes to the given values. We can now read these attributes as follows:

tim.last_name

# We have also defined two additional methods. They can be almost any kind function and contain any code, but a proper method does something with the object and its attributes. Here, it combines first name and last name and outputs the full name:

print(tim.get_full_name())

# The second method combines all attributes and outputs the full address. Note how it uses the first method so we do not have to write the piece of code that gives us the full name more than once.

print(tim.get_address())

# ---
# _This notebook is licensed under a [Creative Commons Attribution 4.0 International License (CC BY 4.0)](https://creativecommons.org/licenses/by/4.0/). Copyright © 2019 [Point 8 GmbH](https://point-8.de)_

StarcoderdataJupyterScriptsDedupFiltered