[Py3.x] Abischnitt-Berechnung

Stellt hier eure Projekte vor.
Internetseiten, Skripte, und alles andere bzgl. Python.
Antworten
Benutzeravatar
daemonTutorials
User
Beiträge: 171
Registriert: Sonntag 6. Februar 2011, 12:06
Kontaktdaten:

Hallo Python-Forum.de,

da bei mir bald die Noten verkündet werden und ich mir vor langer Zeit schon mal eine Excel-Tabelle gebastelt habe, um meinen Abiturnotenschnitt zu berechnen, habe ich mir ein Python-Skript geschrieben, um wieder reinzukommen.

Ich habe Python 3.x verwendet.

Die Formelelemente stammen aus der AVO GOBAK Niedersachsen: http://www.mk.niedersachsen.de/download ... .2014_.pdf für das Berufliche Gymnasium Technik

Doch hier zuerst der Python-Code:

Code: Alles auswählen

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import sys
import os
import csv

ALEVEL_FACTOR = 2
EXAM_FACTOR = 4
POINTS_MULTIPLIER = 40
POINTS_DIVIDER = 48

def readSemesterGrades(filename):
    points = 0
    
    with open(filename, 'r') as grades:
        reader = csv.reader(grades)
        
        for semester in reader:
            semester_grades = semester[0].split(';')
            
            for index, grade in enumerate(semester_grades):
                try:
                    if index <= 2:
                        # First three are the a-level courses
                        points += int(grade) * ALEVEL_FACTOR
                    else:
                        points += int(grade)
                except ValueError as error:
                    print('One of the grades isn\'t a number: {}'.format(error))
                    
    return points

def readExamGrades():
    points = 0
    
    for gradeIndex in range(2, 7):
        points += int(sys.argv[gradeIndex])
    
    return points
    
def calculateAverageGrade(points):
    return (17/3) - (points/180)

def main():
    #### Block 1 ###
    filename = sys.argv[1]    
    points_block1 = readSemesterGrades(filename)
                        
    # Multiply with factor from AVO GOBAK
    points_block1 *= POINTS_MULTIPLIER
    # Divide by divider from AVO GOBAK
    points_block1 = round(points_block1 / POINTS_DIVIDER, 0)
    
    #### Block 2 ####
    # Factor found in AVO GOBAK
    points_block2 = readExamGrades() * EXAM_FACTOR
    
    #### Sum and Average ####
    points_sum = points_block1 + points_block2
    average_grade = calculateAverageGrade(points_sum)

    #### Print results ####
    print(30 * '=')
    print('Points in Block 1: {}'.format(int(points_block1)))
    print('Points in Block 2: {}'.format(points_block2))
    print('Sum (Block1 + Block2): {}'.format(int(points_sum)))
    print(30 * '=')
    print('Average grade: {}'.format(round(average_grade, 2)))

if __name__ == "__main__":
    try:
        main()
    except (FileNotFoundError, IndexError):
        script_name = os.path.basename(sys.argv[0])
        print('Usage:')
        print('./{} grades.csv exam_grade_p1 exam_grade_p2 exam_grade_p3 ' \
              'exam_grade_p4 exam_grade_p5'.format(script_name))
        print()
        print('Example:')
        print('./{} grades.csv 12 13 10 12 9'.format(script_name))
        
Der Code erwartet die Noten für die jeweiligen eingebrachten Fächer in einer CSV-Datei, die pro Semester (4) wie folgt formatiert ist:

Code: Alles auswählen

P1;P2;P3;P4;P5;G1;G2;G3;G4
P1..5 sind die Prüfungsfächer und G1..4 die Abdeckerfächer. Hier unabhängig vom Fach die "Abdeckernoten" eintragen.

Als Argumente erwartet das Skript folgende:

Code: Alles auswählen

~$ ./abinoten.py noten.csv Prüfungsnote_P1 Prüfungsnote_P2 .. Prüfungsnote_P5
Also insgesamt sechs Argumente. Das Skript prüft nicht, ob in der CSV-Datei wirklich nur vier Abdeckerkurse eingetragen sind. Am einfachsten wird es wohl sein in LO Calc oder MS Excel die Daten einzutragen und dann als CSV-Datei zu speichern. Darauf achten, dass keine Spaltenüberschriften in der Datei enthalten sind.

