`__getattribute__` Rekursion umgehen?

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,

grundsätzlich ist mir klar, wie ich die "Rekursionsfalle" beim Überschreiben der `__getattribute__` umgehe:

Code: Alles auswählen

>>> class NoRecursion(object):
...     def __init__(self):
...         self.attribute = "Here I'am!"
...     def __getattribute__(self, name):
...         try:
...             return object.__getattribute__(self, name)
...         except AttributeError:
...             return 'Where are you?'
... 
>>> nr = NoRecursion()
>>> nr.attribute
"Here I'am!"
>>> nr.another
'Where are you?'
Wenn ich nun dasselbe auf ein Attribute eines Attributes anwenden möchte, weiß ich nicht weiter:

Code: Alles auswählen

>>> class NoRecursion(object):
...     def __init__(self):
...         self._datetime = datetime.datetime.now()
...         self.attribute = "Here I'am!"
...     def __getattribute__(self, name):
...         try:
...             return datetime.datetime.__getattribute__(self._datetime, name)
...         except AttributeError:
...             try:
...                 return object.__getattribute__(self, name)
...             except AttributeError:
...                 raise
...
Funktioniert natürlich nicht, da der Zugriff auf `self._datetime` wiederum auf `__getattribute__` zugreift... Wie müsste ein solches Gebilde aussehen? Letztlich möchte ich erreichen, dass `name` zuerst in `self._datetime` abgefragt wird und wenn dort nicht vorhanden in `self`.

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

@mutetella: Das ist eine eigenartige Konstruktion, warum willst Du so etwas denn überhaupt machen? Ich glaube ich habe auch noch gar nicht verstanden *was* Du da eigentlich machen willst? Vielleicht irgendwie zu viel Magie?
mutetella
User
Beiträge: 1695
Registriert: Donnerstag 5. März 2009, 17:10
Kontaktdaten:

@BlackJack
Ich habe eine eigene `DateTime` Klasse, die sich letzten Endes wie ein `datetime` Objekt verhalten soll allerdings zusätzlich noch ein paar Methoden und eine eigene `__format__` Methode hat. Ich möchte nicht von `datetime` erben, da ich beim Addieren und Subtrahieren wiederum keine `datetime`- sondern `DateTime`-Objekte zurückbekommen möchte. Bisher schaut das ganze so aus:

Code: Alles auswählen

class DateTime(object):
    def __init__(self, dt_values, dawn=None):
        '''__init__(dt_values[, dawn])
        __init__(DateTime)

        The given argument(s) must be either a tuple/list
        and/or `dawn` which must be True or False or a
        `DateTime` instance in which case a copy of the
        same will be created.

        If `dt_values` only has 3 elements for year, month
        and date the `dawn` flag must be given otherwise a
        ValueError occurs. This flag indicates whether the
        `DateTime` instance become time values of (0, 0) if
        `dawn` is True or (23, 59) if False. In both cases
        the `has_time` flag of the instance will be False.
        If `dt_values` has 5 elements this values will be
        used to create the datetime. Thereby no `dawn` is
        needed.
        If `dt_values` has more or less than 3 or 5 elements
        a ValueError occurs. '''
        if not isinstance(dt_values, DateTime):
            if len(dt_values) == 3:
                if dawn is None:
                    raise ValueError('Can\'t create a {!r} instance '
                                     'without time values and missing '
                                     '`dawn` flag.'.format(
                                         self.__class__.__name__))
                hour, minute = (0, 0) if dawn else (23, 59)
                self._datetime = datetime.datetime(*dt_values,
                                                   hour=hour, minute=minute)
                self.has_time = False
            elif len(dt_values) == 5:
                self._datetime = datetime.datetime(*dt_values)
                self.has_time = True
            else:
                raise ValueError('Wrong argument amount to create '
                                 '{0!r} instance: {1!r}'.format(
                                     self.__class__.__name__, dt_values))
        else:
            self._datetime = dt_values._datetime
            self.has_time = dt_values.has_time

    @property
    def hour(self):
        return self._datetime.hour

    @property
    def minute(self):
        return self._datetime.minute

    @property
    def day(self):
        return self._datetime.day

    @property
    def month(self):
        return self._datetime.month

    @property
    def year(self):
        return self._datetime.year

    @classmethod
    def now(cls, time=None):
        if time is None:
            return cls(*datetime.datetime.now().timetuple()[0:5])
        return cls(
            *datetime.datetime.now().timetuple()[0:3] +
            ((0, 0) if time == 'start' else (23, 59)),
            has_time = False
        )

    def replace(self, **kwargs):
        new = DateTime(self)
        new._datetime = new._datetime.replace(**kwargs)
        new.has_date = True
        return new

    def timetuple(self):
        return self._datetime.timetuple()

    def weekday(self):
        '''weekday() -> Bundle object

        Returns a bundle object which contains these attributes:
            `weekday`:  Weekday as number (0=Mo - 6=Su)
            `position`: The number of the weekday within
                        month (1 - 5)
            `is_last`:  Is it the last occurence of the
                        weekday within month (True/False) '''
        return utils.Bundle(
            position=((self.day - 1) / 7) + 1,
            is_last=(utils.month_len(self) - self.day) < 7,
            weekday=self._datetime.weekday()
        )

    def get_raws(self):
        return {'YmdHM': self.timetuple()[0:5], 'has_time': self.has_time}

    def _validate_delta(self, delta):
        if delta.seconds + delta.microseconds:
            raise NotImplementedError(
                'Counting with time values leading to non day exactly '
                'resolution currently not implemented.'
            )

    def __add__(self, delta):
        self._validate_delta(delta)
        new = DateTime(self)
        new._datetime = new._datetime + delta
        return new

    def __sub__(self, value):
        if isinstance(value, datetime.timedelta):
            self._validate_delta(value)
            new = DateTime(self)
            new._datetime = new._datetime - value
            return new
        return self._datetime - value._datetime

    def __lt__(self, other):
        return self._datetime < other._datetime

    def __le__(self, other):
        return self._datetime <= other._datetime

    def __gt__(self, other):
        return self._datetime > other._datetime

    def __ge__(self, other):
        return self._datetime >= other._datetime

    def __eq__(self, other):
        return self._datetime == other._datetime

    def __ne__(self, other):
        return self._datetime != other._datetime

    def __format__(self, spec):
        #Needed, cause datetime's strftime()-method
        #has the following year-restrictions:
        # <  Python 3.1 year >= 1900 is required
        # <= Python 3.2 year >= 1000 is required
        # >= Python 3.3 no restriction
        try:
            return format(self._datetime, spec)
        except ValueError:
            for directive, value in zip(
                (('%Y', '{:04d}'), ('%m', '{:02d}'), ('%d', '{:02d}'),
                 ('%H', '{:02d}'), ('%M', '{:02d}')),
                self._datetime.timetuple()[0:5]):
                spec = spec.replace(directive[0], directive[1].format(value))
            return spec

    def __hash__(self):
        return hash((self._datetime, self.has_time))

    def __str__(self):
        def_time = ('', '') if self.has_time else ('[', ']')
        return ('{0:%Y-%m-%d '
                '{def_time[0]}%H:%M{def_time[1]}}'.format(
                    self, def_time=def_time))

    def __repr__(self):
        def_time = ('', '') if self.has_time else ('[', ']')
        return ('{0}.{1}({2:%Y-%m-%d '
                '{def_time[0]}%H:%M{def_time[1]}})'.format(
                    self.__module__, self.__class__.__name__, self,
                    def_time=def_time))
