5 * 4

3 ** 4    # square

(2 + 8) // 3    # integer division

(2 + 8) / 3

56.12 // 12.34    # result is float!

"""
Explanation: <a href="https://colab.research.google.com/github/OSGeoLabBp/tutorials/blob/master/english/python/python_in_a_nutshell.ipynb" target="_parent"><img src="https://colab.research.google.com/assets/colab-badge.svg" alt="Open In Colab"/></a>
Python in a Nutshell
Introduction
Python is a widespread and popular script language, which is capable of supporting object oriented programming, and that has several extending modules, for example GDAL, OGR, numpy, OpenCV, open3d, etc. While Python run time environment is available on different operating systems (e.g. Linux, OS X, Android, Windows), there are many end user programs which use Python as a script language to extend their functionalities (e.g. GIMP, QGIS, Apache, PostgreSQL, GRASS, ...). For those programs, the Python interpreter compiles the source code into the so-called byte code, capable to run the programs quickly, faster than some other script languages.
The Python 2 version reached the end of its lifetime, having its development closed in 2020. The Version 3 development started in 2008, and the two versions are incompatibile. Nowadays, everybody uses the new version for new projects, but there are many projects that have not been upgraded to the new version yet. The examples of this document were tested in Python 3, but probably they also work in Python 2.7.
Depending on the operating system, there are different installation options. For GIS users on Windows, the OSGeo4W is the optimal choice, having the GIS programs Python installed with many extension modules. Python is available in the OSGeo4W Shell window!
Did you know?
The language’s name isn’t about snakes, but about the popular British comedy troupe Monty Python.
If you are interested you can find more interesting facts about python at the following link.
Basics
In Jupyter notebook, it is possible to execute Python commands in code blocks. The backgroud of a code block is grey, having a triangle at the top left corner. After clicking on the triangle to execute the code block, the results are displayed below the code block. Comments start with '#' (hash mark).
Simple basic matemathical operations:
End of explanation
"""


a = 5 * 4
l = True    # Bool value
c = None    # special undefined value
s = "Hello world"   # string
print(a, l, c, s)

"""
Explanation: The results of the expressions can be stored in variables:
End of explanation
"""


b = '16'  # a string variable
a + b     # 'a' got its value in the previous code block

"""
Explanation: There is no automatic type conversion for variables and constants in Python (most of the script languages apply automatic type conversion, e.g. JavaScript, awk, Tcl). Therefore, the variable type is not explicitly given (no declaration), being set at the first assignment. We can query the type of a variable using type function.
End of explanation
"""


type(a)

type(b)

type(b) is str

a + int(b)  # explicit type conversion helps

a * b   # repeat string 'b' 'a' times

s[0]  # first character of a string, the value of s is 'Hello world'

s[1:3]  # second and third characters

s[:5]   # first five characters

s[6:]   # from the sixth character to the end

s[-1]   # last character

s[-5:]  # last five characters

len(s)    # length of string

s[:5] + ' Dolly'   # concatenation of strings

"""
Explanation: In the code block above, we tried to add an integer and a string value, an operation that is not supported.
End of explanation
"""


import math
math.sin(1)

print(f'{math.sin(1):6.4f}')  # formatted output

"""
Explanation: Mathematical (trigonometrical) functions are in an external module. Given this, it is necessary to use the import math module.
End of explanation
"""


from math import cos
cos(1)

"""
Explanation: Modules can be imported two ways. The import module_name will import everything from the module. It is possible to refer to a member of the module using module_name_member_name. The other method is used only when some specified memebers are imported, using from module_name import member_name form. In this case case, it is not necessary to put the module name in front of the member name, being careful to avoid name collision. In the second form, you specify a comma separated list of member names.
End of explanation
"""


help(math)

"""
Explanation: The modules usually have documentation where the available functions can be seen. If you would like to check these, use help(module_name)!
End of explanation
"""


l0 = []       # create an empty list
l1 = list()   # create an empty list
l2 = ['apple', 5, 4] # differenc data types in the same list
print(l2[0])        # access memebers by index
print(l2[1:])       # index ranges similar to strings
print(len(l2))
l2[0] = 3           # lists are mutable
print(l2)
l2.append(5)        # extending list
print(l2)
l2[0:3] = [1]       # replace a range of list with another list
print(l2)
del l2[0]           # delete list item
print(l2)
l3 = [[1, 2], [6, 4]] # list of lists
print(l3[0], l3[0][1])
l4 = l3 + [3, 7]    # concatenating lists
print(l4)
l4 = l3 + [[3, 7]]
print(l4)

