TDDE44 Programmering, grundkurs¶

Föreläsning 4.1¶

Johan Falkenjack, johan.falkenjack@liu.se¶

Föreläsningsöversikt¶

  • Kort om laboration 4
  • Python i verkligheten
    • paket
    • moduler, namespaces
    • virtuella miljöer
    • disposition
    • defaultvärden för funktionsargument
  • Bryta ut funktioner

Laboration 4¶

  • Tillämpning av det mesta ni lärt er hittills
    • skriva funktioner
    • bearbeta listor/dictionaries
    • läsa data från fil
    • strukturera kod
  • Rita diagram med matplotlib
  • Övning i att bryta ut funktioner och strukturera kod (Del 2)

Python i verkligheten¶

Python i verkligheten¶

  • moduler, namnrymder och paket
  • paket
  • virtuella miljöer
  • moduler, namespaces
  • disposition: struktur på kod, returnera och använd. mönster för skript
  • defaultvärden för argument

Namnrymder¶

Namnrymd (eng. Namespace)

namespaces.svg
  • Namnrymd: En samling namn (variabler, funktioner, etc.) som alla måste vara unika.
  • Built-ins
    • Inbyggda funktioner och variabler, t.ex. print och len.
    • Tillgängliga överallt i all Python-kod.
  • Globala namnrymden
    • Alla namn som finns i den aktuella modulen, dvs. i den aktuella kodfilen.
    • Dvs. alla namn vi skapat på toppnivå i modulen (dvs. utanför funktioner).
  • Lokala namnrymden
    • Alla namn som finns inne i en funktion när den exekveras.
    • Dvs. alla parametrar och alla variabler som tilldelats inne i funktionen.
  • På toppnivå i en modul gäller att globala och lokala namnrymden är samma.

Inspektera namnrymder¶

  • Vi kan inspektera globala och lokala namnrymderna med funktionerna globals resp. locals.
    • Båda funktionerna returnerar en dict där nycklarna är strängrepresentationer av de namn som finns i namnrymden och värdena är de värden som namnen refererar till.
  • På toppnivå i en modul (dvs utanför en funktionskropp) är den lokala och den globala namnrymden samma.
    • Dvs om vi anropar locals från toppnivå i modulen (eller i interaktivt läge) får vi alltså samma dict som om vi anropat globals.

Exempel: globals och locals på toppnivå¶

  • Notera att de blir samma:
>>> locals()
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>}
>>> globals()
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>}

Exempel: globals och locals i en funktion¶

  • Notera att de blir olika:
