Fehlersuche

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.
Benutzeravatar
NoPy
User
Beiträge: 158
Registriert: Samstag 28. Dezember 2013, 12:39

Hi, Wissende.

Ich taste mich gerade an openpyxl heran, um mit Excel zu spielen.
Dabei spielt python mit mir leider verstecken und ich kann nicht entdecken, wo ich suchen soll.

Ich habe eine Dump- Funktion geschrieben (die Python- Puristen mögen mir verzeihen, dass sie so aussieht, aber anders kann ich es nun mal nicht vernünftig lesen, wenn ich mir das in einigen Monaten mal wieder ansehen muss.)

Dies Dump- Funktion löst quasi alle iterierbaren Objekte in Unterobjekte auf und schreibt, wenn nichts mehr iterierbar ist, hin, was drin steht, vorausgesetzt, es gibt die Funktion __str__.

Das sieht im Grunde auch so aus, wie erwartet. Allerdings macht mir folgendes Kopfschmerzen.

Code: Alles auswählen

a = [1,2,3,[4,6],6,[7,[8,9]]]
print Dump(a)
print Dump(a)
liefert beides mal das gleiche Ergebnis

Code: Alles auswählen

MyWorkBook = openpyxl.load_workbook('Test.xlsx',read_only = False)
MyWorkSheet = MyWorkBook['Tabelle1']
MyBereich = MyWorkSheet['A1:C2']
print Dump(MyBereich)
print Dump(MyBereich)
liefert beim ersten Dump auch noch, was ich erwarte. Beim zweiten Dump hingegen ist nur noch etwas vom Typ 'generator' übrig.
(zwischen beiden Dumps passiert exakt NICHTS)

Liegt es an meinem Dump oder habe ich wieder irgend etwas noch nicht verstanden?

Die Dump- Funktion sieht so aus:

Code: Alles auswählen

def Dump(pO,pcbMax = False):
    #lvso = type(po)+'\n'
    def Zeile(pI,pcnLevel,pcsIndent):
        if '__iter__' in dir(pI):
            if pcbMax:
                lvsI = 'a)%s%02d: %s\n' % (pcsIndent, pcnLevel, str(type(pI)))
            else:
                lvsI = '%s%s\n' % (pcsIndent, str(type(pI)))
            for pX in pI:
                lvsI += Zeile(pX,pcnLevel+1,pcsIndent+'\t')
        else:
            if 'value' in dir(pI):
                lvsValue = str(pI.value)
            else:
                lvsValue = ''
            if '__str__' in dir(pI):
                if pcbMax:
                    lvsI = 'b)%s%02d: %s [%s]\n' % (pcsIndent, pcnLevel, str(pI), lvsValue)
                else:
                    lvsI = '%s%s [%s]\n' % (pcsIndent, str(pI), lvsValue)
            else:
                if pcbMax:
                    lvsI = 'c)%s%02d: k.A. [%s]\n' % (pcsIndent, pcnLevel, lvsValue)
                else:
                    lvsI = '%sk.A. [%s]\n' % (pcsIndent, lvsValue)
        return lvsI
    lvsO = Zeile(pO,0,'')
    return lvsO
BlackJack

@NoPy: Die Funktion sieht echt gruselig aus was die Namensgebung angeht. Wie man das damit rechtfertigen kann das man die sonst nicht mehr versteht ist mir ein Rätsel.

Welche Objekte haben denn *kein* `__str__`-Attribut? Und der Test wäre auch besser ein ``hasattr(some_object, name)`` statt ``name in dir(some_object)``.

Der Fehler ist das Du irrtümlich erwartest das man einen Iterator/Generator zweimal benutzen kann. Geht halt nicht — wenn die Elemente einmal alle geliefert wurden ist der am Ende und ab da immer ”leer”.

Code: Alles auswählen

In [1]: g = (x**2 for x in xrange(10))

In [2]: list(g)
Out[2]: [0, 1, 4, 9, 16, 25, 36, 49, 64, 81]

In [3]: list(g)
Out[3]: []

In [4]: list(g)
Out[4]: []

In [5]: f = open('test.txt')

In [6]: list(f)
Out[6]: ['    15\xc2\xb0C        17\xc2\xb0C     19\xc2\xb0C        20\xc2\xb0C\r\n']

In [7]: list(f)
Out[7]: []

In [8]: list(f)
Out[8]: []
Übrigens erwischt Du mit dem Test auf `__iter__()` nicht alle iterierbaren Objekte! Auch welche mit `__getitem__()` können iterierbar sein, was man aber erst herausfindet wenn man tatsächlich darüber iteriert.
Benutzeravatar
NoPy
User
Beiträge: 158
Registriert: Samstag 28. Dezember 2013, 12:39

Vielen Dank für die schnelle Antwort.
Mein Problem liegt als im Mißverständnis des Generator- Objektes.

Aufgefallen ist mir das eigentlich nur dadurch, dass ich dieses Objekt einer Funktion übergeben habe und es in dieser weg war. Heisst das, dass man Generatorobjekte nicht als Parameter übergeben kann oder muss ich diesen Fehler noch an anderer Stelle suchen?

Zu den iterierbaren Objekten: Da python ja erst zur Laufzeit weiss, welchen typ irgendwelche Objekte haben, und mich eine IDE nicht unterstützen kann, musste ich mir quasi zu Forschungszwecken die tatsächlich von openpyxl ausgeworfenen Objekte ansehen (situationsabhängig) und diese hatten tatsächlich alle ein __iter__
Es ging also weniger darum, alle iterierbaren abzufassen, als nicht beim Iterieren gegen einen Fehler zu laufen. Dinge, die dort nicht vorkommen, interessieren mich derzeit noch nicht.

