Objektorientierung - unerwartetes Verhalten

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
Holger Chapman
User
Beiträge: 35
Registriert: Samstag 12. Juli 2014, 01:59

Hallo,

ich versuche gerade, mich in Objektorientierung einzuarbeiten, und komme an einer Stelle nicht weiter. Vielleicht kann jemand von euch mir helfen?

Zum Üben habe ich ein kurzes Programm geschrieben, dass .tmp-Dateien und .tmp-Verzeichnisse löschen soll. Beim Löschen soll sowohl die Datei (bzw. das Verzeichnis) gelöscht werden, als auch das dazugehörende Objekt. Das scheint mit unten stehendem Programm auch zu funktionieren.

Code: Alles auswählen

#!/usr/bin/env python2

import logging, os

class path(object):
    count = 0
    def __init__(self, abspath):
        logging.debug('function: path.__init__(\'' + abspath + '\')')
        self.abspath = abspath
        if os.path.isfile(abspath):
            self.__type = 'file'
        elif os.path.isdir(abspath):
            self.__type = 'dir'
        else:
            self.__type = 'other'
        path.count +=1
    def __del__(self):
        logging.debug('function: path.__del__(\'' + self.abspath + '\')')
        if self.__type == 'file':
            os.remove(self.abspath)
            logging.debug('          os.remove(\'' + self.abspath + '\')')
        if self.__type == 'dir':
            os.rmdir(self.abspath)
            logging.debug('          os.rmdir(\'' + self.abspath + '\')')
        path.count -=1
    def __str__(self):
        logging.debug('function: path.__str__(\'' + self.abspath + '\')')
        return '(\'' + self.abspath + '\', \'' + self.__type + '\')'
    def settype(self, newtype):
        logging.debug('function: path.settype(\'' + self.abspath + '\': ' + newtype + ')')
        self.__type = newtype


# main

# create test scenario: empty file '/tmp/1.tmp' + empty dir '/tmp/2.tmp':
os.system('rm -rf /tmp/1.tmp && rm -rf /tmp/2.tmp && touch /tmp/1.tmp && mkdir /tmp/2.tmp && sync')

# define logging; possible values: CRITICAL, WARNING, INFO, DEBUG, NOTSET:
logging.basicConfig(format = "%(asctime)s: %(message)s", datefmt = "%Y-%m-%d %H:%M:%S", level=logging.DEBUG)

pathnamelist = ['/tmp/1.tmp', '/tmp/2.tmp'] # list of pathes to delete (files: always; dirs: if empty)

# build 'pathes' (= list of path objects):
pathes = []
for pathname in pathnamelist:
    pathes.append(path(pathname))

# print pathes:
#for onepath in pathes:
#    print str(onepath)

# delete all files and dirs in pathes
while len(pathes) > 0:
    logging.info('------------------------------------------------------------------------')
    logging.info('length of object list "pathes": ' + str(len(pathes)) + ', number of "path" objects: ' + str(path.count))
    del pathes[0]
    logging.info('length of object list "pathes": ' + str(len(pathes)) + ', number of "path" objects: ' + str(path.count))
Aber wenn ich mir vor dem Löschen (ab Zeile 53) einmal alle Objekte anzeigen lasse (= die Kommentarzeichen in Zeilen 50-51 entferne), erhalte ich zwei Meldungen, die mich irritieren:

1.: Die letzte mit "logging" ausgegebene Zeile lautet jetzt nicht mehr
2017-11-25 18:35:01: length of object list "pathes": 0, number of "path" objects: 0
, sondern
2017-11-25 18:35:01: length of object list "pathes": 0, number of "path" objects: 1
2.: Nach dieser Zeile kommt eine weitere Meldung, die ich nicht verstehe:
Exception AttributeError: "'NoneType' object has no attribute 'debug'" in <bound method path.__del__ of <__main__.path object at 0x7ff9ddb23b10>> ignored

Meine Fragen:

Warum werden beim Programmlauf ohne "print str(onepath)" alle Objekte der Klasse "path" gelöscht, während beim Programmlauf mit der "print str(onepath)"-Schleife ein Objekt übrig bleibt?

Und: Worauf bezieht sich der "Exception AttributeError"?


Vielen Dank für eure Hilfe!
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Bitte vergiss sofort wieder, das es die __del__ Methode in Python gibt. Diese Methode suggeriert einen destruktor wie man ihn von C++ kewnnt, ist es aber nicht. Es gibt KEINE Garantie vom Python Interpreter, ob und wann diese Methode aufgerufen wird. Und hierher kommt auch dein Problem. Du GLAUBST das Objekt wäre weg, ist es aber nicht. Später wird es abgeräumt, doch leider hat Python schon vorher andere Objekte aus dem Interpreter entfernt (wie zb Module und andere), womit es zu Fehlermeldungen kommt wie du sie beobachtest.

