__getattr__ wird nur bei nicht vorhandenen attr aufgerufen?

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
mutetella
User
Beiträge: 1695
Registriert: Donnerstag 5. März 2009, 17:10
Kontaktdaten:

Hallo,

ich habe folgende Klasse gebastelt:

Code: Alles auswählen

class DatetimeArgs(object):
    '''This class is used to store datetime arguments.

    If a asked argument not exist or its boolean value is False 
    the default value which is the equivalent value of the 
    current datetime will be returned.

    Each argument can only set once otherwise a ValueError occurs.'''
    DATETIME_ARG_NAMES = ('year', 'month', 'day', 'hour', 'minute')
    DELTA_ARG_NAMES = ('years', 'months', 'days', 'hours', 'minutes')

    def __init__(self):
        self._defaults = dict(zip((self.DATETIME_ARG_NAMES), 
                                  datetime.datetime.now().timetuple()[0:5]) +
                              zip(self.DELTA_ARG_NAMES,
                                  (0, 0, 0, 0, 0)))

    def __setattr__(self, name, value):
        if not name in self.__dict__:
            self.__dict__[name] = value
        else:
            raise ValueError(ERR_REPEATED_DATETIME_ARG.format(name))
            
    def __getattr__(self, name):
        print '__getattr__ called'
        value = self.__dict__.get(name, self._defaults[name])
        return value or self._defaults[name]

    def get_dict(self, type_):
        type_ = (self.DELTA_ARG_NAMES if type_ == 'deltas' else
                 self.DATETIME_ARG_NAMES)
        return dict((name, getattr(self, name)) for name in type_)
Frage ich ein (noch) nicht vorhandenes Attribut ab, wird wie erwartet '__getattr__' aufgerufen und der jeweilige default-Wert zurückgegeben. Sobald ich allerdings ein Attribut setze, wird bei einer Abfrage desselben dieses zwar zurückgegeben, allerdings nicht über die '__getattr__'-Methode. Warum?

Code: Alles auswählen

>>> results = DatetimeArgs()
>>> results.day
__getattr__ called
14
>>> results.day = 15
>>> results.day
15
mutetella
Entspanne dich und wisse, dass es Zeit für alles gibt. (YogiTea Teebeutel Weisheit ;-) )
lunar

@mutetella: RTFM:
Called when an attribute lookup has not found the attribute in the usual places (i.e. it is not an instance attribute nor is it found in the class tree for self). name is the attribute name. This method should return the (computed) attribute value or raise an AttributeError exception.

Note that if the attribute is found through the normal mechanism, __getattr__() is not called. (This is an intentional asymmetry between __getattr__() and __setattr__().) This is done both for efficiency reasons and because otherwise __getattr__() would have no way to access other attributes of the instance. Note that at least for instance variables, you can fake total control by not inserting any values in the instance attribute dictionary (but instead inserting them in another object). See the __getattribute__() method below for a way to actually get total control over attribute access.
In dem Du das Attribut in ".__dict__" einfügst, wird es zum regulären Exemplarattribut, welches der Python-Interpreter unter Umgehung von "__getattr__()" direkt abfragt. DIe offensichtliche Lösung ist also wie beschrieben, das Attribut nicht in ".__dict__" abzulegen, sondern in einem anderen Exemplarattribut, i.e. "self._args[name] = value", wobei Du "self._args" natürlich in "__init__()" anlegen musst.

Ich sehe allerdings den Sinn dieser Klasse nicht. Wieso eine zusätzliche Indirektion, anstatt einfach gleich direkt "datetime" oder "timedelta"-Objekte zu erzeugen? Und selbst wenn, wieso überschreibst Du ".__getattr__()" und ".__setattr__()"?

Verzeihe mir, doch ich habe – mal wieder – das Gefühl, dass Du über Moskau nach Paris reisen möchtest…
mutetella
User
Beiträge: 1695
Registriert: Donnerstag 5. März 2009, 17:10
Kontaktdaten:

@lunar
Sorry für's nicht Lesen der Doku, ich war so von Fragezeichen umgeben dass ich erstmal um Hilfe rufen musste... :mrgreen:

Ich versuch' mich jetzt schon 'ne Weile an Folgendem:

Code: Alles auswählen

