Funktionsaufruf mit vielen Parametern

Wenn du dir nicht sicher bist, in welchem der anderen Foren du die Frage stellen sollst, dann bist du hier im Forum für allgemeine Fragen sicher richtig.
Antworten
Benutzeravatar
16_Bit
User
Beiträge: 21
Registriert: Donnerstag 26. März 2020, 14:14

Hallo,

ich habe eine Funktion an die ich relativ viele Parameter übergebe - Tendenz steigend.

Code: Alles auswählen

def add_participants(sessionKey: str, surveyID: int, createToken: bool, token: int, validFrom: str, validUntil: str, email: str, lastname: str, firstname: str):
    participant = [{"email": email, "lastname": lastname, "firstname": firstname, "token": token}]
    params = {"sSessionKey": sessionKey, "iSurveyID": surveyID, "aParticipantData": participant, "bCreateToken": createToken}
    ...
    ...

add_participants(sessionKey, surveyID, createToken, token, validFrom, validUntil, "dieter.hallervorden@fun.de", "Hallervorden", "Dieter")
Wie würdet ihr den mit dieser Situation umgehen? Einfach so lassen? Formatierung für Lesbarkeit ändern? Funktion mit def add_participants(*args)? Übergabe eines Dictionaries? Was ganz anderes?

Ich bin auf eure Antworten gespannt.
__deets__
User
Beiträge: 14542
Registriert: Mittwoch 14. Oktober 2015, 14:29

Ohne mehr Kontext nicht abschliessend zu beurteilen. Wenn Teile dieser Daten - sessionKey,y, surveyID - wiederkehrend in anderen Aufrufen sind, dann lohnt sich ggf. die Anlage und Nutzung einer Klasse.

Und eine Anmerkung: du wechseltst die Namenskonvention bei den Parametern. Auch die werden klein_mit_unterstrich geschrieben.
Sirius3
User
Beiträge: 17753
Registriert: Sonntag 21. Oktober 2012, 17:20

Da hast da doch schon eine natürlich Einteilung. Da gibt es den `participant`, der schonmal vier Parameter konsumiert. Das kann man jetzt direkt als Wörterbuch übergeben, oder als Klasse, oder Named-Tuple, je nachdem, wie das sonst verwendet wird. session_key hört sich so an, als ob das ein Zustand dieser add-Funktion wäre, so dass man dafür eigentlich eine Klasse schreiben will, die eine Session repräsentiert.
Benutzeravatar
16_Bit
User
Beiträge: 21
Registriert: Donnerstag 26. März 2020, 14:14

Danke für den Hinweis mit der Namenskonvention. Habe das jetzt angepasst.

Das ist was ich bis jetzt zusammen gefummelt habe mit meinen mageren Python-Wissen.

Code: Alles auswählen

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

import requests
import json
from uuid import uuid4

headers = {'content-type': 'application/json'}
url = "https://umfrage.de/index.php/admin/remotecontrol"
api_user = "lsrc"
api_password = "V&v3DNU#AwV4GJ"
survey_id = 928126
create_token = False
token = 1234567890
valid_from = ''
valid_until = ''
email = "dieter.hallervorden@fun.de"
lastname = "Hallervorden"
firstname = "Dieter"

def set_params(method, params):
    data = {'method': method, 'params': params, 'id': str(uuid4())}
    return json.dumps(data)

def get_session_key():
    params = {'username': api_user, 'password': api_password}
    data = set_params('get_session_key', params)
    request = requests.post(url, data=data, headers=headers)
    return request.json()['result']
    
def release_session_key(session_key):
    params = {"sSessionKey": session_key}
    data = set_params('release_session_key', params)
    request = requests.post(url, data=data, headers=headers)
    return request.json()