Das Skript ist sicherlich verbesserungswürdig. Ich freue mich über Verbesserungsvorschläge. Die Formel für den Durchschnitt habe ich extra in eine Funktion verpackt, um sie durch den Code zu erklären. Ein Kommentar hätte genauso funktioniert.

Liebe Grüße,

Maik
Sirius3
User
Beiträge: 17712
Registriert: Sonntag 21. Oktober 2012, 17:20

@daemonTutorials: Du verwendest csv.reader falsch. Wenn Du als delimiter ';' verwendest, übergibst Du das dem reader und splittest nicht selbst am ';', denn so könntest Du den csv.reader gleich ganz weglassen.
Nach einem Fehler sollte der except-Block einen Zustand herstellen, nach dem das Programm sinnvoll weitergeführt werden kann. Das ist bei Deinem ValueError nicht der Fall, da das Fach einfach mit der Note 0 gezählt wird, Du also viel zu gute Ergebnisse bekommst. Die einzige sinnvolle Behandlung wäre, das Programm komplett abzubrechen. Python kennt 4 verschiedene String-Anführungszeichen, so dass es nie nötig ist ein Anführungszeichen innerhalb eines Strings escapen zu müssen.

Zeile 37: for-Schleifen über einen Index sind in Python ein Anti-Pattern. Am besten iteriert man direkt über die Liste. Oder gleich per sum und map:

Code: Alles auswählen

def readExamGrades():
    if len(sys.argv) != 7:
        raise IndexError("wrong number of arguments")
    return sum(map(int, sys.argv[2:]))
Zeile 43: die Klammern sind überflüssig.
Zeile 46, 50, 52, 55, 56, 59, 63: die Kommentare enthalten keine Information. Weg damit.
Zeile 77: Zeilenfortsetzungszeichen \ vermeiden, da z.B. ein nichtsichtbares Leerzeichen danach zu einem SyntaxError führt. Hier auch gar nicht nötig, da die Zeilen sowieso durch die Klammer zusammengehören.

In Python werden per Konvention auch Funktionsnamen klein_mit_unterstrich geschrieben.
Benutzeravatar
daemonTutorials
User
Beiträge: 171
Registriert: Sonntag 6. Februar 2011, 12:06
Kontaktdaten:

@Sirius3: Vielen Dank für deine Rückmeldung :-)

Ich habe das Programm soweit angepasst. Gerade über die mixedCase Geschichte bin ich auch schön früher gestolpert. Jetzt werde ich mich wieder daran halten :D

Zu dem Tipp mit sum() und map(): Vielen Dank, das ist viel viel eleganter. Ich glaube ich lese mir gleich erstmal die Liste der Built-In Functions durch.

Zu der Zeile, die ich umgebrochen habe: Ich wollte die Zeile nicht übermässig über die 79er Regel übertreten. Was wäre der bessere Weg, um "\" zu vermeiden? Den Hilfetext in eine Datei auslagern, lesen, format nutzen und ausgeben?

Nochmals vielen Dank :-)

Code: Alles auswählen

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import sys
import os
import csv

ALEVEL_FACTOR = 2
EXAM_FACTOR = 4
POINTS_MULTIPLIER = 40
POINTS_DIVIDER = 48

def read_semester_grades(filename):
    points = 0
    
    with open(filename, 'r') as grades:
        reader = csv.reader(grades, delimiter=';')
        
        for semester in reader:
            for index, grade in enumerate(semester):
                try:
                    if index <= 2:
                        # First three are the a-level courses
                        points += int(grade) * ALEVEL_FACTOR
                    else:
                        points += int(grade)
                except ValueError as error:
                    print("One of the grades isn't a number: {}".format(error))
                    print("Fix your issue and start again.")
                    sys.exit(0)
                    
    return points

def read_exam_grades():
    if len(sys.argv) != 7:
        raise IndexError('Wrong number of arguments')
        
    return sum(map(int, sys.argv[2:]))
    