Es ist mir sowieso noch nicht gelungen, mit python wirklich wiederverwendbaren Code zu schreiben, dazu muss ich sicher noch etwas reifen.

Dank für die Hilfe
NoPy
BlackJack

@NoPy: Man kann jedes Objekt als Argument übergeben und Objekte verschwinden nicht einfach so. Iteratoren und Generatoren kann man nur einmal iterieren, also sich nur einmal alle darin enthaltenen Objekte geben lassen. Danach sind sie ”aufgebraucht”. Ich habe ja zwei Beispiele gezeigt, wovon auch nur das erste ein Generator-Objekt war. Man benutzt diese Objekte in der Regel ja genau wegen dieser Eigenschaft, dass die Elemente nicht alle auf einmal existieren müssen, sondern nur bei Anfrage für die einzelnen Elemente geliefert/erzeugt werden.
Benutzeravatar
NoPy
User
Beiträge: 158
Registriert: Samstag 28. Dezember 2013, 12:39

Jupp, mein Problem war, dass ich die Funktionsweise mit dem Generator nicht auf dem Schirm hatte.

Hinter generatoren können generatoren stecken, oder?
Um mal in Deinem Beispiel zu bleiben

Code: Alles auswählen

for x in xrange(10):
    for y in xrange(10):
        print (x+1)*(y+1)
Liefert das - umgeschrieben in die Form, die Du in Zeile 1 hast - generatoren, die generatoren verschachteln?

Und wenn es sie gibt, gibt es eigentlich eine Möglichkeit, hinter Generatoren versteckte Strukturen zu manifestieren?
Irgend etwas in der Form

Code: Alles auswählen

NeueStruktur = Manifestiere(AlteStruktur)
, so dass aus jedem Generator meinetwegen ein Tupel oder eine Liste wird?

Wo finde ich die Regeln, wann Werte kopiert werden, und wann Referenzen? Explizit angeben kann man das ja nicht, oder?

Code: Alles auswählen

Wert1=5
Wert2=Wert1
Wert2+=1
print Wert1
liefert 6 --> Wertkopie

Code: Alles auswählen

class A(object):
    def __init__(self):
        self.XYZ = 10
Objekt1 = A()
Objekt1.XYZ = 11
Objekt2 = Objekt1
Objekt2.XYZ = 15
print Objekt1.XYZ
liefert 15 --> Referenzkopie
BlackJack

@NoPy: Generatoren/Iteratoren können selber wieder auf Generatoren/Iteratoren zurückgreifen.

Du kannst natürlich die Elemente von Iteratoren in Listen auffangen, aber danach hast Du natürlich andere Typen und das kann zu Problemen führen wenn die Funktion die die ursprüngliche Datenstruktur erwartet hat etwas anderes ansprechen will als die Iterierbarkeit. Ein weiteres Problem sind Iteratoren die verwendet werden weil die Daten in Listen zu viel Arbeitsspeicher verbrauchen würde. Bis hin zu dem Fall das man ja auch *endlose* Iteratoren haben kann, die vom Code nur teilweise ausgewertet werden, aber Deine `dump()`-Funktion weiss ja nicht wie weit sie auswerten muss/darf.

Letztendlich verändert iterieren den internen Zustand der Objekte und es gibt keinen universellen Weg das wieder zurückzusetzen oder vorher eine 1:1 Kopie zu erstellen.

Es werden bei Zuweisungen und Argumentübergaben *nie* Werte kopiert. Ganz einfache Regel. Dein Beispiel für Wertkopie ist keines, auch da wird nichts kopiert. Du bindest `g` zuerst an die Liste [1,2,3] und danach an die Liste [2,4,6]. Wo wurde denn da Deiner Meinung nach etwas kopiert? Wenn man einen Namen an ein Objekt bindet, hat das keinerlei Einfluss auf das Objekt das vorher vielleicht mal an diesen Namen gebunden war, und umgekehrt.
Benutzeravatar
NoPy
User
Beiträge: 158
Registriert: Samstag 28. Dezember 2013, 12:39

BlackJack hat geschrieben:@NoPy: Generatoren/Iteratoren können selber wieder auf Generatoren/Iteratoren zurückgreifen.

Du kannst natürlich die Elemente von Iteratoren in Listen auffangen, aber danach hast Du natürlich andere Typen und das kann zu Problemen führen wenn die Funktion die die ursprüngliche Datenstruktur erwartet hat etwas anderes ansprechen will als die Iterierbarkeit.
Das ist für mich auch ein grundsätzliches Problem mit Generatoren/Iteratoren. Das erhöht sehr deutlich den Hä- Effekt, weil die Reihenfolge der Auswertung/Benutzung eines Objektes schlagartig eine Rolle spielt und einmal getroffene Erkenntnisse nicht reproduzierbar sind.
BlackJack hat geschrieben:Ein weiteres Problem sind Iteratoren die verwendet werden weil die Daten in Listen zu viel Arbeitsspeicher verbrauchen würde. Bis hin zu dem Fall das man ja auch *endlose* Iteratoren haben kann, die vom Code nur teilweise ausgewertet werden, aber Deine `dump()`-Funktion weiss ja nicht wie weit sie auswerten muss/darf.
Für das Sparen von Speicher erscheint mir persönlich das ein sehr hoher Preis zu sein. Endlose Iteratoren sind sicher ein Gegenargument.
BlackJack hat geschrieben:Letztendlich verändert iterieren den internen Zustand der Objekte und es gibt keinen universellen Weg das wieder zurückzusetzen oder vorher eine 1:1 Kopie zu erstellen.
Eine 1:1- Kopie vorher zu erstellen wäre schon möglich. Das größere Problem ist m.E., dass ein Objekt dem anderen hintenrum die Daten verbiegt, da man in python eben nicht angeben kann, ob etwas als Referenz oder als Kopie vorliegt.
BlackJack hat geschrieben:Es werden bei Zuweisungen und Argumentübergaben *nie* Werte kopiert. Ganz einfache Regel. Dein Beispiel für Wertkopie ist keines, auch da wird nichts kopiert. Du bindest `g` zuerst an die Liste [1,2,3] und danach an die Liste [2,4,6]. Wo wurde denn da Deiner Meinung nach etwas kopiert? Wenn man einen Namen an ein Objekt bindet, hat das keinerlei Einfluss auf das Objekt das vorher vielleicht mal an diesen Namen gebunden war, und umgekehrt.
Sorry dafür, das war ein Fehler meinerseits, ich hatte es daher zwischenzeitlich auf

