Seite 1 von 1

Dekoratoren: Unterschied zwischen Methode und Funktion?

Verfasst: Freitag 12. März 2010, 20:43
von Defnull
Folgendes Beispiel:

Code: Alles auswählen

def do_something(func):
    magic_happens_here(func)

class MyClass(object):
    @do_something
    def method(self):
        pass

@do_something
def function():
    pass
Wie kann ich in do_something() zwischen method und function sicher unterscheiden?

Verfasst: Freitag 12. März 2010, 20:50
von BlackJack
@Defnull: Schon im `inspect`-Modul geschaut ob `ismethod()`, `ismethoddescriptor()`, oder `isfunction()` das gewünschte leisten?

Verfasst: Freitag 12. März 2010, 20:55
von Defnull
Kein Unterschied. Und nach dem `self` Parameter zu suchen halte ich für etwas unsicher.

Verfasst: Freitag 12. März 2010, 21:10
von Defnull
Ich hab was, aber es ist dreckig:

Code: Alles auswählen

import inspect

def do_something(func):
    try:
        print 'Class method' if inspect.stack(1)[2][4][0].strip().startswith('class') else ''
    except IndexError:
        print 'No class method'
    return func

class MyClass(object):
    @do_something
    def method(self):
        pass

@do_something
def function():
    pass

Verfasst: Freitag 12. März 2010, 22:04
von snafu
Defnull hat geschrieben:Kein Unterschied.
Kann ich nicht so recht nachvollziehen, denn:

Code: Alles auswählen

>>> from inspect import isfunction, ismethod
>>> def foo(): pass
... 
>>> isfunction(foo)
True
>>> ismethod(foo)
False
>>> class Foo(object):
...     def bar(self): pass
... 
>>> foo = Foo()
>>> isfunction(foo.bar)
False
>>> ismethod(foo.bar)
True

Verfasst: Freitag 12. März 2010, 22:16
von Defnull
Du wendest `inspect.ismethod()` ja auch auf eine fertig gebundene Methode an. Ich muss es dagegen auf ein Funktionsobjekt innerhalb eines class-Blocks zur Definitionszeit anwenden können.

Code: Alles auswählen

>>> import inspect
>>> 
>>> def do_something(func):
...     print inspect.ismethod(func), inspect.isfunction(func)
...     return func
... 
>>> class MyClass(object):
...     @do_something
...     def method(self):
...         pass
... 
False True
>>> @do_something
... def function():
...     pass
... 
False True

Verfasst: Freitag 12. März 2010, 22:18
von Trundle
Die Sache ist, dass zu dem Zeitpunkt, in dem der Dekorator ausgeführt wird, das tatsächlich noch keine Methode ist. Das wird erst dann eine Methode, wenn die Klasse erstellt wird. Man kann es also (in schön) gar nicht feststellen.

Oder man benutzt eben so etwas, wie das zope.interface macht und hangelt sich den Stack hoch und macht damit dann magische Dinge.

Verfasst: Freitag 12. März 2010, 22:26
von snafu
Oder man wartet auf Python 5.3 8)

Verfasst: Freitag 12. März 2010, 22:36
von snafu
Wie wäre erstmal ein `@do_something(type='function')`, welches der Benutzer bei Bedarf auf `type='method'` stellt? Im Zuge von späteren Verbesserungen kann man da ja immer noch was dran machen. Bei dieser Stack-Lösung ist die Sache, dass es zum Einen hässlich ist und man zum Anderen ja nicht unbedingt wissen kann, ob das bei jeder Python-Version/-Implementierung so klappt.

Aber sicher kann man da `sys.version` oder so prüfen, um spezifischen Code zu schreiben... ;P

Verfasst: Freitag 12. März 2010, 22:49
von ms4py
snafu hat geschrieben:Wie wäre erstmal ein `@do_something(type='function')`, welches der Benutzer bei Bedarf auf `type='method'` stellt? Im Zuge von späteren Verbesserungen kann man da ja immer noch was dran machen. Bei dieser Stack-Lösung ist die Sache, dass es zum Einen hässlich ist und man zum Anderen ja nicht unbedingt wissen kann, ob das bei jeder Python-Version/-Implementierung so klappt.
Für die zwei Möglichkeiten reicht im Prinzip auch ein Bool 8)
Aber ansonsten finde ich das auch die sauberste Lösung.