def add_participants(session_key: str, survey_id: int, create_token: bool, token: int, valid_from: str, valid_until: str, email: str, lastname: str, firstname: str):
    participant = [{"email": email, "lastname": lastname, "firstname": firstname, "token": token}]
    params = {"sSessionKey": session_key, "iSurveyID": survey_id, "aParticipantData": participant, "bCreateToken": create_token}
    data = set_params('add_participants', params)
    request = requests.post(url, data=data, headers=headers)
    return request.json()


if __name__ == "__main__":
    session_key = get_session_key()
    add_participants(session_key, survey_id, create_token, token, valid_from, valid_until, email, lastname, firstname)
    release_session_key(session_key)
    
# END
Der Aufruf von def get_session_key(): ist glaube ich auch nicht so ganz sauber, auch wenn es funktioniert. Müsste man nicht besser api_user und api_password mit übergeben?

Bei add_participants sind folgende Parameter während der Laufzeit variabel, da mehrere Personen angelegt werden sollen. Dazu wird später eine CSV-Datei ausgelesen, in der die Daten stehen.

Code: Alles auswählen

 token, valid_from, valid_until, email, lastname, firstname
Eine eigene Klasse habe ich bisher noch nie geschrieben. Wenn das nötig sein sollte müsste ich mich erst einmal ausgiebig damit beschäftigen.
Sirius3
User
Beiträge: 17753
Registriert: Sonntag 21. Oktober 2012, 17:20

KONSTANTEN schreibt man komplett groß, wenn es Konstanten sind, ansonsten haben die Variablen auch nichts auf oberster Ebene verloren.
Wie schon geschrieben, gehört das ganze Session-Handling in eine Klasse, die die Web-API kapselt.
Wenn die Daten eh schon aus einer csv-Datei kommen, dann liefert Pandas schon automatisch die passenden Datensätze zusammen, da gibt es also keinen Grund, die für die Übergabe an Funktionen auseinander zu nehmen.
einfachTobi
User
Beiträge: 491
Registriert: Mittwoch 13. November 2019, 08:38

Hier gibt es schon eine Implementierung bei der du schauen kannst, wie es da gelöst ist: https://github.com/lindsay-stevens/limesurveyrc2api.
Dort wird eine Liste mit einem Dict je User erwartet. Das Ganze steckt dort auch ein einer Klasse.
Benutzeravatar
DeaD_EyE
User
Beiträge: 1021
Registriert: Sonntag 19. September 2010, 13:45
Wohnort: Hagen
Kontaktdaten:

Hier ein Beispiel: https://python-emails.readthedocs.io/en/latest/
render und smtp werden als dicts an die Methode send übergeben.

Es kann nicht schaden, sich mal bereits existierenden Code anzusehen.
Es gibt gute und nicht so gute Beispiele.
sourceserver.info - sourceserver.info/wiki/ - ausgestorbener Support für HL2-Server
Benutzeravatar
__blackjack__
User
Beiträge: 13111
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@16_Bit: Auf Modulebene sollte wie schon erwähnt wurde keine Variablen stehen. Deshalb gehört auch der Code im ``if __name__ …``-Zweig in eine Funktion.

`headers` und das `json`-Modul brauchst Du nicht, denn `requests.post()` hat auch ein `json`-Argument welches das für Dich erledigt.

`set_params()` macht relativ wenig und ist Teil von etwas das bei den drei anderen Funktionen am Ende steht und überall eigentlich bis auf den Methodennamen in den Daten und die Parameter gleich ist.

Das was Du da immer `request` nennst ist genau das Gegenteil: `response`.

Man sollte die `response` auf Fehler testen.

`add_participants()` ist als Name falsch weil damit nur *ein* Teilnehmer hinzufügt werden kann.

Mal ein kleiner Ausschnitt aus dem Zwischenstand um zu sehen was aus `set_params()` geworden ist (`call()`) und wie kurz die anderen Funktionen dadurch werden:

Code: Alles auswählen