Code: Alles auswählen

Wert1=5
Wert2=Wert1
Wert2+=1
print Wert1
geändert.

Ich weiß, der Mechanismus heißt copy on write, aber damit ist trotzdem noch nicht klar, an welchen Stellen was greift. Strings? Sets? Dictionarys? Listen?
Oder anders ausgedrückt: Welche Typen werden als Werte und welche als Referenz gespeichert?
Sirius3
User
Beiträge: 17750
Registriert: Sonntag 21. Oktober 2012, 17:20

@NoPy: alle Namen sind nur Referenzen auf Werte. Das "Wert2 += 1" ist auch eine Zuweisung und der alte Wert, an den Wert2 gebunden ist, wird durch den neuen ersetzt.

Bei Generatoren darfst Du einfach keine falschen Erwartungen haben, oder anders ausgedrückt, was in einer for-Schleife benutzt wird, ist nicht immer eine Liste. Du hast implizite Vorstellungen, wie etwas in Python funktioniert, was schlicht falsch ist. Wenn man die Vergänglichkeit von Generatoren akzeptiert, kann man gut damit leben.
BlackJack

@NoPy: Das mit dem speichersparen kommt halt darauf an was man so verarbeitet. Wenn man n Schritte bei der Verarbeitung von vielen gleichartigen Daten hat, und die einzelnen Schritte diese Daten immer transformieren, kann man das so machen das man jeden Schritt nacheinander macht, wodurch die Daten mindestens zweimal im Speicher liegen für jeden Schritt, die Daten vor jedem Schritt und die transformierten Daten nach dem Schritt, oder man löst das über Iteratoren/Generatoren und ”steckt” die zu einer Art ”Pipeline” zusammen und hat neben weniger Speicherverbrauch auch schneller die ersten Ergebnisse am Ende weil man nicht die n Schritte erst alle abarbeiten muss und dann alle Ergebnisse vorliegen, sondern man hat das erste Teilergebnis sowie der erste Datensatz einmal komplett durch die Pipeline gelaufen ist. Python bietet sowohl in der Sprache selbst (z.B. Generatorausdrücke und -funktionen) als auch bei den eingebauten Funktionen und der Standardbibliothek (z.B. das `itertools`-Modul) Werkzeuge um auf diese Art zu programmieren. Selbst die ``for``-Schleife arbeitet ja ausschliesslich auf iterierbaren Objekten.

Eine allgemeine 1:1-Kopie von einem Iterator/Generator ist nicht generell möglich, insbesondere dann wenn externer Zustand involviert ist, wie Dateien oder Datenbankverbindungen/-cursor. Und wenn ein in C geschriebener Iterator nicht serialisierbar ist weil der Entwickler das nicht vorgesehen hat, weil Iteratoren in der Regel auch nicht kopiert werden, dann hat man auch verloren.

Nochmal: Es gibt keinen Unterschied! *Alle* Objekte werden bei Zuweisungen und Argumentübergaben gleich behandelt. Auch in dem geänderten Beispiel wird nichts kopiert. Du bindest das Objekt 5 an `Wert1`, dann bindest Du das Objekt das an `Wert1` gebunden ist auch an `Wert2`, die sind jetzt beide an das selbe Objekt gebunden. Und dann führst Du eine Addition durch wordurch ein neues Objekt 6 entsteht was dann an `Wert2` gebunden wird.

Von Referenz und Wert zu sprechen macht keinen Sinn, denn auf Sprachebene gibt es nur Werte und in der Implementierung werden dafür Referenzen verwendet. Die Referenzen sind aber kein Datentyp von Python, das heisst man kann die nicht bewusst dereferenzieren oder halt nicht. Das Modell wird auch als „object sharing“ bezeichnet, weil sich alle Namen an die ein Objekt gebunden ist, das selbe Objekt teilen. Da wird wie gesagt *nie* ein Wert kopiert, ausser man macht das selbst explizit in dem man eine entsprechende Funktion oder Methode nutzt die ein neue Objekt erstellt und den oder die Werte aus dem alten in das neue kopiert — meistens entsteht dabei auch nur eine ”flache” Kopie, das heisst Original und Kopie sind zwar nicht das selbe Objekt, beide teilen sich aber die Werte die als Elemente oder Attribute in ihnen existieren.

Falls Du Variablen als Kästen mit einem Namen ansiehst in die man einen Wert hinein tut, dann solltest Du dieses Bild ganz schnell aus dem Kopf verbannen! Das passende Bild für Python (und auch einige andere Programmiersprachen) sind Objekte auf die kleine Post-It-Zettel mit Namen geklebt werden. Eine Zuweisung nimmt also nicht etwas aus einer Box und tut es in eine andere, sondern es wird ein Zettel auf ein Objekt geklebt, der vorher eventuell auf einem anderen Objekt klebte.
Benutzeravatar
NoPy
User
Beiträge: 158
Registriert: Samstag 28. Dezember 2013, 12:39

