Python2 - Variablename entscheidet über AttributeError - hä?

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
Astorek
User
Beiträge: 72
Registriert: Samstag 24. Januar 2009, 15:06
Kontaktdaten:

Hi @ all,

nach längerer Python-Abstinenz wollte ich mal wieder ganz langsam anfangen und stehe hier vor einem Problem, bei dem ich das Gefühl habe, Python will mich verarschen - oder ich habe zufällig irgendeinen Bug gefunden. Anders kann ich mir das Verhalten von Python nicht erklären.

Ich habe hier folgenden Code rumliegen:

Code: Alles auswählen

class Karte:
    anzahl = 0
    def __init__(self):
        Karte.anzahl += 1

    def __del__(self):
        Karte.anzahl -= 1

    def __str__(self):
        return str(Karte.anzahl)


card = Karte()
print card
Wenn ich diesen Code in eine Datei schreibe und ihn mit Python2 ausführe, erhalte ich die Fehlermeldung:

Code: Alles auswählen

Exception AttributeError: "'NoneType' object has no attribute 'anzahl'" in <bound method Karte.__del__ of <__main__.Karte instance at 0x7f75d282e5f0>> ignored
Wenn ich hingegen die letzten 2 Zeilen ändere, und zwar stattdessen in

Code: Alles auswählen

test2 = Karte()
print test2
Gibt es garkeine(!) Fehlermeldung.

Was zum Teufel ist da los? Wie kann es sein, dass der Variablenname "card" diesen seltsamen Fehler wirft, nicht aber der Variablenname "test2"? Nur durch Zufall habe ich herausgefunden, dass "test2" keinen Fehler wirft - wenn die Variable stattdessen z.B. "bliblablubb1" nehme, bekomme ich wieder den AttributeError, aber bei "bliblablubb15" (eine 5 hintendran) hingegen wieder nicht...

Auch interessant: Wenn ich die Methode "__del__" aus der Klasse entferne, erhalte ich diesen Fehler nicht - unabhängig vom Variablennamen. Leider bin ich auf die "__del__"-Methode angewiesen, da ich jederzeit sehen muss, wie viele Variablen die Klasse bereits nutz(t)en...

Tjoar... Frage: Bekommt ihr zufällig denselben Fehler, wenn ihr diesen Schnipsel-Code ausführt? Falls ja: Woran könnte es liegen? Ich habe leider keinen Zweit-PC oder -Betriebssystem zur Hand, auf dem ich das gerade testen könnte...

Schonmal im Voraus Danke für jede Hilfe! :) Verwendete Python-Version ist 2.7.2 unter Archlinux 64 bit; Python wurde direkt als Paket vom Distributor installiert...

EDIT: Es wäre auch super, wenn sich jemand melden würde, bei dem der Fehler NICHT auftritt. Dann weiß ich, dass mit meinem Computer irgendwas nicht stimmt^^.
deets

Das Verhalten ist bei mir gleich, und ein Beleg dafuer, dass man __del__ nicht benutzen sollte.

Funktionieren tut es wenn du es so programmierst:

Code: Alles auswählen

class Karte:
    anzahl = 0
    def __init__(self):
        self.__class__.anzahl += 1

    def __del__(self):
        self.__class__.anzahl -= 1

    def __str__(self):
        return str(self.__class__.anzahl)


card = Karte()
print card
Was passiert ist recht simpel - Python versucht, das __main__-Modul zu entfernen. Dabei setzt es dann alle darin bekannten Namen auf "None", um deren Objekte garbage collecten zu lassen. Und dein Namensgebung hat durch Zufall nun einen Fall erzeugt, wo einmal zuerst Karte, und danach card, und beim anderen mal zuerst test2, und dann Karte auf None gesetzt werden. Je nach dem was zuerst passiert, kommt es zu dem Fehler.

Die self.__class__-Referenz hingegen hat bestand.

Ich wuerde mal sagen, du kommst auch ohne __del__ aus, in all meinen Jahren als Python-Programmierer habe ich das eigentlich nie wirklich gebraucht. Was ist denn dein Anwendungsfall?
DasIch
User
Beiträge: 2718
Registriert: Montag 19. Mai 2008, 04:21
Wohnort: Berlin