class DatetimeArgs(object):
    DATETIME_ARG_NAMES = ('year', 'month', 'day', 'hour', 'minute')
    DELTA_ARG_NAMES = ('years', 'months', 'days', 'hours', 'minutes')

    def __init__(self):
        self._defaults = dict(zip((self.DATETIME_ARG_NAMES), 
                                  datetime.datetime.now().timetuple()[0:5]) +
                              zip(self.DELTA_ARG_NAMES,
                                  (0, 0, 0, 0, 0)))

    def __setattr__(self, name, value):
        if not hasattr(object, name):
            object.__setattr__(self, name, value)
        else:
            raise ValueError(ERR_REPEATED_DATETIME_ARG.format(name))
            
    def __getattribute__(self, name):
        print '__getattribute__ called'
        try:
            return object.__getattribute__(self, name) or \
                    self._defaults[name]
        except AttributeError:
            return self._defaults[name]
Das Problem dabei ist, dass ich die Prüfung, ob ein Attribut bereits gesetzt ist, daran scheitert, dass beim Zugriff auf ein nicht vorhandenes Attribut der Fehler (auf den ja 'hasattr' reagiert) bereits in der '__getattribute__' behandelt wird. Ich steh' jetzt also vor einem "entweder oder"...

Diese Lösung...

Code: Alles auswählen

class DatetimeArgs(object):
    DATETIME_ARG_NAMES = ('year', 'month', 'day', 'hour', 'minute')
    DELTA_ARG_NAMES = ('years', 'months', 'days', 'hours', 'minutes')

    def __init__(self):
        self._defaults = dict(zip((self.DATETIME_ARG_NAMES), 
                                  datetime.datetime.now().timetuple()[0:5]) +
                              zip(self.DELTA_ARG_NAMES,
                                  (0, 0, 0, 0, 0)))
        self._assigned = {}

    def set(self, name, value):
        if name in self._assigned:
            raise ValueError(ERR_REPEATED_DATETIME_ARG.format(name))
        else:
            self._assigned[name] = value
            
    def get(self, name):
        print '__getattribute__ called'
        value = self._assigned.get(name, self._defaults[name])
        return value or self._assigned[name]
funktioniert natürlich, gefällt mir halt nicht so gut, weil 'get' und 'set' verwendet werden muss.

Ist es tatsächlich so, dass ich direkten Zugriff auf die Attribute so nicht regeln kann?

mutetella
Entspanne dich und wisse, dass es Zeit für alles gibt. (YogiTea Teebeutel Weisheit ;-) )
lunar

@mutetella

Code: Alles auswählen

class DatetimeArgs(object):
    #…
    DATETIME_ARG_NAMES = ('year', 'month', 'day', 'hour', 'minute')
    DELTA_ARG_NAMES = ('years', 'months', 'days', 'hours', 'minutes')

    def __init__(self):
        self._defaults = dict(zip((self.DATETIME_ARG_NAMES), 
                                  datetime.datetime.now().timetuple()[0:5]) +
                              zip(self.DELTA_ARG_NAMES,
                                  (0, 0, 0, 0, 0)))
        self._assigned = {}

    def __setattr__(self, name, value):
        if not name in self._assigned:
            self._assigned[name] = value
        else:
            raise ValueError(ERR_REPEATED_DATETIME_ARG.format(name))
            
    def __getattr__(self, name):
        return self._assigned.get(name, self._defaults[name])
Wie kommst Du überhaupt darauf, nun ".__getattribute__()" zu verwenden?! Weder habe ich Dir dazu geraten, noch war ist das die Lösung, von der die Dokumentation spricht.

Nebenbei bemerkt findet sich in Deinen Beiträgen folgender Quelltext:

Code: Alles auswählen

value = self._assigned.get(name, self._defaults[name])
return value or self._assigned[name]
Dieses Konstrukt ist mindestens komisch, und eigentlich sogar ziemlich falsch. Der rechte Operand in der zweiten Zeile wird nur ausgewertet, wenn "name in self._assigned" und "not self._assigned[name]" gilt, sprich wenn "name" auf einen "falschen" Wert wie "None" oder die leere Zeichenkette abgebildet wird. In diesem Fall aber gilt "value is self._assigned[name]" (da "value" in der ersten Zeile an den Inhalt von "self._assigned[name]" gebunden wird). Die zweite Zeile lautet dann also "value or value", was offensichtlich gleich "value" und noch dazu ziemlich sinnlos ist, da "value" ohnehin ein falscher Wert ist, so dass Du in diesem Fall auch gleich "None" zurückgegeben könntest.

Richtig falsch wird es, wenn "name not in self._assigned" gilt. Dann gilt ja "value is self._defaults[name]", da ".get()" ja auf das zweite Argument zurückfällt (es sei denn, es gibt schon vorher einen "KeyError", weil "name not in self._defaults"). Für jeden Schlüssel aus "self.DELTA_ARG_NAMES" aber ist "self._defaults[name] = 0". Die zweite Zeile lautet dann "0 or self._assigned[name]". Es wird also immer der rechte Operand ausgewertet. Der aber wirft dann immer einen "KeyError", da "name" ja nicht in "self._assigned" steht.