Wenn du verlässliche Semantik brauchst, musst du das ausprogrammieren. Zb mit ContextManagern (with-Statement) oder explizit.

Es gibt noch viele andere Anmerkungen zu deinem Code, ein paar davon:

- doppelte unterstriche sind in Python nicht üblich, weil sie nicht wirklich privatheit bedeuten. Sondern Namenskollisionen vermeiden sollen. Nicht benutzen, einfacher unterstrich genügt.
- statt + Benutze die format-Methode zum basteln von strings. Und os.path.join für Pfade.
- os.system sollte man nicht verwenden. Benuzte das subprocess Modul.
__deets__
User
Beiträge: 14545
Registriert: Mittwoch 14. Oktober 2015, 14:29

Und nich ein n Nachtrag: wenn du eine setter schreibst, machst du was falsch. Benuzte stattdessen direkten Attribut-Zugriff, oder wenn es sein MUSS ein property.
Sirius3
User
Beiträge: 18265
Registriert: Sonntag 21. Oktober 2012, 17:20

@Holger Chapman: so ganz habe ich den Sinn der Klasse nicht verstanden. Was für eine Funktion hat sie?

Zum Code: Schreibe jeden Import in eine eigene Zeile. Klassen werden per Konvention groß geschrieben. Es gibt vier verschiedene Arten von Anführungszeichen für literale Strings, daher ist es eigentlich nie nötig, Anführungszeichen zu escapen. Nimm einfach die anderen Anführungszeichen. Bei Logging nimmt man %-formatierung und übergibt die Variablen als weitere Parameter, dann spart man sich die Formatierung, wenn der Text gar nicht ausgegeben wird. Zu __del__, doppelten Unterstrichen und settype hat __deets__ ja schon alles gesagt.
Benutzeravatar
snafu
User
Beiträge: 6862
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Klassen zeichnen sich vor allem darin aus, dass sie Eigenschaften haben. Zusätzlich kann es Operationen geben, die diese Eigenschaften verändern. Eine Liste hat z.B. eine bestimmte Länge und bestimmte Elemente (wobei letzteres eigentlich keine Eigenschaft ist). Die Operationen sind Hinzufügen, Entfernen und Abfragen einzelner (oder mehrerer) Elemente. Anderes Beispiel: Eine Datei hat eine Größe und einen bestimmten Pfad. Als Operationen kann man Inhalt zur Datei hinzufügen oder auslesen, die Datei löschen, usw. So versteht an Objektorientierung normalerweise. Was du machst, ist weit von dieser Idee entfernt.

Die Idee mit del path ist übrigens schön gedacht, aber an der Stelle funktioniert Python, wie schon geschrieben wurde, anders als du denkst. Baue einfach eine delete()-Methode ein. Anstatt den Dateitypen als String zu setzen, könntest du einfach self.isdir als Eigenschaft einbauen und dort einen Wahrheitswert verwenden. Das versteht man besser, weil man sich nicht den exakten Text für den String merken muss und es ist bei zwei Möglichkeiten auch naheliegend.
Holger Chapman
User
Beiträge: 35
Registriert: Samstag 12. Juli 2014, 01:59

__deets__ hat geschrieben:Bitte vergiss sofort wieder, das es die __del__ Methode in Python gibt. Diese Methode suggeriert einen destruktor wie man ihn von C++ kewnnt, ist es aber nicht. Es gibt KEINE Garantie vom Python Interpreter, ob und wann diese Methode aufgerufen wird.
Danke für den Hinweis. Wenn ich weiß, dass ich ein Objekt nicht mehr brauche, lösche ich es also nicht ausdrücklich, sondern überlasse das Löschen dem Python-Interpreter (bzw. der Garbage Collection). Richtig?

Das heißt für mein Programm:
- ich streiche die "__del__"-Methode
- ich streiche die drei Zeilen mit "count", weil ich ohnehin nicht zuverlässig zählen kann, wie viele Instanzen von "path" es gibt
- ich definiere eine neue "delete_path"-Methode, in der ich a) die Datei/das Verzeichnis lösche und b) das Objekt (self) aus meiner Liste "pathes" lösche, in der alle Objekte drin sind, die ich noch brauche.
__deets__ hat geschrieben:Wenn du verlässliche Semantik brauchst, musst du das ausprogrammieren. Zb mit ContextManagern (with-Statement) oder explizit.
Das mit den ContextManagern habe ich noch nicht ganz verstanden. Die Suchmaschine führt mich zu PEP 343, aber das ist (für mich) nicht in fünf Minuten zu verstehen. - Werde ich mir morgen mal in Ruhe ansehen.