def call(method, params):
    response = requests.post(
        URL, json={"method": method, "params": params, "id": str(uuid4())}
    )
    response.raise_for_status()
    return response.json()


def get_session_key():
    return call(
        "get_session_key", {"username": API_USER, "password": API_PASSWORD}
    )["result"]


def release_session_key(session_key):
    return call("release_session_key", {"sSessionKey": session_key})

...
Packen wir das in eine Klasse die eine Session repräsentiert und benennenen `release_session_key()` in ein traditionelleres `close()` um:

Code: Alles auswählen

class Session:
    def __init__(self):
        self.session_key = None
        self.session_key = self.call(
            "get_session_key", {"username": API_USER, "password": API_PASSWORD}
        )["result"]

    def call(self, method, params=None):
        if self.session_key:
            params["sSessionKey"] = self.session_key
        response = requests.post(
            URL,
            json={
                "method": method,
                "params": params if params else {},
                "id": str(uuid4()),
            },
        )
        response.raise_for_status()
        return response.json()

    def close(self):
        return self.call("release_session_key")
    
    ...
Bietet sich jetzt einfach an einen Kontextmanager aus dem Datentyp zu machen, damit man das Hauptprogramm so schreiben kann:

Code: Alles auswählen

def main():
    with Session() as session:
        session.add_participant(
            SURVEY_ID,
            CREATE_TOKEN,
            TOKEN,
            VALID_FROM,
            VALID_UNTIL,
            EMAIL,
            LASTNAME,
            FIRSTNAME,
        )
Und jetzt mal alles, mit einem Teilnehmer beispielhaft zusammengefasst mit einer Klasse + `attr`-Modul (ungetestet):

Code: Alles auswählen

#!/usr/bin/env python3
from uuid import uuid4

import requests
from attr import asdict, attrib, attrs

URL = "https://umfrage.de/index.php/admin/remotecontrol"
API_USER = "lsrc"
API_PASSWORD = "V&v3DNU#AwV4GJ"
SURVEY_ID = 928126
CREATE_TOKEN = False
VALID_FROM = ""
VALID_UNTIL = ""
EMAIL = "dieter.hallervorden@fun.de"
LASTNAME = "Hallervorden"
FIRSTNAME = "Dieter"
TOKEN = 1234567890


@attrs(frozen=True)
class Participant:
    email = attrib()
    lastname = attrib()
    firstname = attrib()
    token = attrib()

    as_dict = asdict


@attrs
class Session:
    session_key = attrib(default=None)

    def __attrs_post_init__(self):
        self.session_key = self.call(
            "get_session_key", {"username": API_USER, "password": API_PASSWORD}
        )["result"]

    def __enter__(self):
        return self

    def __exit__(self, _exception_type, _value, _traceback):
        self.close()

    def call(self, method, params=None):
        if self.session_key:
            params["sSessionKey"] = self.session_key
        response = requests.post(
            URL,
            json={
                "method": method,
                "params": params if params else {},
                "id": str(uuid4()),
            },
        )
        response.raise_for_status()
        return response.json()

    def add_participant(
        self, survey_id, create_token, valid_from, valid_until, participant
    ):
        return self.call(
            "add_participants",
            {
                "iSurveyID": survey_id,
                "aParticipantData": [participant.as_dict()],
                "bCreateToken": create_token,
            },
        )

    def close(self):
        return self.call("release_session_key")


def main():
    with Session() as session:
        session.add_participant(
            SURVEY_ID,
            CREATE_TOKEN,
            VALID_FROM,
            VALID_UNTIL,
            Participant(EMAIL, LASTNAME, FIRSTNAME, TOKEN),
        )


if __name__ == "__main__":
    main()
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Benutzeravatar
16_Bit
User
Beiträge: 21
Registriert: Donnerstag 26. März 2020, 14:14