"""
Explanation: More information about modules also can be found on the internet. For example, the math module's functions can be seen on the following URL: https://docs.python.org/3/library/math.html. Remember, if you are not sure about something, Google is your friend! 
Python data structures
The base data types of Python can be divided into two groups, mutable and unmutable.
Mutable objects: data can be partially change in place.
list, set, dictionary

Immutable object: the stored data cannot be changed.
bool, int, float, string, tuple
The usage of simple variable types (e.g. bool, int, float) is similar to all other programming languages. We'll discuss only compound types (e.g. list, tuple, dictionary, set). From Python3, even simple data types are represented by objects.
Lists
Lists are ordered and mutable Python container. Lists can contain elements of different types, even other lists. The items are indexed starting from zero.
End of explanation
"""


help(list)

"""
Explanation: You can use help(list) to get more information about lists.
End of explanation
"""


t = (65, 6.35)    # create a new tuple, brackets are not obligatory -> t = 65, 6.35
print(t[0])       # indexing
u = tuple()       # empty tuple
v = 2,            # comma is neccessary to creat a list of one item
print(v)

"""
Explanation: There are three operators related to lists:

+ concatenate lists
* repeat the list (similar to strings)
in is value in the list

Open a new code block and try the operators above. Studying Lists may help in trying some other methods of list objects (e.g. pop, sort, reverse)
Tuples
List like immutable ordered data type.
End of explanation
"""


l = [2, 6, -3, 4]
l2 = [x**2 for x in l]    # square of all mebers of the list into a new list
print(l2)
l3 = (i**0.5 for i in l if i > 0) # square root of positive members of list
print(l3)
print(tuple(l3))

"""
Explanation: List comprehesion
List comprehension is an effective tool to apply the same operation for all elements of the list/tuple.
End of explanation
"""


from math import sin
l4 = map(sin, l)
print(l4)
print(tuple(l4))
print([a for a in l if a % 2])    # odd numbers from the list

"""
Explanation: The map function is similar, but a function can be applied for each member of the list.
End of explanation
"""


a = "abcabdseacbfds"
s = set(a)            # create a set repeated values stored once
print(s)
t = set((1, 3, 2, 4, 3, 2, 5, 1))
print(t)
e = set()             # empty set
u = set(['apple', 'plum', 'peach', 'apple'])
print(u)

"""
Explanation: Sets
Sets are mutable unordered data types. It is simply possible to create a set from a string or a list.
End of explanation
"""


a = set('abcdefgh')
b = set('fghijklm')
print(a - b)          # difference of sets
print(a | b)          # union of sets
print(a & b)          # intersection of sets
print(a ^ b)          # symmetrical difference

"""
Explanation: There are different operators for sets, including difference, union, intersection and symmetrical difference.
End of explanation
"""


dic = {}            # empty dictionary
dic['first'] = 4    # adding new key and value to the list
dic[5] = 123
print(dic)
dic['first'] = 'apple'  # new value for a key
print(dic)
print('first' in dic)   # is the key in dic?
d = {'b': 1, 12: 4, 'lista': [1, 5, 8]} # initialization
t = {(1,1): 2, (1,2): 4, (2,1): -1, (2,2): 6} # keys are tuples
print(t[1,1])

"""
Explanation: Dictionaries
Dictionaries are mutable unordered date types. Each member of a dictionary has a key value, which can be used as an index, that can be a string or a number. The members of a dictionary can be lists or dictionaries.
End of explanation
"""


txt = "Hello world!"
#txt[0] ='h'   # a part of a string cannot be changed!

print(id(txt))
txt = 'h' + txt[1:] # here we create a new object using an existing object, we didn't change its value
print(id(txt))      # the two ids are different, the memory allocated for the old str will be freed by grabage collection
print(txt)

t = (2, 4, 6)
#t.append(8)     # tuple cannot be extended

#t = t + (8)     # upps it should work

print(id(t))
t = t + (8,)    # (8) is an integer for the Python
print(t)
print(id(t))

"""
Explanation: Some restrictions for immutable objects:
End of explanation
"""


""" My first program
    the sum of the firs 100 numbers
"""
s = 0
for i in range(1, 101):
  s += i
print(s)