__del__ ist kein Destruktor. Es ist nicht definiert wann oder ob die Methode aufgerufen wird. Instanzen, von Klassen die __del__ definieren, werden vom Garbage Collector der CPython für zirkuläre Referenzen verwendet nicht aufgeräumt und können so zu Memory Leaks führen.

Das Problem in deinem Fall, ist dass bei dem Aufruf von __del__ das Attribut `anzahl` schon nicht mehr existiert, dies führt zu dem AttributeError.

Wie deets gezeigt hat lässt sich das beheben wenn man ein Attribut mit einem Unterstrich prefixt, in diesem Fall wird ein Attribut erst nach der Ausführung von __del__ - sofern sie überhaupt statt findet - aufgeräumt wird.

Unabhängig davon ist dein Design allerdings prinzipiell problematisch, eine Klasse sollte eine Instanz ausschliesslich beschreiben, mit Instanzen sollte sie nicht agieren und nach ihrer Definition sollte man Veränderungen prinzipiell vermeiden.

Wenn du wissen willst wieviele Exemplare einer Klasse existieren, verwende eine geeignete Datenstruktur um diese zu erfassen und zu zählen.
Astorek
User
Beiträge: 72
Registriert: Samstag 24. Januar 2009, 15:06
Kontaktdaten:

Danke für die Aufklärung deets :) . Leuchtet alles ein, was du gesagt hast... Mir war nicht bewusst, dass die __del__-Methode solche zufällige Konstellationen erzeugen konnte. Das werde ich mir merken^^.
deets hat geschrieben:Ich wuerde mal sagen, du kommst auch ohne __del__ aus, in all meinen Jahren als Python-Programmierer habe ich das eigentlich nie wirklich gebraucht. Was ist denn dein Anwendungsfall?
Offen gesagt, war das nur mehr oder weniger eine Spielerei, die ich aus dem Tutorial A Byte of Python aufgeschnappt habe. Ich wollte eine Art "Universelle Kartenspiel-Engine" schreiben, bei der es auch die Möglichkeit gibt, Karten direkt "verschwinden" zu lassen - und das wollte ich mir eigentlich ganz einfach mit der __del__-Methode machen^^. Ich finde da schon was, wie ich das anders lösen werde ;)

EDIT:
@DasIch: Danke für die Aufklärung... Ich denke, ich werde mir noch anderweitig ein paar Tutorials ansehen, da ich, was Klassen angeht, noch relativ unerfahren bin^^...
deets

DasIch hat geschrieben: Wie deets gezeigt hat lässt sich das beheben wenn man ein Attribut mit einem Unterstrich prefixt, in diesem Fall wird ein Attribut erst nach der Ausführung von __del__ - sofern sie überhaupt statt findet - aufgeräumt wird.
Habe ich? Was hat denn ein Unterstrich vor einem Attribut damit zu tun? Was ich gezeigt habe ist, dass eine Instanz immer eine Referenz auf ihre Klasse hat. Das hat nix mit dem Attribut zu tun. Was veraendert wurde ist der globale Namensraum, auf den sich "Karte" bezieht.
BlackJack

@Astorek: Was meinst Du mit „verschwinden lassen“? Die `__del__()`-Methode lässt nichts verschwinden, sondern wird (vielleicht) aufgerufen wenn das Exemplar von selbst „verschwindet“, also wenn das Objekt vom Programm aus nicht mehr erreichbar ist. Zu dem Zeitpunkt ist das Objekt für das Programm also schon weg — da ist kein heran kommen mehr. Und das ist unabhängig davon ob die Methode aufgerufen wird oder nicht.
Leonidas
Python-Forum Veteran
Beiträge: 16025
Registriert: Freitag 20. Juni 2003, 16:30
Kontaktdaten:

@BlackJack: ich gehe davon aus, dass der OP eine Art Kartenspiel programmiert und in der __del__ Methode die Karte aus der UI rausnehmen will. Und das will er dann mit ``del karte`` erreichen, so dass die Karte dann aus der GUI verschwindet.