__deets__ hat geschrieben: - doppelte unterstriche sind in Python nicht üblich, weil sie nicht wirklich privatheit bedeuten. Sondern Namenskollisionen vermeiden sollen. Nicht benutzen, einfacher unterstrich genügt.
Okay. Ich hatte mich an http://gwise.itwelzel.biz/Books/Openboo ... 9c385b9ec3 orientiert.
__deets__ hat geschrieben: - statt + Benutze die format-Methode zum basteln von strings. Und os.path.join für Pfade.
Stimmt, damit wird's wohl übersichtlicher. Ich hab die String-Zusammensetzungen mit "+" umgestellt auf "format".
__deets__ hat geschrieben: - os.system sollte man nicht verwenden. Benuzte das subprocess Modul.
Das fertige Programm soll weder "os.system" noch "subprocess" benutzen. Ich habe "os.system" nur benutzt, um für Testzwecke schnell ("quick & dirty") die Dateien anzulegen, die ich zum Testen brauche. Das scheint mir leichter zu sein, als "subprocess" ... gerade weil ich nicht nur einen Befehl aufrufe, sondern gleich fünf.
__deets__ hat geschrieben:Und nich ein n Nachtrag: wenn du eine setter schreibst, machst du was falsch. Benuzte stattdessen direkten Attribut-Zugriff, oder wenn es sein MUSS ein property.
Okay, das wusste ich nicht. Ich hatte umgekehrt gedacht, der direkte Attribut-Zugriff sei verpönt und man solle eine Setter-Methode (ggf. mit Umweg über "property") schreiben. - Auch da habe ich versucht, mich an http://gwise.itwelzel.biz/Books/Openboo ... 9c385b9ec3 zu orientieren.

Danke erstmal für Deine Hilfe, ich habe mein Programm entsprechend überarbeitet (vgl. mein übernächster Post in diesem Thread).

Schönen Gruß


Holger
Holger Chapman
User
Beiträge: 35
Registriert: Samstag 12. Juli 2014, 01:59

Sirius3 hat geschrieben:@Holger Chapman: so ganz habe ich den Sinn der Klasse nicht verstanden. Was für eine Funktion hat sie?
Ich will ein Programm schreiben, das verschiedene Sachen (Löschen, Umbenennen, Dateigröße auslesen usw.) mit Einträgen im Dateisystem (Dateien, Verzeichnissen, Symlinks usw.) macht. Die Klasse "path" soll alle Attribute und Methoden beinhalten, die man mit jeder Art von Dateisystem-Einträgen machen kann. In einem zweiten Schritt will ich dann Unterklassen "file", "dir" und "symlink" anlegen, in denen dann Attribute/Methoden definiert sind, die man nur mit dieser Art von Dateisystem-Einträgen machen kann. ("symlink" soll zum Beispiel ein Attribut "targetstring" kennen.)
Vorerst geht es mir aber nur darum, zu verstehen bzw. auszuprobieren, wie man sowas objektorientiert schreibt.
Sirius3 hat geschrieben:Zum Code: Schreibe jeden Import in eine eigene Zeile. Klassen werden per Konvention groß geschrieben. Es gibt vier verschiedene Arten von Anführungszeichen für literale Strings, daher ist es eigentlich nie nötig, Anführungszeichen zu escapen. Nimm einfach die anderen Anführungszeichen. Bei Logging nimmt man %-formatierung und übergibt die Variablen als weitere Parameter, dann spart man sich die Formatierung, wenn der Text gar nicht ausgegeben wird. Zu __del__, doppelten Unterstrichen und settype hat __deets__ ja schon alles gesagt.
Ich hab mal versucht, das umzusetzen. Hoffentlich sieht's jetzt besser aus :-)

Danke für Deine Hinweise,
schönen Gruß


Holger
Holger Chapman
User
Beiträge: 35
Registriert: Samstag 12. Juli 2014, 01:59

Ich hoffe, so sieht es schon deutlich besser aus:

Code: Alles auswählen

#!/usr/bin/env python2

import logging
import os

class Path(object):
    def __init__(self, abspath):
        logging.debug("function: Path.__init__('{}')".format(abspath))
        self.abspath = abspath
        if os.path.isfile(abspath):
            self._type = 'file'
        elif os.path.isdir(abspath):
            self._type = 'dir'
        else:
            self._type = 'other'
    def __str__(self):
        logging.debug("function: Path.__str__('{}')".format(self.abspath))  
        return "('{}', '{}')".format(self.abspath, self._type)
    def delete_path(self):
        logging.debug("function: Path.delete_path('{}')".format(self.abspath))
        if self._type == 'file':
            os.remove(self.abspath)
            logging.debug("          os.remove('{}')".format(self.abspath))
        if self._type == 'dir':
            os.rmdir(self.abspath)
            logging.debug("          os.rmdir('{}')".format(self.abspath))
        pathes.remove(self)