Danke schon mal für die zahlreichen Antworten. Um ehrlich zu sein bin ich mit den meisten Ideen und Vorschlägen überfordert, da objektorientierte Programmieren Böhmische Dörfer sind und ich bisher nichts verstehe.

@__blackjack__

Danke für deine ausführliche Antwort. Ich bin dein Script gerade am testen, um mich dann Zeile für Zeile durchzuackern, in der Hoffnung es irgend wann verstehen zu können.

Im Moment bekomme ich aber schon direkt am Anfang eine Fehlermeldung.

Code: Alles auswählen

from attr import asdict, attrib, attrs
ImportError: cannot import name 'asdict' from 'attr' (/Users/patrick/Coding/LimeSurvey/env/lib/python3.7/site-packages/attr.py)
Hatte zuvor pip install attr ausgeführt.
Benutzeravatar
__blackjack__
User
Beiträge: 13111
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

@16_Bit: Falsches Modul. Es gibt leider ein `attr`-PyPI-Package das ein `attr.py`-Modul installiert und es gibt ein `attrs`-PyPI-Package das ein `attr`-Package installiert.
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Benutzeravatar
16_Bit
User
Beiträge: 21
Registriert: Donnerstag 26. März 2020, 14:14

@__blackjack__

Ok, danke. Mit dem richtigen attr ist die Hürde genommen.

Aktuell bricht das Script mit dieser Fehlermeldung ab.

Code: Alles auswählen

(env) ➜  LimeSurvey ./lm1.py
Traceback (most recent call last):
  File "./lm1.py", line 87, in <module>
    main()
  File "./lm1.py", line 82, in main
    Participant(EMAIL, LASTNAME, FIRSTNAME, TOKEN),
  File "./lm1.py", line 43, in __exit__
    self.close()
  File "./lm1.py", line 72, in close
    return self.call("release_session_key")
  File "./lm1.py", line 47, in call
    params["sSessionKey"] = self.session_key
TypeError: 'NoneType' object does not support item assignment
Ich brüte schon länger über dem Script aber ich kann den Fehler selber nicht finden. Dazu fehlen mir einfach zu viele Grundlagen.

Das def call scheint ein universeller Aufruf zu sein dem man einfach nur die gewünschte Methode übergibt. Also get_session_key, add_participants oder was auch immer

Code: Alles auswählen

    def __attrs_post_init__(self):
        self.session_key = self.call(
            "get_session_key", {"username": API_USER, "password": API_PASSWORD}
        )["result"]
Kann ich noch nicht einordnen. Ich denke durch die Unterstriche ruft sich die Funktion am Anfang selber auf.

Code: Alles auswählen

        if self.session_key:
            params["sSessionKey"] = self.session_key
Wegen __attrs_post_init__ ist die if Abfrage true und versucht den session_key nach params zu schreiben.

Code: Alles auswählen

def call(self, method, params=None):
Darf params dann nicht vom Typ None sein???
Sirius3
User
Beiträge: 17753
Registriert: Sonntag 21. Oktober 2012, 17:20

Das passiert, wenn man nachträglich noch etwas erweitert, dann aber nicht mehr gründlich testet. Unten wird zwar auf params == None geprüft, oben wird params aber schon benutzt. Also muß man die Prüfung nach oben schieben.

Code: Alles auswählen

    def call(self, method, params=None):
        params = dict(params) if params else {}
        if self.session_key:
            params["sSessionKey"] = self.session_key
        response = requests.post(
            URL,
            json={
                "method": method,
                "params": params,
                "id": str(uuid4()),
            },
        )
        response.raise_for_status()
        return response.json()
Benutzeravatar
__blackjack__
User
Beiträge: 13111
Registriert: Samstag 2. Juni 2018, 10:21
Wohnort: 127.0.0.1
Kontaktdaten:

Ich habe ja extra „ungetestet“ dran geschrieben. 🙂
„All religions are the same: religion is basically guilt, with different holidays.” — Cathy Ladman
Antworten