Was in Gottes Namen hast Du Dir dabei nur gedacht?!

Dein zweiter Beitrag erklärt im Übrigen noch immer nicht den eigentlichen Sinn und Zweck dieser Klasse. Warum übergibst Du die Argumente nicht einfach direkt an "datetime.datetime()" oder "datetime.timedelta()"? Wieso sammelst Du sie erst umständlich in einer separaten Klasse?
mutetella
User
Beiträge: 1695
Registriert: Donnerstag 5. März 2009, 17:10
Kontaktdaten:

lunar hat geschrieben:

Code: Alles auswählen

value = self._assigned.get(name, self._defaults[name])
return value or self._assigned[name]
Was in Gottes Namen hast Du Dir dabei nur gedacht?!
Oh Mann, sorry, da habe ich Code gepostet, den ich kurz darauf folgendermaßen korrigiert habe...:

Code: Alles auswählen

value = self._assigned.get(name, self._defaults[name])
return value or self._defaults[name]
1. Zeile: Wenn das Attribute noch nicht zugewiesen wurde, nimm den default-Wert
2. Zeile: Falls der zugewiesene Wert 0 oder '' ist, nimm ebenfalls den default-Wert. Sind beide Werte 0, spielt's ja keine Rolle.
lunar hat geschrieben:Wieso sammelst Du sie erst umständlich in einer separaten Klasse?
Hatte ich ursprünglich auch so gemacht. Allerdings ...
1. ... stolperte ich dann über den Umstand, dass, wenn z. B. in der Argumentenliste das year-Attribut doppelt vorkommt, das vorderste durch das hinterste überschrieben wird. Kann unter bestimmten Umständen an anderer Stelle eine Fehlermeldung verursachen, die so dann nicht mehr nachvollziehbar ist und ...
2. ... nicht gesetzte Argumente durch defaults ersetzt werden müssen.
Beides möchte ich nicht während des Parsens machen. Das mit einer Klasse zu lösen geisterte mir schon länger im Kopf herum. AWie kommst Du überhaupt darauf, nun ".__getattribute__()" zu verwenden?! Weder habe ich Dir dazu geraten, noch war ist das die Lösung, von der die Dokumentation spricht.ls ich dann im parser aus dem dateutils-Modul sah, dass auch dort eine Result-Klasse verwendet wird, machte ich mich an die Arbeit...
lunar hat geschrieben:Wie kommst Du überhaupt darauf, nun ".__getattribute__()" zu verwenden?! Weder habe ich Dir dazu geraten, noch war ist das die Lösung, von der die Dokumentation spricht.
Nun, dann hab' ich die Doku wohl falsch verstanden.
Jedenfalls wollte ich neben der jetzigen Lösung mit 'get'/'set' ausprobieren, ob das Überprüfen auf doppelte Attributszuweisung und automatische default-Bestückung nicht doch direkt geht.

mutetella
Entspanne dich und wisse, dass es Zeit für alles gibt. (YogiTea Teebeutel Weisheit ;-) )
lunar

mutetella hat geschrieben:

Code: Alles auswählen

value = self._assigned.get(name, self._defaults[name])
return value or self._defaults[name]

Code: Alles auswählen

return self._assigned.get(name) or self._defaults[name]
BlackJack

Wobei das nur funktioniert solange der Default immer 0 ist. Dann könnte man aber auch gleich ``return self._assigned.get(name, 0)`` schreiben.
lunar

@BlackJack Uhm, "self._assigned.get(name, 0)" ist nicht dasselbe wie "self._assigned.get(name) or self._defaults[name]". Warum der Standardwert da 0 sein müsste, erschließt sich mir ebenfalls nicht.
BlackJack

@lunar: Wenn man 0 zuweist wird beim Abfragen nicht die 0 sondern der Standardwert zurückgegeben. Dieses scheinbar clevere ``or`` um etwas komplizierteren Code zu sparen, fällt in der Regel auf die Nase wenn der linke Ausdruck auch im „pythonisch-boole'schen” Sinne falsch sein darf.

Angenommen der Minutenzeiger steht auf der 42ten Minute:

Code: Alles auswählen

da = DatetimeArgs()
print da.minute # -> 42
da.minute = 23
print da.minute # -> 23
da.minute = 0
print da.minute # -> 42
Nicht das was man haben möchte.
Sirius3
User
Beiträge: 18335
Registriert: Sonntag 21. Oktober 2012, 17:20

Hallo mutetella,