>>> def my_fun(param):
...     print(f"{locals()=}")
...     print(f"{globals()=}")
...
>>> my_fun("arg")
locals()={'param': 'arg'}
globals()={'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 'my_fun': <function my_fun at 0x7fd0f6d9f380>}

Exempel: globals och locals i en funktion igen¶

>>> def my_fun(param):
...     local_variable = "a local string"
...     print(f"{globals()=}")
...     print(f"{locals()=}")
...
>>> my_fun("arg")
globals()={'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 'my_fun': <function my_fun at 0x7fd0f6d9f380>}
locals()={'param': 'arg', 'local_variable': 'a local string'}

Import av modul

namespaces_import.svg
  • När vi importerar en modul med en vanlig import-sats så binder vi dess namnrymd till ett prefix i den lokala namnrymden.
    • (Ett prefix är bara en variabel vars värde är en sådan namnrymd.)
  • import random binder namnrymden random till prefixet random.
    • Vi kan nu komma åt namnrymden med hjälp av prefixet, t.ex.
In [1]:
import random
print(random.randint(1, 10))
8
  • Testa att köra print(globals()) före och efter import-satsen.

Exempel: Efter import i en funktion¶

>>> def my_fun(param):
...     import math
...     print(f"{locals()=}")
...     print(f"{globals()=}")
...
>>> my_fun("arg")
locals()={'param': 'arg', 'math': <module 'math' from '/home/johsj47/miniconda3/envs/py3_11/lib/python3.11/lib-dynload/math.cpython-311-x86_64-linux-gnu.so'>}
globals()={'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 'my_fun': <function my_fun at 0x7fd0f6d9f560>}

Paket

paket.svg
  • Paket är ett sätt att strukturera större namnrymder hierarkiskt.
  • Paket består av moduler och underpaket (som i sin tur kan bestå av moduler och underpaket, osv.)
    • Vi kan jämföra ett paket med en katalogstruktur.
  • Ofta används paket också synonymt med verktyg som skapats av andra Python-utvecklare och som utökar Python med ny funktionalitet.
    • Man kan jämföra dessa med expansioner eller DLCs till spel (fast oftast är de gratis och skapade av tredjepartsutvecklare, så det är kanske mer korrekt att jämföra dem med moddar).
    • Paketet requests som ni installerade i Lektion 3 är ett sådant paket.
    • På PyPI (Python Package Index) kan man hitta de flesta sådana paket som kan installeras med pip.

Paket för Python¶

  • Det finns många fritt tillgängliga paket att använda med Python (se t.ex. PyPI - the Python Package Index).
  • Finns ingen anledning att återuppfinna hjulet när det finns färdiga hjul att installera och använda.
    • (I riktiga projekt, dvs. Det finns självklart goda pedagogiska skäl att återuppfinna alla möjliga olika hjul i en kurs som denna.)
  • Exempel
    • Requests (hämta data från webben). https://requests.readthedocs.io/en/master/
    • NumPy (matrisberäkningar m.m.). https://numpy.org/
    • TensorFlow (maskininlärning). https://www.tensorflow.org/
    • Pandas (databearbetning). https://pandas.pydata.org/
    • Pillow (bildbehandling). https://python-pillow.org/
    • Flask (webbserver). https://palletsprojects.com/p/flask/
    • Beautiful Soup (extrahera data från webbsidor). https://www.crummy.com/software/BeautifulSoup/
    • OpenCV (bildbehandling). https://docs.opencv.org/master/index.html
  • När de väl är installerade är det ingen skillnad mellan inbyggda moduler och installerade paket från programmerarens perspektiv.

Installera paket (repetition)¶

  • Enkelt att installera paket:
    • $ python3 -m pip install <namn på paket>
    • eller
    • $ pip3 install <namn på paket>
    • (Detta bör ni ha gjort med requests under Lektion 3.)

Alias och underpaket

namespaces_alias.svg
  • När vi importerar kan vi använda as för att skapa ett lokalt alias, dvs ett alternativt prefix.
    • Ett sådant alias kan vara vad som helst, men det finns ofta starkt etablerade konventioner.
  • När vi skapar ett alias kan vi använda punktnotation för att importera bara ett specifikt underpaket:
import matplotlib.pyplot as plt

Några standardalias man kan stöta på¶

  • Inbyggda moduler
MODULE                  ALIAS       IMPORT STATEMENT
datetime                dt          import datetime as dt
multiprocessing         mp          import multiprocessing as mp
  • Externa paket
NumPy                   np          import numpy as np
SciPy                   sp          import scipy as sp
Pandas                  pd          import pandas as pd
Seaborn                 sns         import seaborn as sns 
TensorFlow              tf          import tensorflow as tf
Tkinter                 tk          import tkinter as tk

Importera till lokala namnrymden

namespaces_from.svg
  • Vi kan importera enskilda namn till den lokala namnrymden med hjälp av nyckelordet from, t.ex:
from math import sqrt
  • Vi behöver inget prefix, men varning för namnkonflikter.
In [ ]:
# import the randint function from the random module to the local namespace
from random import randint, choice

# import the plot function from the matplotlib.pyplot module to the local namespace
from matplotlib.pyplot import plot

print(f"{randint(0, 100)=}")
print(f"{choice(['a', 'b', 'c'])=}")


# anrop till funktionen plot() från modulen matplotlib.pyplot
print(f"{plot([0, 1, 2, 3])=}")

Varning!!¶

  • Använd inte konstruktionen
from modulename import *
  • Stor risk för namnkonflikter.
  • Importerade namn kan överskugga (eng. shadow) inbyggda och globala namn och orsaka svårhittade buggar.
  • Exempel:
>>> locals()
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>}
>>> from math import *
>>> locals()
{'__name__': '__main__', '__doc__': None, '__package__': None, '__loader__': <class '_frozen_importlib.BuiltinImporter'>, '__spec__': None, '__annotations__': {}, '__builtins__': <module 'builtins' (built-in)>, 'acos': <built-in function acos>, 'acosh': <built-in function acosh>, 'asin': <built-in function asin>, 'asinh': <built-in function asinh>, 'atan': <built-in function atan>, 'atan2': <built-in function atan2>, 'atanh': <built-in function atanh>, 'cbrt': <built-in function cbrt>, 'ceil': <built-in function ceil>, 'copysign': <built-in function copysign>, 'cos': <built-in function cos>, 'cosh': <built-in function cosh>, 'degrees': <built-in function degrees>, 'dist': <built-in function dist>, 'erf': <built-in function erf>, 'erfc': <built-in function erfc>, 'exp': <built-in function exp>, 'exp2': <built-in function exp2>, 'expm1': <built-in function expm1>, 'fabs': <built-in function fabs>, 'factorial': <built-in function factorial>, 'floor': <built-in function floor>, 'fmod': <built-in function fmod>, 'frexp': <built-in function frexp>, 'fsum': <built-in function fsum>, 'gamma': <built-in function gamma>, 'gcd': <built-in function gcd>, 'hypot': <built-in function hypot>, 'isclose': <built-in function isclose>, 'isfinite': <built-in function isfinite>, 'isinf': <built-in function isinf>, 'isnan': <built-in function isnan>, 'isqrt': <built-in function isqrt>, 'lcm': <built-in function lcm>, 'ldexp': <built-in function ldexp>, 'lgamma': <built-in function lgamma>, 'log': <built-in function log>, 'log1p': <built-in function log1p>, 'log10': <built-in function log10>, 'log2': <built-in function log2>, 'modf': <built-in function modf>, 'pow': <built-in function pow>, 'radians': <built-in function radians>, 'remainder': <built-in function remainder>, 'sin': <built-in function sin>, 'sinh': <built-in function sinh>, 'sqrt': <built-in function sqrt>, 'tan': <built-in function tan>, 'tanh': <built-in function tanh>, 'trunc': <built-in function trunc>, 'prod': <built-in function prod>, 'perm': <built-in function perm>, 'comb': <built-in function comb>, 'nextafter': <built-in function nextafter>, 'ulp': <built-in function ulp>, 'pi': 3.141592653589793, 'e': 2.718281828459045, 'tau': 6.283185307179586, 'inf': inf, 'nan': nan}

Använda pythonfil som modul¶

  • Filer med pythonkod är moduler
  • I filen uppg1.py:
def load_csv(filename):
    # satser i funktionen
  • Import och användning av uppg1.py som modul från annan pythonfil i samma katalog
import uppg1
uppg1.load_csv("data.csv")

Vad händer när man importerar en modul?¶

  • Vid import körs alla rader från filen som importeras.
  • Eventuella problem
    • vi vill att viss kod körs när vi använder filen som ett skript
    • vi vill inte att skript-koden körs när vi importerar filen som en modul
  • Exempel
    • Filen pokedex.py innehåller kod som kan användas för ta fram information om en Pokémon via PokeAPI
    • När pokedex.py körs kommer den att titta på argumenten som skriptet fått
    • Vi skriver ett annat program som ska använda sig av Pokémon-data, vi vill därför använda funktionerna i pokedex.py, men vill inte att pokedex-skriptet körs.

Den nuvarande namnrymnden¶

  • Speciella variabeln __name__ innehåller namnet på namnrymden för koden som körs
  • Namnrymden för ett skript (huvudprogram) som körs är "__main__"
  • Namnrymden för en kod i en modul är namnet på modulen

  • Exempelanvändning: kör inte viss kod när en fil importeras:
# om vi kör filen som ett skript, kör programmet
# annars utvärderas endast funktionerna m.m. i filen
if __name__ == "__main__":
    main()

Exempel¶




utskrifter.py och skript.py¶

utskrifter.py¶

In [ ]:
print(f"\n{'#'*10} utskrifter.py körs {'#'*10}")

print(f"* __name__ i början av utskrifter.py: {__name__}")

def skriv_ut_namnrymd():
    print(f"* __name__ i funktionen skriv_ut_namnrymd(): {__name__}")

print("\nAnropar skriv_ut_namnrymd() från utskrifter.py")
skriv_ut_namnrymd()

if __name__ == "__main__":
    print("\n__name__ har värdet '__main__' i utskrifter.py, kör skriv_ut_namnrymd()!")
    skriv_ut_namnrymd()
    
print(f"{'#'*10} utskrifter.py klar {'#'*10}\n")

skript.py¶

In [ ]:
#!/usr/bin/env python3
print(f"\n{'#'*10} skript.py körs {'#'*10}")

import utskrifter

print("__name__ i skript.py: {}".format(__name__))

print("Anropar utskrifter.skriv_ut_namnrymd() från skript.py")
utskrifter.skriv_ut_namnrymd()

if __name__ == "__main__":
    print("Anropar utskrifter.skriv_ut_namnrymd() IGEN från skript.py")
    utskrifter.skriv_ut_namnrymd()
    
print(f"\n{'#'*10} skript.py klar {'#'*10}\n")

Virtuella miljöer¶

No description has been provided for this image
  • Har inget med Virtual Reality att göra.

Problem vid installation av paket¶

  • Vissa projekt ska köras i miljöer med begränsade resurser
    • Dvs vi kan inte installera hur många onödiga paket som helst
  • Olika projekt kan behöva olika versioner av samma paket
  • Rättigheter kan saknas för att installera paket på systemnivå

  • Lösning
    • Användar- och/eller projektspecifika virtuella miljöer med t.ex. modulen venv

  • Överkurs (alternativ till venv som inte är inbyggda i Python):
    • pyenv
    • virtualenv (tredjepartsbibliotek som inspirerat venv)
    • pipenv
    • poetry
    • conda

venv - inbyggd modul¶

  • Skapar virtuell Python-miljö som kan aktiveras/avaktiveras.
  • Olika virtuella miljöer kan skapas för olika uppsättningar av Python-paket.
  • Endast virtuella miljöer för aktuell version av Python 3 kan skapas, t.ex. 3.10.12 på LiUs system.
    • (Med överkursverktygen kan även flera olika Python-versioner existera parallellt.)

venv¶

  • Skapa virtuell miljö:
    • $ python3 -m venv <katalog>
  • Aktivera virtuell miljö
    • $ source venv_mittprojekt/bin/activate
  • Av-aktivera virtuell miljö
    • $ deactivate
  • Installera paket i aktiv miljö
    • $ python3 -m pip install <paket>

Är det inte bättre att installera allt man behöver i en och samma miljö?¶

  • I verkligheten jobbar man ofta parallellt i flera olika projekt.
  • Alla projekt kan inte ha en produktionsmiljö där där massor onödiga saker finns.
  • Utvecklingsmiljön bör spegla produktionsmiljön i möjligaste mån.

Vanlig disposition för skript¶

  1. importer
  2. globala variabler
  3. funktioner
  4. kod utanför funktioner (ofta i ett __name__ == "__main__"-block).
In [2]:
import math

PRECISION = 3

def rounded_sqrt(value):
    """Compute the square root rounded to PRECISION decimal places."""
    return round(math.sqrt(value), PRECISION)

if __name__ == "__main__":
    print(rounded_sqrt(float(input(">>> "))))
>>> 7
2.646

Default-värden för parametrar¶

Default-värden¶

  • Funktioner med valfria argument; parametrar med defaultvärden
  • Används när man vill ha möjligheten att ange fler argument för att ändra en funktions beteende, men vill göra det möjligt att använda funktionen utan att ange alla argument
  • Används av många av funktionerna i matplotlib (Laboration 4, Del 1) men även av funktioner vi använt tidigare
  • Exempel
open(filnamn) # är samma sak som
open(filnamn, 'r', -1, None, None, None, True, None)

Default-värden¶

  • Syntax
In [3]:
def funktionsnamn(param1, param2=10, param3=20):
    print(f"param1: {param1}, param2: {param2}, param3: {param3}")
    
  • Argument utan default-värde måste alltid anges
  • Argument med default-värde kan hoppas över eller anges explicit (nyckelordsargument)
  • Positionella argument måste användas före nyckelordsargument.
In [7]:
funktionsnamn(1, param3=2)
  Cell In[7], line 1
    funktionsnamn(1, param3=2, 3)
                                ^
SyntaxError: positional argument follows keyword argument

Funktionsdefinitioner, defaultvärden¶

In [10]:
def print_greeting1(name):
    print(f"Hej {name}!")
    
def print_greeting2(name="Guido"):
    print(f"Hej {name}!")
    
def print_greeting3(name="Eggbert", greeting="Hej"):
    print(f"{greeting} {name}!")
    
print_greeting1("Yoda")
print_greeting2()
print_greeting3(greeting="Tjena")
print_greeting3("Tjena")
print_greeting3("Yoda", "Tjena")
Hej Yoda!
Hej Guido!
Tjena Eggbert!
Hej Tjena!
Tjena Yoda!

Använd inte muterbara värden som defaultvärden!¶

In [11]:
def get_short_names(name_candidates, short_names=[]):
    for name in name_candidates:
        if len(name) < 4:
            short_names.append(name)
    return short_names
In [40]:
print(get_short_names(["1234", "12345", "12"]))
['12', 'ab', 'ab', 'ab', 'ab', 'ab', 'ab', 'ab', 'ab', 'ab', 'ab', 'ab', 'ab', 'ab', 'ab', 'ab', '12', '12', '12', '12', '12', '12', '12', '12', '12', '12', '12', '12', '12']
In [27]:
print(get_short_names(["abcd", "abcde", "ab"]))
['12', 'ab', 'ab', 'ab', 'ab', 'ab', 'ab', 'ab', 'ab', 'ab', 'ab', 'ab', 'ab', 'ab', 'ab', 'ab']

Lösning: använd istället None som defaultvärde, hantera i funktionen¶

In [41]:
def get_short_names(name_candidates, short_names=None):
    if short_names is None:
        short_names = []
    for name in name_candidates:
        if len(name) < 4:
            short_names.append(name)
    return short_names
In [42]:
print(get_short_names(["1234", "12345", "12"]))
['12']
In [43]:
print(get_short_names(["abcd", "abcde", "ab"]))
['ab']

Bryta ut funktioner¶

Varför bryta ut funktion?¶

  • Minska upprepad kod
    • lättare att underhålla kod och uppdatera kod
    • lättare att läsa kod, inte lika lång
  • Minska väldigt snarlik kod
    • ersätt snarlika kodavsnitt med funktion som anpassar beteende givet argumenten
  • Underlätta läsning av kod – givet att bra funktionsnamn används.

Bryta ut funktioner, saker att tänka på¶

  • Scope och namespace: globala variabler och lokala variabler
  • Lokala variabler
    • alla variabler som definieras inne i en funktion (inkl. argumenten) är bara tillgängliga inuti funktionen
  • Globala variabler
    • variabler som definieras utanför funktioner är globala
    • om de ska användas inuti en funktion bör nyckelordet global användas
  • Undvik att använda globala variabler. Konstanter är ok, använd i så fall variabelnamn med endast versaler

Namngivning¶

  • Bra namn hjälper läsaren och programmeraren förstå vad koden gör.
  • Dåliga namn är intetsägande eller, ännu värre, vilseledande.
  • Använd verbliknande namn till funktioner
    • show_commands(), get_shortest_name(), load_data()
  • Använd substantivliknande namn till variabler; singular om det är ett värde, plural om det handlar flera värden (t.ex. listor och dictionaries)
    • command, commands, name, names

Exempel, scope¶

  • Globala och lokala variabler: localglobal.py
    • https://trinket.io/python3/923b3c723d
  • Varför blir det fel: localvars.py
    • https://trinket.io/python3/cfcddbf3ce
  • Varför blir det fel: globalvars.py
    • https://trinket.io/python3/f27c4c8c94

Bryta ut funktioner, exempel på tillvägagångssätt¶

  • Ta kod som upprepas och stoppa in den i en funktion
  • Lägg till argument till funktionen efter behov.
  • Byt ut upprepande kod mot anrop till funktionen du skapat.

  • Vid parprogrammering är en typisk uppgift för navigatören att påpeka när kod bör brytas ut till en egen funktion.

treasure_finder.py¶

In [ ]:
def treasure_finder():
    map1 = [55, 94, 39, 38, 57, 25, 53, 13, 57, 51]
    map2 = [71, 44, 98, 26, 18, 64, 37, 91, 81, 14]
    
    treasure_value = 81
    
    treasure_found = False
    pos = 0
    while pos < len(map1):
        if map1[pos] == treasure_value:
            treasure_found = True
            break
        pos += 1

    if treasure_found:
        print(f"Yay! Found treasure in map1 at position {pos}!")
    else:
        print("No treasure found in map1 :(")
        
    treasure_found = False
    pos = 0
    while pos < len(map2):
        if map2[pos] == treasure_value:
            treasure_found = True
            break
        pos += 1
        
    if treasure_found:
        print(f"Yay! Found treasure in map2 at position {pos}!")
    else:
        print("No treasure found in map2 :(")

treasure_finder()

treasure_finder_funcs.py¶

In [ ]:
def get_treasure_position(lst, treasure_value):
    pos = 0
    while pos < len(lst):
        if lst[pos] == treasure_value:
            return pos
        pos += 1
    return -1

def print_result(map_name, pos):
    if pos >= 0:
        print(f"Yay! Found treasure in {map_name} at position {pos}!")
    else:
        print(f"No treasure found in {map_name} :(")

def treasure_finder():
    map1 = [55, 94, 39, 38, 57, 25, 53, 13, 57, 51]
    map2 = [71, 44, 98, 26, 18, 64, 37, 91, 81, 14]
    treasure_value = 81
    treasure_pos = get_treasure_position(map1, treasure_value)
    print_result("map1", treasure_pos)
    treasure_pos = get_treasure_position(map2, treasure_value)
    print_result("map2", treasure_pos)

treasure_finder()

Riktlinjer för vad och hur man ska bryta ut¶

  • Försöka att se till att varje funktion endast gör en sak.
  • Försök att hålla funktioner korta (olika beroende på kodstil).
    • Ex. ~10 rader.

  • Låt funktioner ha verbliknande namn.
  • Låt variabler ha substantivliknande namn.
  • Använd förklarande namn.
  • Använd namn i plural för listor och dictionaries.

Bra heurestiker för att bryta ut funktioner¶

  • Försök se till så att en funktion bara gör en sak.
  • Är det svårt att skriva en lättförståelig men utförlig dokumentationssträng?
    • I så fall bör funktionen antagligen brytas upp i mindre delar som kan beskrivas enkelt.
  • Sätt en maxlängd på hur många rader en funktion får ha.
  • Sätt ett maxdjup på hur många nivåer av indentering en funktion får ha.