Nun dachte ich mir eben, dass die ganzen `__eq__`, `__ne__`, `@property: year` etc. Definitionen wegfallen könnten, wenn ich einfach erstmal gewünschte Attribute auf `self._datetime` abfrage. Wenn eine Methode oder ein Attribut dort nicht gefunden wird, schaue ich auf `self` nach oder löse dann eine Exception aus.

mutetella
Entspanne dich und wisse, dass es Zeit für alles gibt. (YogiTea Teebeutel Weisheit ;-) )
Sirius3
User
Beiträge: 17754
Registriert: Sonntag 21. Oktober 2012, 17:20

@mutetella: explizit zu sagen, welche Attribute geerbt werden sollen, ist besser, als alles erstmal vom datetime-Objekt zu übernehmen. Da die Attribute ja nicht wirklich dynamisch sind. Spezialfunktionen __xxx__ müssen übrigens direkt in der jeweiligen Klasse implementiert sein, weil Python intern Optimierungen macht, die nur funktionieren, wenn Python auch weiß, welche Operatoren überschrieben sind.
mutetella
User
Beiträge: 1695
Registriert: Donnerstag 5. März 2009, 17:10
Kontaktdaten:

@Sirius3
Wenn ich Dich richtig verstanden habe, dann ist meine Lösung, sofern ich nicht von `datetime.datetime` erben lasse, soweit in Ordnung und ich vergesse die `__getattribute__` Geschichte wieder?

Andererseits wollte ich ja nur deshalb nicht von `datetime.datetime` erben, weil ich nicht wusste, wie ich es dabei anstellen könnte, bei Methoden, die ein neues `datetime.datetime` Objekt zurückgeben, dieses durch ein neues `DateTime` Objekt zu ersetzen. Nun habe ich heute folgende Lösung gefunden, die eben dieses Problem löst:

Code: Alles auswählen