def calculate_average_grade(points):
    return 17/3 - points/180

def main():
    filename = sys.argv[1]    
    points_block1 = read_semester_grades(filename)
    points_block1 *= POINTS_MULTIPLIER
    points_block1 = round(points_block1 / POINTS_DIVIDER, 0)
    
    points_block2 = read_exam_grades() * EXAM_FACTOR
    points_sum = points_block1 + points_block2
    average_grade = calculate_average_grade(points_sum)

    print(30 * '=')
    print('Points in Block 1: {}'.format(int(points_block1)))
    print('Points in Block 2: {}'.format(points_block2))
    print('Sum (Block1 + Block2): {}'.format(int(points_sum)))
    print(30 * '=')
    print('Average grade: {}'.format(round(average_grade, 2)))

if __name__ == "__main__":
    try:
        main()
    except (FileNotFoundError, IndexError) as error:
        script_name = os.path.basename(sys.argv[0])
        print(error)
        print()
        print('Usage:')
        print('./{} grades.csv exam_grade_p1 exam_grade_p2 exam_grade_p3 exam_grade_p4 exam_grade_p5'.format(script_name))
        print()
        print('Example:')
        print('./{} grades.csv 12 13 10 12 9'.format(script_name))
        
        
Liebe Grüße,

Maik
LG Maik
BlackJack

@daemonTutorials: Das \ am Zeilenende hättest Du einfach weglassen können. Da noch Klammern ”offen” sind, weiss der Compiler auch so dass der Ausdruck noch nicht zu Ende sein kann und (hoffentlich) in der nächsten Zeile weiter geht.
Benutzeravatar
daemonTutorials
User
Beiträge: 171
Registriert: Sonntag 6. Februar 2011, 12:06
Kontaktdaten:

@BlackJack: Ah, vielen Dank. Ich habe gerade mal durchgespielt, was du damit meinst.

Code: Alles auswählen

        # ....
        print('./{} grades.csv exam_grade_p1 exam_grade_p2 exam_grade_p3' 
              'exam_grade_p4 exam_grade_p5'.format(script_name))
        # ...
Vielen Dank :)
LG Maik
BlackJack

@daemonTutorials: Das `read_semester_grades()` das Programm abbrechen kann ist nicht gut. `sys.exit()` gehört irgendwo in das Hauptprogramm und auf keinen Fall in Funktionen die man einzeln testen können sollte. Benutzerinteraktion wie `print()` gehört in so eine Funktion auch nicht wirklich. Als `logging` wäre es vielleicht okay, weil dann der Aufrufer noch steuern kann ob und was ausgegeben wird.

Die Werte sollten auch besser geprüft werden. Stimmt die Anzahl der Zeilen und Spalten im CSV, sind alle Noten im richtigen Wertebereich. Man glaubt manchmal gar nicht was Benutzer so alles eingeben wenn man sie lässt. ;-)

Bei den Angaben auf der Kommandozeile kann `argparse` beim Prüfen nützlich sein. Und eine Hilfeausgabe bekommt man noch fast gratis dazu.
Benutzeravatar
daemonTutorials
User
Beiträge: 171
Registriert: Sonntag 6. Februar 2011, 12:06
Kontaktdaten:

@BlackJack: Ich habe argparse eingebaut und die Fehlerbehandlung etwas ausgebaut. Ist es bei argparse möglich anstatt 5x "grade" etwas wie "[P1..P5]" anzugeben?

Ich hoffe die Docstring-Kommentare sind sinnvoll.

Wäre es bei if 0 <= int(grade) <= 15 sinnvoller "if int(grade) not in range(0,16)" zu nutzen?

Code: Alles auswählen

#!/usr/bin/env python3
# -*- coding: utf-8 -*-

import sys

import csv
import argparse

ALEVEL_FACTOR = 2
EXAM_FACTOR = 4
POINTS_MULTIPLIER = 40
POINTS_DIVIDER = 48