Ist aber keine gute Lösung, eigentlich nichtmal eine "Lösung", weil es nicht funktioniert.
My god, it's full of CARs! | Leonidasvoice vs (former) Modvoice
Astorek
User
Beiträge: 72
Registriert: Samstag 24. Januar 2009, 15:06
Kontaktdaten:

Leonidas hat recht damit, was ich mit der __del__Methode ursprünglich erreichen wollte. Der Code, den ich hier reingestellt habe, habe ich zuvor bis auf die wichtigsten Stellen gekürzt.

---

Bei der Gelegenheit würde ich ehrlichgesagt gerne ein paar Meinungen über "meine Methode" beim Erstellen eines Kartenspieles einholen^^. Wie gesagt, ich habe noch wenig praktische Erfahrung beim Programmieren in Klassen und bin mir nicht sicher, ob die Verwendung von Klassen so gedacht ist, wie ich sie mir vorgestellt hätte.

Bis jetzt hätte ich mir mein Kartenspiel so vorgestellt:
- Eine Klasse "Karte" nimmt sämtliche Werte auf, die eine einzelne(!) Karte hat, z.B. Farbe und Wertigkeit.
- Die ganzen Karten werden innerhalb der main() in einer List gespeichert, nach dem Motto:

Code: Alles auswählen

i = 0
for j in "yellow", "magenta", "green", "orange", "yellow":
    for k in range(12):
        cards[i] = Karte(color = j, number = k + 1)
        i += 1
Daneben gäbe es noch die Klassen "Kartendeck" (symbolisiert quasi die Karten, die man während des Spiels abheben kann), "Ablage" (die Karten, die man ausgespielt hat) und "Spieler". Der Transfer von Karten würde immer innerhalb der main() und mit Verwendung der List "cards" stattfinden... Ich hätte mir dabei gedacht, dass ich mir dabei den Transfer "realistisch" vorstellen kann: Karten werden vom Kartendeck genommen, bekommt Spieler XY oder landet in der Ablage etc. , Bei Spielende legen alle Spieler ihre Karten wieder zum Kartendeck dazu etc...

Ist das eine sinnvolle Verwendung von Klassen, bzw. gibts in der Art des Gedankengangs Optimierungen? Oder habe ich den Nutzen von Klassen ganz falsch verstanden?^^
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

Du solltest Dir mal ``enumerate`` angucken. Indizes für Iterables erzeugt man am besten damit. Könntest Du zweimal einsetzen, so weit ich das auf die schnelle gesehen habe.

Edit: Das erste kannst Du Dir auch schenken. `list.append` existiert ja.

So, hier mal die ganz elegante Variante:

Code: Alles auswählen

from itertools import product

class Card:
    def __init__(self, color, number):
        self.color, self.number = color, number

cards = [Card(*item) for item in product(("yellow", "magenta", "green"), range(1, 13))]
encoding_kapiert = all(verstehen(lesen(info)) for info in (Leonidas Folien, Blog, Folien & Text inkl. Python3, utf-8 everywhere))
assert encoding_kapiert
Benutzeravatar
snafu
User
Beiträge: 6731
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

DasIch hat geschrieben:Wie deets gezeigt hat lässt sich das beheben wenn man ein Attribut mit einem Unterstrich prefixt, in diesem Fall wird ein Attribut erst nach der Ausführung von __del__ - sofern sie überhaupt statt findet - aufgeräumt wird.
Ist das dokumentiert oder geraten? Höre das nämlich in Zusammenhang mit `__del__` zum ersten Mal. Aber man lernt ja nie aus...
deets

@snafu

Ich habe das nicht gezeigt... und ich halte es auch fuer falsch. Python hat ja gar kein Attribut entfernt/auf None gesetzt. Sondern modul-globale Namen entfernt.
Benutzeravatar
snafu
User
Beiträge: 6731
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

@deets: Das hatte ich schon verstanden. Du hattest dich ja danach auch von der dir zugeschriebenen Aussage distanziert. Die Verallgemeinerung hatte ja DasIch gebracht. Es mag ja sein, dass (C)Python eine bestimmte Reihenfolge bei der Abarbeitung bzw insbesondere beim Abräumen der verschiedenen Attribut"arten" geht. Ich wüsste aber halt nicht, dass diese Vorgehensweise "offiziell" wäre (sofern sie denn überhaupt existiert). Ich mein, im Prinzip kann man das ja ganz einfach testen. Ich fragte mich halt nur, wie DasIch zu seiner Feststellung gekommen ist.
derdon
User
Beiträge: 1316
Registriert: Freitag 24. Oktober 2008, 14:32