class DateTime(datetime.datetime):
    def __new__(cls, dt_values):
        if not isinstance(dt_values, DateTime):
            return datetime.datetime.__new__(cls, *dt_values)
        else:
            return datetime.datetime.__new__(cls, *dt_values.timetuple()[0:5])

    def replace(self, **kwargs):
        new = super(DateTime, self).replace(**kwargs)
        return DateTime(new.timetuple()[0:5])

    def _validate_delta(self, delta):
        if delta.seconds + delta.microseconds:
            raise NotImplementedError(
                'Counting with time values leading to non day exactly '
                'resolution currently not implemented.'
            )

    def __add__(self, delta):
        self._validate_delta(delta)
        new = super(DateTime, self).__add__(delta)
        return DateTime(new.timetuple()[0:5])

    def __radd__(self, delta):
        return self.__add__(delta)

    def __sub__(self, value):
        new = super(DateTime, self).__sub__(value)
        if isinstance(value, datetime.timedelta):
            self._validate_delta(value)
            return DateTime(new.timetuple()[0:5])
        return new

    def __rsub__(self, value):
        return self.__sub__(value)

    def __format__(self, spec):
        #Needed, cause datetime's strftime()-method
        #has the following year-restrictions:
        # <  Python 3.1 year >= 1900 is required
        # <= Python 3.2 year >= 1000 is required
        # >= Python 3.3 no restriction
        try:
            return format(self._datetime, spec)
        except ValueError:
            for directive, value in zip(
                (('%Y', '{:04d}'), ('%m', '{:02d}'), ('%d', '{:02d}'),
                 ('%H', '{:02d}'), ('%M', '{:02d}')),
                self._datetime.timetuple()[0:5]):
                spec = spec.replace(directive[0], directive[1].format(value))
            return spec
Was haltet ihr davon? Damit müsste ich nur die Methoden überschreiben bzw. neu schreiben, die ich auch tatsächlich berühren möchte.

Was ich halt leider immer noch nicht so wirklich verstehe, ist diese `super()` Sache... :?

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

@mutetella: Ich würde von der `super()`-Sache einfach die Finger lassen. Beispiel `replace()` (ungetestet):

Code: Alles auswählen

    def replace(self, **kwargs):
        return DateTime(datetime.datetime.replace(self, **kwargs))
Während man `__radd__()` so implementieren kann, weil die Addition kommutativ ist, dürfte das bei `__rsub__()` nicht gehen die Operanden zu vertauschen. ;-)
mutetella
User
Beiträge: 1695
Registriert: Donnerstag 5. März 2009, 17:10
Kontaktdaten:

@BlackJack
``17 - 10`` ist natürlich etwas anderes als ``10 - 17``, aber weshalb funktioniert das dann:

Code: Alles auswählen

    def __sub__(self, value):
        if isinstance(value, datetime.timedelta):
            self._validate_delta(value)
            return DateTime(
                datetime.datetime.__sub__(self, value).timetuple()[0:5]
            )
        return datetime.datetime.__sub__(self, value)

    def __rsub__(self, value):
        return self.__sub__(value)

Code: Alles auswählen

In [84]: d = dt.DateTime((2014, 4, 17))

In [85]: d1 = dt.DateTime((2014, 4, 10))

In [86]: d - d1
Out[86]: datetime.timedelta(7)

In [87]: d1 - d
Out[87]: datetime.timedelta(-7)
Wenn ich das umschreibe, passiert nichts anderes, außer dass die nicht mögliche Variante ``timedelta - datetime`` nun auch einen Fehler wirft:

Code: Alles auswählen

    def __sub__(self, value):
        if isinstance(value, datetime.timedelta):
            self._validate_delta(value)
            return DateTime(
                datetime.datetime.__sub__(self, value).timetuple()[0:5]
            )
        return datetime.datetime.__sub__(self, value)

    def __rsub__(self, value):
        if isinstance(value, datetime.timedelta):
            # must raise a `TypeError`!
            datetime.datetime.__rsub__(self, value)
        return datetime.datetime.__rsub__(self, value)

Code: Alles auswählen

In [111]: d = dt.DateTime((2014, 4, 17))

In [112]: d1 = dt.DateTime((2014, 4, 10))

In [113]: d - d1
Out[113]: datetime.timedelta(7)

In [114]: d1 - d
Out[114]: datetime.timedelta(-7)

In [115]: d - _113
Out[115]: DateTime(2014, 4, 10, 0, 0)

In [116]: _113 - d
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
<ipython-input-116-94a4866e422d> in <module>()
----> 1 _113 - d

TypeError: unsupported operand type(s) for -: 'datetime.timedelta' and 'DateTime'
Weshalb also funktionieren beide Varianten, abgesehen vom fehlenden `TypeError` in Variant 1?
BlackJack hat geschrieben:Ich würde von der `super()`-Sache einfach die Finger lassen.
Ich habe inzwischen ein paar Artikel über `super()` gelesen und versucht, diese zu verstehen. Mein Fazit: Ich nehme Deinen Rat an, auch wenn ich keine Ahnung habe, was es mit dieser `super()`-Sache eigentlich wirklich auf sich hat... :roll:

mutetella
Entspanne dich und wisse, dass es Zeit für alles gibt. (YogiTea Teebeutel Weisheit ;-) )
mutetella
User
Beiträge: 1695
Registriert: Donnerstag 5. März 2009, 17:10
Kontaktdaten:

Also eigentlich kann ich die `__rsub__()` doch komplett weglassen. Die wird ja eh nur dann aufgerufen, wenn verschiedene Typen subtrahiert werden und dann wiederum nur, wenn meine `DateTime` rechts steht, was nicht erlaubt ist. Und diesen `TypeError` kann dann auch die `timedelta.__sub__` werfen.

Ist das, was ich schreibe, so richtig?

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