warum überhaupt eine __getattr__-Method?

Code: Alles auswählen

class DatetimeArgs(object):
    DATETIME_ARG_NAMES = ('year', 'month', 'day', 'hour', 'minute')
    DELTA_ARG_NAMES = ('years', 'months', 'days', 'hours', 'minutes')
    def __init__(self):
        self.__dict__.update(zip(self.DATETIME_ARG_NAMES,
                                  datetime.datetime.now().timetuple()))
        self.__dict__.update(zip(self.DELTA_ARG_NAMES, (0, 0, 0, 0, 0)))
        self.__dict__['_attr_set']=set()
    
    def __setattr__(self, name, value):
        if name in self._attr_set:
            raise ValueError(ERR_REPEATED_DATETIME_ARG.format(name))
        self._attr_set.add(name)
        object.__setattr__(self,name,value)
Grüße
Sirius
BlackJack

@Sirius3: Warum ``self.__dict__['_attr_set']=set()``? Das kann man doch direkter als ``self._attr_set = set()`` schreiben.
mutetella
User
Beiträge: 1695
Registriert: Donnerstag 5. März 2009, 17:10
Kontaktdaten:

BlackJack hat geschrieben:Das kann man doch direkter als ``self._attr_set = set()`` schreiben.
Dann ist '_attr_set' aber beim setzen von sich selbst in der '__setattr__' nicht vorhanden.
Entspanne dich und wisse, dass es Zeit für alles gibt. (YogiTea Teebeutel Weisheit ;-) )
lunar

@BlackJack Das ist mir klar, nur sehe ich das Problem nicht. mutetella hat dieses Verhalten explizit gewünscht:
mutetella hat geschrieben:Falls der zugewiesene Wert 0[...], nimm ebenfalls den default-Wert.
BlackJack

@lunar: Das hatte ich nicht gelesen. Und kann mir irgendwie nicht vorstellen warum man das möchte, dass ein völlig legitimer Wert für Minuten und Stunden nicht verwendet werden kann. Zumal das ohne deutliche Dokumentation sehr überraschend ist.
mutetella
User
Beiträge: 1695
Registriert: Donnerstag 5. März 2009, 17:10
Kontaktdaten:

BlackJack hat geschrieben:... warum man das möchte, dass ein völlig legitimer Wert für Minuten und Stunden nicht verwendet werden kann.
Das liegt daran, dass ich zum "schnell mal testen" immer nur ymd-Argumente verwendete. Inzwischen ist mir das natürlich um die Ohren geflogen... :)

Dank Eurer Hilfe (@Sirius3: Vielen Dank, das naheliegendste lag für mich mal wieder so fern... :wink: ) sieht das jetzt so aus:

Code: Alles auswählen

class DatetimeArgs(object):
    '''This class is used to store datetime and
    relativedelta arguments.

    If an asked argument 
      - not exist or 
      - its boolean value is False (except for datetime
        hour and/or minute argument for which a value 
        of zero is allowed)
    the default value will be returned. 

    The default value for 
      - a datetime argument is the equivalent value 
        of the current datetime
      - a relativedelta argument is zero.

    Each argument can only set once otherwise a ValueError occurs.'''
    DATETIME_ARG_NAMES = ('year', 'month', 'day', 'hour', 'minute')
    DELTA_ARG_NAMES = ('years', 'months', 'days', 'hours', 'minutes')

    def __init__(self):
        self.__dict__.update(zip(self.DATETIME_ARG_NAMES,
                                  datetime.datetime.now().timetuple()))
        self.__dict__.update(zip(self.DELTA_ARG_NAMES, (0, 0, 0, 0, 0)))
        self.__dict__['_attr_set'] = set()
    
    def __setattr__(self, name, value):
        if name in self._attr_set:
            raise ValueError(ERR_REPEATED_DATETIME_ARG.format(name))
        self._attr_set.add(name)
        if value or value == 0 and name in ('hour', 'minute'):
            object.__setattr__(self,name,value)

    def get_dict(self, type_):
        type_ = (self.DELTA_ARG_NAMES if type_ == 'deltas' else
                 self.DATETIME_ARG_NAMES)
        return dict(((name, getattr(self, name)) for name in type_))
Während des parsens übergebe ich die Ergebnisse somit an 'DatetimeArgs' und hole mir dann mit 'DatetimeArgs.get_dict()' die jeweiligen Argumente zum Bilden eines datetime- und eines relativedelta-Objekts.

Falls alles gut geht... :mrgreen:

mutetella
Entspanne dich und wisse, dass es Zeit für alles gibt. (YogiTea Teebeutel Weisheit ;-) )
Antworten