Sirius3 hat geschrieben:@NoPy: alle Namen sind nur Referenzen auf Werte. Das "Wert2 += 1" ist auch eine Zuweisung und der alte Wert, an den Wert2 gebunden ist, wird durch den neuen ersetzt.
Ja, im Grunde ist das schon klar, dennoch finde ich, ist die Sauberkeit durch Definition erzwungen.

Wenn ich die Funktion MachWas(x) aufrufe, dann wird - wenn x beispielsweise eine Zahl ist, mein x unverändert bleiben. Egal, was die Funktion MachWas tut, wenn x vorher den wert 3 hatte, hat es auch hinterher den Wert 3.
Wenn ich mir aber dir Mühe machen würde, eine Integer- Klasse zu bauen und diese an MachWas() übergebe, dann kann es sein, dass hinterher nicht mehr der Wert 3 in meinem x steht, weil hier nur die Referenz kopiert wird und sich das Copy on Write nicht auf die Elemente - also in diesem Fall den Wert - sondern den Verweis erstreckt.
Das meine ich mit Hä- Effekt.
Sirius3 hat geschrieben:Bei Generatoren darfst Du einfach keine falschen Erwartungen haben, oder anders ausgedrückt, was in einer for-Schleife benutzt wird, ist nicht immer eine Liste. Du hast implizite Vorstellungen, wie etwas in Python funktioniert, was schlicht falsch ist. Wenn man die Vergänglichkeit von Generatoren akzeptiert, kann man gut damit leben.
Mein Problem ist nicht die Vergänglichkeit von Generatoren an sich. Mein Problem ist, dass ich mich durch die Excel- Klassen durchhangle, die nicht in der Vollständigkeit dokumentiert sind, wie ich es brauche. Wenn ich dann - ähnlich der Heisenbergschen Unschärferelation - den Wert nur einmal auslesen kann, dann macht es das arbeiten schwer.

Für flüchtige Dinge ist es mit Sicherheit ein großer Vorteil, aber wenn Du den Inhalt einer Excel- Tabelle nach dem Anzeigen nicht mehr im Speicher hast, dann ist das blöd. Der Designer der Klasse hat zumindest anders gedacht, als ich. Sprich: Du fragst ihn nach einem Bereich, er baut einen Generator für dich, du hampelst durch diesen durch und manipulierst Werte und wenn Du hinterher wissen willst, ob es funktioniert hat, dann musst Du Dir einen neuen Generator geben lassen. Das geht zweifellos, aber mir macht es das Arbeiten gerade schwerer als nötig.
Grund: Ich brauche eine universelle Funktion, die einen rechteckigen Bereich manipuliert, ohne zu wissen, wo er sich befindet. Dieser Bereich soll dann später von dem, der diese Funktion benutzt hat, weiterverarbeitet werden. Und nachdem alles fertig ist, muss das zurückgeschrieben werden.
So bin ich gezwungen, der Funktion das Wissen über den Ort (im Tabelleblatt), an dem sich das befindet, mitzugeben (in irgend einer Weise, ob ich dazu eine eigene Klasse schreibe, um das zu kapseln oder schlicht die Grenzen mitgebe, ist dafür unerheblich)

Wie gesagt, alles lösbare Probleme, aber im Moment macht es meinen Code nur komplizierter.
BlackJack

@NoPy: Ich glaube Du hast es immer noch nicht verstanden: Es gibt hier kein „copy on write”, es passiert bei der Übergabe von `x` exakt das gleiche, völlig unabhängig welchen Typ `x` hat. Ob sich der Zustand des Objekts verändert liegt einzig an dem Objekt: Ob es sich verändern lässt. `int`-Objekte lassen sich halt nicht verändern. Du kannst auch eigene Werttypen schreiben und denen einfach keine öffentliche API geben mit der man deren Wert verändern kann. Wenn der Typ in C geschrieben ist, kann man das erzwingen (solange niemand auf das `ctypes`-Modul zurückgreift und böse Sachen macht), bei in Python geschriebenen musst Du darauf vertrauen das die Funktion nicht in den Interna Deines Wertes herummanipuliert. Und das ist etwas auf das man bei Python schon vetrauen sollte. „Consenting adults“ und so. :-)
Benutzeravatar
NoPy
User
Beiträge: 158
Registriert: Samstag 28. Dezember 2013, 12:39

BlackJack hat geschrieben:Falls Du Variablen als Kästen mit einem Namen ansiehst in die man einen Wert hinein tut, dann solltest Du dieses Bild ganz schnell aus dem Kopf verbannen! Das passende Bild für Python (und auch einige andere Programmiersprachen) sind Objekte auf die kleine Post-It-Zettel mit Namen geklebt werden. Eine Zuweisung nimmt also nicht etwas aus einer Box und tut es in eine andere, sondern es wird ein Zettel auf ein Objekt geklebt, der vorher eventuell auf einem anderen Objekt klebte.
Ist schon richtig, das Problem, das ich damit habe, ist ein logisches.
Wenn ich x als ein Ding betrachte und Teile dieses Dings verändere, dann würde ich erwarten, dass - bei copy on write - bei der Veränderung dieses Dinges eine neue Kiste mit dem alten PostIt angelegt wird. Tatsächlich passiert das aber nur durch explizite Zuweisung, also beispielsweise den Operator "=".
Beim Aufruf einer Funktion wird ebenfalls eine solche Zuweisung durchgeführt. Wenn ich in einer solchen Funktion Teile modifiziere, ohne/ehe erneut zuzuweisen, ändere ich damit zwangsläufig den übergebenen Wert. Bei einfachen Typen, wie integer und float- Typen spielt das keine Rolle, da es keine Funktionen gibt, diese zu modifizieren. Bei Veränderungen müssen sie erneut zugwiesen werden. Für den Aufrufenden ist der Hä- Effekt also gering. Wenn der Wert in der Funktion geändert wird, dann kommt auf die Änderung ein neues PostIt drauf, seine PostIts kleben immer noch auf Werten, die unverändert sind.
Selbst, wenn der Hintergrund ein anderer ist, resultierend wirkt es, wie eine Übergabe per Wert.