# main

# create test scenario: empty file '/tmp/1.tmp' + empty dir '/tmp/2.tmp':
os.system('rm -rf /tmp/1.tmp && rm -rf /tmp/2.tmp && touch /tmp/1.tmp && mkdir /tmp/2.tmp && sync')

# define logging; possible values: CRITICAL, WARNING, INFO, DEBUG, NOTSET:
logging.basicConfig(format = "%(asctime)s: %(message)s", datefmt = "%Y-%m-%d %H:%M:%S", level=logging.DEBUG)

pathnamelist = ['/tmp/1.tmp', '/tmp/2.tmp'] # list of pathes to delete (files: always; dirs: if empty)

# build 'pathes' (= list of Path objects):
pathes = []
for pathname in pathnamelist:
    pathes.append(Path(pathname))

# print pathes:
for onepath in pathes:
    print str(onepath)

# delete all files and dirs in "pathes"
while len(pathes) > 0:
    logging.info('------------------------------------------------------------------------')
    logging.info('length of object list "pathes": {}'.format(len(pathes)))
    Path.delete_path(pathes[0])
    logging.info('length of object list "pathes": {}'.format(len(pathes)))
Holger Chapman
User
Beiträge: 35
Registriert: Samstag 12. Juli 2014, 01:59

snafu hat geschrieben:Anstatt den Dateitypen als String zu setzen, könntest du einfach self.isdir als Eigenschaft einbauen und dort einen Wahrheitswert verwenden. Das versteht man besser, weil man sich nicht den exakten Text für den String merken muss und es ist bei zwei Möglichkeiten auch naheliegend.
Ups. Die Setter-Methode mit dem Dateityp sollte eigentlich gar nicht hier im Forum landen. (Sie wird ja im Code auch gar nicht benutzt.) Da habe ich nur einmal die Syntax für eine Setter-Methode ausprobiert. - Inhaltlich macht es ja auch wenig Sinn, den Dateityp ändern zu wollen (wie macht man aus einer Datei ein Verzeichnis?).

Danke für Deine Hinweise!


Holger
Benutzeravatar
snafu
User
Beiträge: 6862
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Also man könnte es so schreiben:

Code: Alles auswählen

self.isdir = os.path.isdir(path)
Analog für Symlinks, usw.

Oder du schaust dir einfach mal Properties an:

Code: Alles auswählen

@property
def isdir(self):
    return os.path.isdir(self.abspath)
Damit machst du die Abfrage nur, wenn sie wirklich gebraucht wird. Außerdem deckt dies den Fall ab, wenn tatsächlich mal eine "normale" Datei zwischenzeitlich durch einen Symlink überschrieben wurde oder sowas. Die Abfrage ist also immer aktuell, d.h. sie spiegelt Änderungen im Dateisystem unmittelbar wider.
narpfel
User
Beiträge: 690
Registriert: Freitag 20. Oktober 2017, 16:10

@Holger Chapman: Ex sieht so aus, als würdest du gerade `pathlib` nachprogrammieren. Zum Lernen kann man das machen, aber um das danach zu benutzen macht das IMHO keinen Sinn.

Zum Galileo-Openbook ist vieles gesagt worden (nicht nur das hier), aber gutes war (sehr) wenig dabei. Die neueren Ausgaben scheinen besser zu sein, aber ich würde trotzdem zu anderer Literatur raten. Meist reicht allerdings auch die offizielle Dokumentation.

Zu Kontextmanagern (und PEPs): PEPs sind eher formelle Spezifikation (wie muss ich `with` in meinen Python-Interpreter einbauen) und keine Dokumentation (wie benutze ich `with`, um in meinem Python-Programm etwas sinnvolles zu tun). Die Dokumentation zu Kontextmanagern ist da vielleicht für den Einstieg verständlicher.
Holger Chapman
User
Beiträge: 35
Registriert: Samstag 12. Juli 2014, 01:59

narpfel hat geschrieben:Zum Galileo-Openbook ist vieles gesagt worden (nicht nur das hier), aber gutes war (sehr) wenig dabei.
Danke für den Hinweis. Mit den Kapiteln vor dem OOP-Kapitel war ich ganz zufrieden, das OOP-Kapitel hat mich doch etwas verwirrt.

Ich glaube, dass ich jetzt erstmal weiter komme.

Schönen Gruß


Holger
Antworten