Hyperion hat geschrieben:So, hier mal die ganz elegante Variante:

Code: Alles auswählen

from itertools import product

class Card:
    def __init__(self, color, number):
        self.color, self.number = color, number

cards = [Card(*item) for item in product(("yellow", "magenta", "green"), range(1, 13))]
Ganz elegant heißt mit collections.namedtuple ;)
Darii
User
Beiträge: 1177
Registriert: Donnerstag 29. November 2007, 17:02

snafu hat geschrieben:@deets: Das hatte ich schon verstanden. Du hattest dich ja danach auch von der dir zugeschriebenen Aussage distanziert. Die Verallgemeinerung hatte ja DasIch gebracht. Es mag ja sein, dass (C)Python eine bestimmte Reihenfolge bei der Abarbeitung bzw insbesondere beim Abräumen der verschiedenen Attribut"arten" geht.
Nein das ist unsinn das tut Python nicht, deswegen steht das auch nicht in der Doku. Zwar wird Python sicherlich Referenzen in der Reihenfolge in der sie im betreffenden dict stehen löschen, deswegen klappt das Umbenennen im Beispiel auch. Aber Umbennen von Variablen ist keine richtige Lösung weil man sich darauf nicht verlassen darf. Die Lösung ist keine Referenzen außer self in __del__ zu verwenden.

Und die eigentlich Lösung ist __del__ nur zu verwenden, wenn man nicht von Python verwaltete Ressourcen freigeben muss. Was aber auf bedeutet, dass man ggf. alle Funktionen die man dafür braucht, in der Klasse zwischenspeichern muss.
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

derdon hat geschrieben: Ganz elegant heißt mit collections.namedtuple ;)
Nee nee, "ganz" ist schon meine... "super" elegant wäre Deine Lösung mit ``namedtuple``, die Du uns ja gleich zeigen wirst ;-)
encoding_kapiert = all(verstehen(lesen(info)) for info in (Leonidas Folien, Blog, Folien & Text inkl. Python3, utf-8 everywhere))
assert encoding_kapiert
lunar

@snafu: CPython garantiert, dass globale Namen, welche mit einem Unterstrich beginnen, vor globalen Namen gelöscht werden, die keinen führenden Unterstrich haben. Aus der Dokumentation:
Warning: […]Starting with version 1.5, Python guarantees that globals whose name begins with a single underscore are deleted from their module before other globals are deleted; […]
In diesem Punkt hat der Unterstrich im Bezug auf "__del__()" also tatsächlich eine besondere Bedeutung. Ich glaube, DasIch hat da einfach was verwechselt, oder falsch in Erinnerung behalten.
derdon
User
Beiträge: 1316
Registriert: Freitag 24. Oktober 2008, 14:32

Hyperion hat geschrieben:
derdon hat geschrieben: Ganz elegant heißt mit collections.namedtuple ;)
Nee nee, "ganz" ist schon meine... "super" elegant wäre Deine Lösung mit ``namedtuple``, die Du uns ja gleich zeigen wirst ;-)
Einfach nur die Definition von Card anpassen:

Code: Alles auswählen

>>> from collections import namedtuple
>>> Card = namedtuple('Card', 'color number')
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

derdon hat geschrieben: Einfach nur die Definition von Card anpassen:

Code: Alles auswählen

>>> from collections import namedtuple
>>> Card = namedtuple('Card', 'color number')
Ja, ganz nett. Aber darum ging es mir ja nicht - wie er seinen Datentypen letztlich aufbaut ist Sache des OP. Je nach Anwendungsfall mag eine eigene Klasse auch sinnvoller sein als Deine Idee.
encoding_kapiert = all(verstehen(lesen(info)) for info in (Leonidas Folien, Blog, Folien & Text inkl. Python3, utf-8 everywhere))
assert encoding_kapiert
Antworten