"""
Explanation: Python programs
Interactive use of Python is good for simple tasks or for trying different things. In productive use, Python codes are written into files (modules) and are run as a single command. A complex task is usually divided into smaller code blocks (function or objects), connected by the use of loops and conditional expressions. Before we start to write programs, it is important to highlight a speciality of the syntax of the Python: the hierarchy of code blocks are marked by the number of spaces at the beginning of the program lines, forcing the users to write more readable programs. Usually four spaces are used to indent code blocks. In case of complex programs, an Integrated Development Environment (IDE) is very useful. The simplest IDE is IDLE which is written in pure Python. There are several open source IDEs (e.g. Spyder, Eric) and propriatery ones. The minimal requirements to develop Python programs are:

Install Python and the neccesarry modules on your machine (there are lots of installation guides for different operating systems)
Install a text editor with Python syntax highlighting (for example Notepad++)

Let's create our first program whose objective is to calculate the sum of the first 100 integer numbers (loop with counter). """ marks the start and the end of a multiline comment.
End of explanation
"""


def f(n):
  """ factorial calculation using while loop """
  w = 1
  while n > 0:
    w *= n
    n -= 1
  return w

"""
Explanation: The first three lines of this program are comments. The colon (:) at the end of the fifth line marks the start of a new block, having the following indented lines belonging to this block (in our program, it is a single line block). The range function creates an iterator between the to parameters (first inclusive and last exculsive, that's why we write 101 to finish at 100). The s variable is intialized by zero, and the integer values are added in the loop. If you copy the code above into a text file (e.g. first.py), you can run it from the command line:
python first.py
Python functions
Python functions are flexible building blocks of a program. Functions have input parameters and can return a single or compound data.
We'll give more solutions to calculate the value of nth the factorial.
n! = 1 * 2 * 3 * ... * n
In the first solution we'll use the while loop.
End of explanation
"""


n = 6
print(n, '!:', f(n))
print(f'15!: {f(15)}')

"""
Explanation: Let's try our function.
End of explanation
"""


def f1(n):
  """ factorial calculation using for loop """
  w = 1
  for i in range(1, n+1):     # range(1, n) gives a serie from 1 to n-1!
    w *= i
  return w

"""
Explanation: In the second version of factorial, we'll use the for loop.
End of explanation
"""


n = 6
print(f'{n}!: {f1(n)}')
print(f'15!: {f1(15)}')

"""
Explanation: Let's try it too.
End of explanation
"""


def f2(n):
  """ recursive factorial calculation """
  if n <= 1:              # stop criteria for recursion
    return 1
  return n * f2(n-1)      # the function calls itself, it is called recursive function

"""
Explanation: We can use the recursive formula of factorials.
n! = n * (n-1)!
End of explanation
"""


print(f'6!: {f2(6)}')

"""
Explanation: Let's try it.
End of explanation
"""


import numpy as np
def f3(n):
  """ factorial calculation using built in functions """
  return np.prod(range(1, n+1))

"""
Explanation: Finally, let's make a function without loop or recursion using Python built in functions.
End of explanation
"""


f3(6)

"""
Explanation: Let's try it.
End of explanation
"""


def quadratic(x, a0, a1=1, a2=0):
  """ claculate the value of a quadratic function """
  return a0 + a1 * x + a2 * x**2

# valid usages
print(quadratic(2.5, 3))              # a1=1 and a2=0
print(quadratic(4.1, 1.5, 3.3))       # a2=0
print(quadratic(-2.3, 5.1, -2, 0.56))

"""
Explanation: Function parameters
A function can have optional parameters with default values. The following function has two obligatory parameters (x, a0) and two optional ones (a1, a2). Optional parameters have to be at the end of the parameter list.
End of explanation
"""


print(quadratic(a2=2, a1=4, a0=1, x=3))
print(quadratic(3, 5, a2=4))              # x=3, a0=5, a1=1

"""
Explanation: The order of the function parameters are given by the function definition. You can change oder when you call the function.
End of explanation
"""


class Point2D(object):
  """ class for 2D points """
  def __init__(self, east = 0, north = 0):    # it is the constructor
    """ initialize point

        :param east: first coordinate
        :param north: second coordinate
    """
    self.east = east              # member variable
    self.north = north

  def abs(self):
    """ distance from the origin
        :returns: distance
    """
    return (self.east**2 + self.north**2)**0.5

  def __str__(self):
    """ Convert point to string for prining

        :returns: string with coordinates
    """
    return "{:.3f}; {:.3f}".format(self.east, self.north)

  def __add__(self, p):
    """ sum of two vectors
        :param p: point to add
        :returns: sum as a Point2D
    """
    return Point2D(self.east + p.east, self.north + p.north)

"""
Explanation: Object Oriented Programming (OOP)
In Python version 3.x, all variables are objects (an instance of a class). Now, we'll create custom classes. In this example, we'll create a class for 2D points.
End of explanation
"""


p1 = Point2D()  # origin
p2 = Point2D(10,8)
p3 = Point2D(-1, 4)
print(p1.east, p1.north)
print(p1)
print(p2)
print(p2.abs())
print(p2.__doc__)
print('p1 + p2: ', p2 + p3)     # p1 + p2 -> p1.__add__(p2)

"""
Explanation: Let's create an istance of the point class.
End of explanation
"""