def read_semester_grades(grades):
    """ Reads the semester grades from the given file """
    reader = csv.reader(grades, delimiter=';')
    if sum(1 for line in grades) is not 4:
        raise IndexError("The *.csv-file doesn't have the expected "
                         "four lines of semester grades.")
    grades.seek(0)
    
    points = 0
    for semester in reader:
        if len(semester) is not 9:
            raise IndexError("One of the semesters doesn't have the "
                             "expected nine grades.\nLength: {} => {}"
                             .format(len(semester), semester))
        
        for index, grade in enumerate(semester):
            try:
                int(grade)
            except ValueError as error:
                raise ValueError("One of the grades isn't a number. "
                                 "Please correct that.") from error
            
            if not 0 <= int(grade) <= 15:
                raise ValueError("Your grades should be between 0 and 15.")
            
            if index <= 2:
                # First three are the a-level courses
                points += int(grade) * ALEVEL_FACTOR
            else:
                points += int(grade)
                    
    return points

def read_exam_grades(grades):
    """ Reads the exam grades from the argument list """
    for grade in grades:
        if not 0 <= int(grade) <= 15:
            raise ValueError("Your grades should be between 0 and 15.")
        
    return sum(map(int, grades))
    
def calculate_average_grade(points):
    """ Calculates the average grade """
    return 17/3 - points/180

def main():
    parser = argparse.ArgumentParser(description='Calculates the average grade'
                                                 ' of your A-levels.')
    parser.add_argument('file', metavar='grades.csv', 
                        type=argparse.FileType('r'), nargs=1,
                        help='csv file with semester grades')
    parser.add_argument('grades', metavar='grade', type=int, nargs=5, 
                        help='five exam grades', choices=range(0,16))
    if len(sys.argv) is 1:
        parser.print_help()
        sys.exit(0)
    args = parser.parse_args()

    points_block1 = read_semester_grades(args.file)
    points_block1 *= POINTS_MULTIPLIER
    points_block1 = round(points_block1 / POINTS_DIVIDER, 0)
    
    points_block2 = read_exam_grades(args.grades) * EXAM_FACTOR
    points_sum = points_block1 + points_block2
    average_grade = calculate_average_grade(points_sum)

    print(30 * '=')
    print('Points in Block 1: {}'.format(int(points_block1)))
    print('Points in Block 2: {}'.format(points_block2))
    print('Sum (Block1 + Block2): {}'.format(int(points_sum)))
    print(30 * '=')
    print('Average grade: {}'.format(round(average_grade, 2)))

if __name__ == "__main__":
    try:
        main()
    except (ValueError, IndexError) as error:
        print(error)
        sys.exit(0)
        
Viele Grüße,

Maik
LG Maik
Sirius3
User
Beiträge: 17712
Registriert: Sonntag 21. Oktober 2012, 17:20

@daemonTutorials: hier mein Anmerkungen:

Zeile 17: die Datei zu lesen, nur um die Anzahl der Zeilen zu ermitteln ist unschön. Du kannst doch die Zeilen beim Verarbeiten zählen und wenn es keine 4 sind, immer noch den Fehler melden. Mit `is` sollte man nur bei wirklicher Objektidendität prüfen (meist bei None). is bei Strings oder Zahlen ist ein Programmierfehler, da er nur bei manchen Python-Implementierungen funktioniert.
Zeile 24: s. 17
Zeile 31: wenn Du schon die Umwandlung nach int machst, dann speichere sie doch in einer Variablen.
Zeile 36: <= ist der richtige Vergleich. Das ist performanter und meiner Meinung nach lesbarer.
Zeile 92: ein ErrorCode von 0 sagt normalerweise, dass kein Fehler aufgetreten ist.
Benutzeravatar
daemonTutorials
User
Beiträge: 171
Registriert: Sonntag 6. Februar 2011, 12:06
Kontaktdaten:

Hallo Sirius,

in meinem Skript war eh ein Fehler, und zwar mit dem ArgumentParser und dem FileType('r'). CSVReader hat da einen Fehler (iterator should return strings, not _io.TextIOWrapper (did you open the file in text mode?)) zurückgegeben. Also habe ich kurzerhand wieder nur einen string abgefragt.

Der Code ist jetzt hier zu finden, dann sind die Posts nicht zu lang: https://github.com/maikwoehl/abinoten/b ... binoten.py
LG Maik
Antworten