Verfasst: Samstag 13. März 2010, 00:26
von Defnull
Ich werde wohl oder übel zwei Dekoratoren nehmen. mroute() oder so. Blöd, das das nicht mit einem geht.

Verfasst: Samstag 13. März 2010, 12:07
von sma
Ich würde empfehlen, die Klasse, in der du Methoden dekorieren willst, von einer Klasse mit eigener Metaklasse erben zu lassen, so das du dort das dict der Attribute nach dekorierten Funktionen durchsuchen kannst und jetzt dem Dekorator geeignet mitteilen kannst, dass er es später mit einer Methode zu tun hat (oder ihn einfach ersetzen).

Code: Alles auswählen

def deco(f):
    def art(*args):
        print("Ich dekoriere Funktion %s mit %d args" % (f, len(args)))
        return f(*args)
    art.f = f
    return art

@deco
def f(n):
    return n + 1

def method_deco(f):
    def method_art(*args):
        print("Ich dekoriere eine Methode %s mit %d args" % (f, len(args)))
        return f(*args)
    return method_art

class M(type):
    def __init__(self, name, bases, dct):
        super(M, self).__init__(name, bases, dct)
        for k, v in dct.items():
            if isinstance(v, type(deco)) and hasattr(v, "f"):
                setattr(self, k, method_deco(v.f))

class B(metaclass=M):
    pass

class C(B):
    @deco
    def m(self, n):
        return n - 1

print(f(3))
print(C.m(C(), 4))
print(C().m(5))
Stefan

Verfasst: Samstag 13. März 2010, 13:33
von Defnull
@sma: Metaklassen sind die Lösung! Dann muss ich nicht bis zur ersten Initialisierung, sondern nur bis zum Ende des Class-Blocks warten, bis ich die fälschlich angelegten Routen (ja, natürlich get es um Bottle) wieder entfernen kann. Du bist ein Genie :D

Also im Fall von Bottle:

Alle Funktionsobjekte werden von @route() registriert, ganz egal ob es sich um echte Funktionen oder um zukünftige Methoden handelt. Unterscheiden kann ich sie eh nicht. Gleichzeitig werden aber die dafür nötigen Parameter in ein Attribut der eben registrierten Callback-Funktion gespeichert (z.B. `func.bottle_hasroute = (route, options)`*).

Alle Klassen, für die das funktionieren soll, müssen von BaseApp erben. Die `MetaApp.__init__` Methode der Metaklasse von BaseApp wird aufgerufen, wenn z.b. mit `class MyApp(BaseClass)` eine neue App-Klasse definiert (nicht instantiiert) wurde. In `MetaApp.__init__` gehe ich dann alle Attribute von MyApp durch, sucht nach `.bottle_hasroute` und entferne die Route wieder aus dem globalen Routen-Register, und zwar bevor sie Schaden anrichten konnte. Das passiert nämlich sofort nach Abschluss des class-Blocks von MyApp.

Die `__new__` Methode der besagten BaseApp sucht nun wiederum nach diesem besonderen `.bottle_hasroute` Attributen in den Methoden der Instanz, die sie gerade erschaffen hat, und wendet den @route() Dekorator mit den gespeicherten Optionen erneut an, diesmal aber auf die gebundene Instanzmethode.

Für den Benutzer sieht das dann so aus:

Code: Alles auswählen

from bottle import route, BaseApp
class MyApp(BaseApp):
    @route('/')
    @route('/:name')
    def index(self, name="World"):
        return "Hello %s!" % name
Solange MyApp nicht instantiiert wird, gibt es keine Routen zu den Methoden. Man kann also das Modul, in dem MyApp definiert wird, problemlos importieren, ohne seltsame Nebeneffekte zu haben. Die fälschlich in Zeile 3 und 4 angelegten Routen sind in Zeile 7 schon wieder weg.

Genau das wollte ich. Danke :D


* bottle_hasroute ist natürlich dann später eine liste, da man ja mehr als eine Route pro Callback registrieren kann.

Verfasst: Samstag 13. März 2010, 14:48
von ms4py
Defnull hat geschrieben:Genau das wollte ich. Danke :D
Und genau das wollte ich für bottle. Super! :)