Sind die Variablen aber komplexer, also so, dass es Modifikationsmöglichkeiten gibt (Listen, Dictionaries, Klassen), dann klebt das PostIt auf einem Kästchen, dessen Inhalt sich ändert, OHNE dass ein neues PostIt angefangen wird.

Also nach dieser Sichtweise gibt es schon einen Unterschied.

Ich habe für mich noch keinen guten Weg gefunden, python- Code wiederverwendbar zu schreiben. Erklärung:

Code: Alles auswählen

#Modul A:
def a(x)...

#Modul B:
def b(x):
  A.a(x)

#Modul C:
def c(x):
  B.b(x)
Wenn ich b(x) schreibe, dann muss ich sowohl Annahmen treffen, für welche Sorte von Objekten ich funktionieren muss, ohne das so ohne Weiteres deklarieren zu können.
Die einzige Möglichkeit besteht darin, irgend etwas der Form

Code: Alles auswählen

if 'ABC' in dir(x) and (('DEF' in dir(x)) or ('EFG' in dir(x)):
  ...
else:
  return None
zu machen.
Und selbst, wenn ABC, DEF oder EFG vorkommen, kann ich immer noch nicht wissen, ob sie mit den von mir erwogenen Parametern funktionieren

Und im Hinblick auf a(x) weiß ich zwar, welche Parameter ich übergeben kann oder muss, aber welchen Typ diese Parameter haben müssen (oder um mit Duck- Typing zu sprechen, welche Funktionen sie auf welche Weise implementieren müssen, bekomme ich auch nur heraus, wenn ich mich durch den Quellcode von a hangele.

Als b(x) muss ich also ausreichend flexibel sein, so dass ein an sich sehr kompakt zu schreibender Python- Code theoretisch durch jede Menge Fallunterscheidungen aufgebläht werden müsste in der Weise entweder

Code: Alles auswählen

def SchmutzBeseitigen(Zimmer):
    if hatFunktion('Fegen'):
        Zimmer.Fegen(heute)
    elif hatFunktion('Wischen'):
        Zimmer.Wischen(heute)
    elif hatFunktion('Sprengen'):
        Zimmer.Sprengen(heute)
    else:
        return 'Schmutz nicht entfernbar'


oder

Code: Alles auswählen

def SchmutzBeseitigen2(Zimmer):
    ging = False
    if not ging:
        try:
            Zimmer.Fegen(heute)
            ging = True
        except
            pass
    if not ging:
        try:
            Zimmer.Wischen(heute)
            ging = True
        except
            pass
    if not ging:
        try:
            Zimmer.Sprengen(heute)
            ging = True
        except
            pass
    if not ging:
        return 'Schmutz nicht entfernbar'
wobei die letzte Variante den Charme hat, auch zu funktionieren, wenn die Funktion Sprengen vielleicht etwas anderes tut, als für ein Zimmer zu erwarten wäre.

Hat jemand diesbezüglich irgend eine Literaturempfehlung (und nein, es fängt nicht damit an, Variablen zu benennen oder Einrückungen zu beachten. Das ist trivial. Wie geht man damit um, auf ALLE Eventualitären an jeder Stelle gefasst sein zu müssen?
In meiner Vorstellung ist python quasi die Weiterentwicklung von Templates. Templates wurden geschaffen, damit man nicht für jeden Datentyp eigene Sortierroutinen, Stapel, Listen, Bäume schreiben muss. Allerdings habe ich den letzten 30 Jahren vielleicht 30 Klassen geschrieben, die im weitesten Sinne Templates waren. Bei dem Rest waren Klassen/Funktionen/Methoden etc. mit weit weniger Freiheitsgraden verbunden, weil sie Geschäftsprozesse beschrieben haben, die klar umrissen werden konnten.

Daher tue ich mich noch etwas schwer damit, effizient (nicht laufzeit- sondern entwicklungszeiteffizient mit Nachnutzung und so weiter) in python code zu schreiben.

Wo finde ich an dieser Stelle Hilfe, habt ihr eine Idee?
BlackJack

@NoPy: Das Post-It bei veränderbaren Objekten klebt auf dem Objekt. Und dessen Zustand kann man ändern. Das ist halt objektorientierte Programmierung (OOP).

Deine Beispiele sehen so aus als wenn Du da gegen OOP programmierst anstatt mit. Diese Tests sind in OOP ein „code smell“, das sieht eher wie ein Vorgehen aus welches man durch OOP gerade beseitigen will.

Der Test wäre übrigens mit `hasattr()` besser als mit ``in`` und `dir()`. Aber wie gesagt, eigentlich sollte man so etwas überhaupt nicht machen.

Was eine Funktion für Duck-Typen erwartet steht normalerweise in der Dokumentation oder sollte aus dem Kontext ersichtlich werden.

Die `SchmutzBeseitigen()`-Funktion ist überflüssig. Man würde einfach die `SchmutzBeseitigen()`-Methode auf `Zimmer` aufrufen. Und das Objekt entscheidet dann selber ob es sich fegt, wischt, oder sprengt. Oder etwas ganz anderes tut. Oder keine solche Methode besitzt und deshalb eine Ausnahme ausgelöst wird.

Die beiden Varianten wären kompakter mit einer Schleife und ohne `ging` im zweiten Fall. Die Ausnahmebehandlung ist suboptimal weil da ja nicht nur `AttributeError` ”behandelt” wird, sondern jede Ausnahme einfach verschwiegen wird und es sogar vorkommen kann das mehrere der Methoden vorhanden sind und teilweise ausgeführt werden und damit vielleicht zu einem inkonsistenten Zustand von `Zimmer` führen ohne das der Aufrufer dieser Funktion das durch eine Ausnahme mitbekommt wenn mindestens eine Methode dann fehlerfrei durchläuft. Dabei dann aber schon auf einem inkonsistenten Zimmer operieren könnte.
Sirius3
User
Beiträge: 17750
Registriert: Sonntag 21. Oktober 2012, 17:20

@NoPy: eine Funktion sollte eine Sache machen, und nur eine. Deine Funktionen sind mach_alles-Funktionen. Niemand baut eine Maschine, die Kaffee kochen, Auto wachen, Radio spielen und Zimmer aufräumen kann. Das Problem, das Du beschreibst gibt es also nicht, wenn man sich an die Grundsätze guter Programmierung hält, die quasi für jede Programmiersprache gelten. In C würde man auch nicht alles als void* übergeben und dann raten, auf was denn der Pointer zeigt. Dort geht es nicht, weil es gar kein Mittel zur Introspektion gibt. Warum sollte man das also in Python machen wollen?
Benutzeravatar
NoPy
User
Beiträge: 158
Registriert: Samstag 28. Dezember 2013, 12:39

BlackJack hat geschrieben:Die `SchmutzBeseitigen()`-Funktion ist überflüssig. Man würde einfach die `SchmutzBeseitigen()`-Methode auf `Zimmer` aufrufen. Und das Objekt entscheidet dann selber ob es sich fegt, wischt, oder sprengt. Oder etwas ganz anderes tut. Oder keine solche Methode besitzt und deshalb eine Ausnahme ausgelöst wird.
Ist sie nicht. Es geht hier ja ums Prinzip, das ich nicht verstehe. Die Funktion SchmutzBeseitigen könnte beispielsweise von der Funktion Raumschiff stammen. Dieses Raumschiff kann verschiedene Typen von Zimmern verwalten - ob es das letztlich in einer Liste, einem Baum oder einem Wörterbuch tut, ist dann Sache des Entwicklers des Raumschiffes.
Dass in dieses Raumschiff verschiedene Sorten von Zimmern "eingebaut" werden können, die von sehr unterschiedlichen Quellen kommen können (einer entwickelt alle Mannschaftsunterkünfte, einer alle Büros etc.), das ist ja nun auch nicht ungewöhnlich. (oder sie sind zu verschiedenen Zeiten unter verschiedenen Vorgaben entstanden). Und das man nicht immer den Einfluss darauf hat, dass eine jede Klasse die Funktion "SchmutzBeseitigen" bauen muss, in der sie dann Fegen, Wischen, Sprengen kapselt, ist auch nicht abwegig.
BlackJack hat geschrieben:Die beiden Varianten wären kompakter mit einer Schleife und ohne `ging` im zweiten Fall. Die Ausnahmebehandlung ist suboptimal weil da ja nicht nur `AttributeError` ”behandelt” wird, sondern jede Ausnahme einfach verschwiegen wird und es sogar vorkommen kann das mehrere der Methoden vorhanden sind und teilweise ausgeführt werden und damit vielleicht zu einem inkonsistenten Zustand von `Zimmer` führen ohne das der Aufrufer dieser Funktion das durch eine Ausnahme mitbekommt wenn mindestens eine Methode dann fehlerfrei durchläuft. Dabei dann aber schon auf einem inkonsistenten Zimmer operieren könnte.
Es ging mir um Anschaulichkeit. Die Frage bleibt: Wie gehe ich damit um, dass ich zur Design- Zeit eines "universellen" Moduls noch nicht bis ins Detail weiß, womit es mal von irgendwem gefüttert wird? Wie finde ich heraus, welche "Sorten" Parameter eine von mir benutzte Methode haben will und was sie mir zurückwirft, wenn ich eben keine "brauchbare" Dokumentation bekomme? Python dokumentiert sich insofern selbt, dass die Logik innerhalb eines Moduls/Klasse/Methode leicht zu erkennen ist. Aber aufgrund der großen Freiheiten benötigt man deutlich mehr externe Dokumentation, um das Zusammenspiel von Softwareteilen zu gewährleisten.
Das ist keine explizite Kritik, ich weiß nur, dass ich bislang mehr als die dreifache Zeit benötigt habe, etwas umzusetzen, als mit den "alten" Sprachen, und da rede ich noch nicht einmal von einer gescheiten Oberfläche. Ich will halt nur gern begreifen, wie ich es besser machen kann.
Benutzeravatar
NoPy
User
Beiträge: 158
Registriert: Samstag 28. Dezember 2013, 12:39

Sirius3 hat geschrieben:@NoPy: eine Funktion sollte eine Sache machen, und nur eine. Deine Funktionen sind mach_alles-Funktionen. Niemand baut eine Maschine, die Kaffee kochen, Auto wachen, Radio spielen und Zimmer aufräumen kann. Das Problem, das Du beschreibst gibt es also nicht, wenn man sich an die Grundsätze guter Programmierung hält, die quasi für jede Programmiersprache gelten. In C würde man auch nicht alles als void* übergeben und dann raten, auf was denn der Pointer zeigt. Dort geht es nicht, weil es gar kein Mittel zur Introspektion gibt. Warum sollte man das also in Python machen wollen?
Nein, aus Sicht eines Raumschiffes ist es eine elementare Funktion, das Zimmer sauberzubekommen. Und je nachdem, wie vielfältig die verschiedenen eingebauten Objekte sind, muss in irgendeiner Weise darauf reagiert werden. Und es kann ja nicht Sinn der Sache sein, alle Softwarebestandteile nachzubauen oder mit Wrappern zu versehen.

Um das noch ein wenig auszuschmücken:

Code: Alles auswählen

class MyMannschaftsUnterkunft(MannschaftsUnterkunft):
    def __init__(self,???):
        #wie ein Konstruktor aussehen müsste, der zuerst den geerbten Konstruktor ausführt, 
        #weiß ich im Moment nicht
        pass
        
    def SchmutzBeseitigen(self):
        #ist nur dafür da, damit Raumschiff auch mit Mannschaftsunterkunft leicht spielen kann
        self.Fegen()

class Raumschiff(objekt):
    def SchmutzBeseitigen(self,Zimmer):
        if hatFunktion('Fegen'):
            Zimmer.Fegen(heute)
        elif hatFunktion('Wischen'):
            Zimmer.Wischen(heute)
        elif hatFunktion('Sprengen'):
            Zimmer.Sprengen(heute)
        else:
            return 'Schmutz nicht entfernbar'
        
    def UeberallSchmutzBeseitigen(self):
        for Zimmer in self.AlleZimmer:
            self.SchmutzBeseitigen(Zimmer)
        self.Huelle.SchmutzBeseitigen()

    def UeberallSchmutzBeseitigen2(self):
        for Zimmer in self.AlleZimmer:
            Zimmer.SchmutzBeseitigen()
        self.Huelle.SchmutzBeseitigen()
        
    def EinfuegenZimmer(self, Zimmer):
        self.AlleZimmer.append(Zimmer) 

class Universum(objekt):
    def EinfuegenZimmerInRaumschiff(self):
        if random(3)==0:
            self.Raumschiff.EinfuegenZimmer(MyMannschaftsUnterkunft())
Nur mal so als PseudoCode. Es wäre schön, wenn ihr mir sagen könntet, wie ihr das machen würdet. Was daran falsch ist, ist leider nicht immer hilfreich.
BlackJack

@NoPy: Doch die Funktion ist überflüssig, denn das was Du da mit dem Raumschiff beschreibst ist genau das was man *nicht* machen will, weil das unflexibel und schlecht erweiterbar ist. Zimmer müssen die Methode `schmutz_beseitigen()` haben weil das Teil der API ist. Ohne diese Methode sind es für ein Raumschiff ganz einfach keine geeigneten Zimmer. Eine ``if``/``elif``-Kaskade die letztendlich auf Duck-Typen prüft ist in OOP genauso ein „anti pattern“ wie das prüfen auf konkrete Typen.

Du musst nicht herausfinden was Deine universelle Methode bekommen könnte sondern *festlegen* was sie akzeptiert und was nicht. Wenn dokumentiert ist das ein Zimmer für diese Funktion nur dann ein sinnvolles Zimmer ist wenn es eine `schmutz_beseitigen()`-Methode hat, dann ist alles geregelt. Und wirklich universelle Funktionen sind auch eher sehr selten. Damit würde ich auch nicht anfangen, denn allzu oft hat man dann einen Haufen ”universellen” Kram der genau *eine* Sache macht, aber komplizierter als wenn man vom konkreten ausgegangen wäre und dort dann die Gemeinsamkeiten abstrahiert hätte.

Bei dem Raumschiff-Beispiel würde ich ganz einfach festlegen das Zimmer eine `schmutz_beseitigen()`-Methode haben müssen und gut ist.

Wenn die das nicht bieten musst Du vielleicht Wrapper schreiben, aber der Code müsste sonst ja in der `Raumschiff.schmutz_beseitigen()` mit den vielen ``elif``\s stehen. Nur das er *dort* nicht hingehört weil das ein Albtraum beim warten und erweitern ist wenn man immer irgendwo so eine zentrale Methode hat die alles mögliche über Objekte wissen muss über die sie nichts wissen müssen sollte. Jemand der neue Zimmer baut müsste zum Beispiel Code beim Raumschiff ändern wenn er das hinzufügen möchte. Das schliesst Zimmer von Drittanbietern aus.
Benutzeravatar
NoPy
User
Beiträge: 158
Registriert: Samstag 28. Dezember 2013, 12:39

Die Praxis ist doch niemals so, dass Du Herr über das komplette Geschehen bist. Du bekommst Module geliefert, die zwar funktional das haben, was Du benötigst, aber nicht so, wie Du es benötigst. Selbst, wenn Du ein komplettes Projekt aus dem Nichts erschaffst, benutzt Du doch in der Regel IMMER irgendwelche Module, die auf Datenbanken, Webservices, WebFeatureServices etc. zugreifen. Und wenn Du etwas brauchst, das nach oben hin eine Funktionalität zur Verfügung stellst, die "transparent" machen, was Du benutzt, dann muss an irgend einer Stelle eine Anpassung stattfinden zwischen den verschiedenen Interfaces.

Also: Wrapper ist aus Deiner Sicht die Antwort.

Im Grunde muss man dann für jedes benutzte Modul, was nicht von sich aus in der Weise liefert, wie Du es oben benutzen willst, einen Wrapper schreiben.
Und es steht damit im Grunde fest, dass für das Modul, was ich gerade schreibe, auch wieder irgend jemand einen Wrapper schreiben muss, denn wenn aus meiner Sicht "fegen" eine sinnvolle Methode ist, ein anderer aber das universellere "SauberMachen" benutzen will, dann muss er meine Klasse kapseln.

Wie geht das unter Python richtig?

Code: Alles auswählen

class A(objekt):
    def __init__(self,a,b,c,d):
        self.x=[a,b,c,d]
    def Fegen(self):
        pass
    
class WrapperA(???):
    ???
    def SauberMachen(self):
        self.Fegen()
        
MeinA = WrapperA(1,2,3,4,5,6,7,8)
BlackJack

@NoPy: In der Praxis benutze ich die API von den Bibliotheken die ich einbinde und versuche da nicht selber vorher und prophylaktisch irgendeine universelle API als Zwischenschicht einzuziehen. Das wird doch erst interessant wenn man die Bibliothek tatsächlich mal austauschbar machen will, was in der überwiegenden Anzahl der Fälle nie passiert. Und falls es dann doch sein muss kann man zu dem Zeitpunkt noch eine eine Zwischenschicht ziehen, die dann auch tatsächlich praktisch verwendet wird. Ich denke Du gehst zu theoretisch an die Sache.

Das Problem welches Du beschreibst ist in dem Sinne dann ja auch gar nichts Python-spezifisches. Das hätte man doch in jeder Programmiersprache wenn man externe APIs benutzt die man unbedingt alle austauschbar machen möchte.
Benutzeravatar
NoPy
User
Beiträge: 158
Registriert: Samstag 28. Dezember 2013, 12:39

BlackJack hat geschrieben:@NoPy: In der Praxis benutze ich die API von den Bibliotheken die ich einbinde und versuche da nicht selber vorher und prophylaktisch irgendeine universelle API als Zwischenschicht einzuziehen. Das wird doch erst interessant wenn man die Bibliothek tatsächlich mal austauschbar machen will, was in der überwiegenden Anzahl der Fälle nie passiert.
Mein Problem ist, dass ich momentan wie ein Idiot jedesmal von vorn anfange, weil ich zu unterschiedlichen Zeiten mit python zwar ähnliche Dinge mache, die aber nie vollständig identisch sind. Und es ist mir nicht gelungen, irgendetwas wiederzuverwenden, nicht einmal Copy/Paste hilft mir weiter, da mich kein Compiler vor irgendwelchen Fehlern warnt und die erst zur Laufzeit auftreten.

Ich benutze Python derzeit an 3 verschiedenen Stellen: FME (Feature Manipulating Engine), für diverse kleinere Tools, Anzapfen der Google- Maps- API (und an dieser Stelle habe ich auch den Austausch der Bibliotheken, da tatsächlich die Bibliotheken mehrerer Anbieter in Frage kommen könnten. Letztlich will ich von oben sagen: Hier hast Du 2 Adressen, berechne mir die Entfernung, die der Routenplaner liefert und die Zeit und wirf sie mir zurück. Eventuell muss das noch parametrisiert werden.

Ich habe grundsätzlich mit Datenbanken zu tun, mit Excel- Tabellen, mit Shapes etc.
Im Moment muss ich aus einer Excel- Tabelle lesen, die Abfragen an die API basteln, das Ergebnis wieder nach Excel schaufeln.

Es nervt mich total, dass ich jetzt wieder eine eigene Klasse baue für Excel- Zugriff, für Google- API etc, obwohl ich auch vorher schon mal was für Excel und diese API geschrieben habe.
Unter C; Delphi; Lazarus war das kein Problem, ich konnte auf einmal geschriebenes aufbauen.
Mir ist schon klar, dass das unter python auch gehen muss. Aber ICH weiß noch nicht, wie ICH das anfangen muss.
BlackJack hat geschrieben:Und falls es dann doch sein muss kann man zu dem Zeitpunkt noch eine eine Zwischenschicht ziehen, die dann auch tatsächlich praktisch verwendet wird. Ich denke Du gehst zu theoretisch an die Sache.
Was soll ich sagen, ehe ich es theoretisch nicht durchstiegen habe, kann ich nicht zu praktikablen Lösungen finden.
BlackJack hat geschrieben:Das Problem welches Du beschreibst ist in dem Sinne dann ja auch gar nichts Python-spezifisches. Das hätte man doch in jeder Programmiersprache wenn man externe APIs benutzt die man unbedingt alle austauschbar machen möchte.
Das hat man, aber damit komme ich schon zurecht. Das löst sich relativ einfach dadurch, dass nicht das Schriftbild des Quelltextes reglementiert ist, sondern die Schnittstelle.
Wenn ich schreibe

Code: Alles auswählen

      if (
a=1000) then
                    write('Ätsch');
a.HastDuNichtGesehen(12345);
dann sieht das zwar Scheiße aus, aber funktioniert trotzdem ohne weitere Verwirrung.
Wenn a aber mit 1000 nichts anfangen kann, weil es vom Typ string ist, dann sagt mir das der Compiler schon zur Designzeit. Dazu kommt, dass - wenn a ein Objekt ist - die IDE mir mühelos sagen kann, welche Methoden von a überhaupt an dieser Stelle zur Verfügung stehen. Und bei a.HastDuNichtGesehen kommt beim Compilieren der Fehler, nicht später. So kommt man - vorausgesetzt, jemand hat wirklich sprechende Namen verwendet - auch mit schlecht dokumentierten Modulen zurecht, auch ohne im Quelltext zu wühlen.

Das mag nicht besser sein, aber im Moment empfinde ich es als störunanfälliger. Mein eigentliches Ziel ist aber, zu lernen, wie man es mit Python SINNVOLL im Python- Stil macht, bislang kann es nicht sinnvoll gewesen sein, soviel Code, wie ich mehrfach geschrieben habe.
Antworten