Dekoratoren: Unterschied zwischen Methode und Funktion?

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
Benutzeravatar
Defnull
User
Beiträge: 778
Registriert: Donnerstag 18. Juni 2009, 22:09
Wohnort: Göttingen
Kontaktdaten:

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?
Bottle: Micro Web Framework + Development Blog
BlackJack

@Defnull: Schon im `inspect`-Modul geschaut ob `ismethod()`, `ismethoddescriptor()`, oder `isfunction()` das gewünschte leisten?
Benutzeravatar
Defnull
User
Beiträge: 778
Registriert: Donnerstag 18. Juni 2009, 22:09
Wohnort: Göttingen
Kontaktdaten:

Kein Unterschied. Und nach dem `self` Parameter zu suchen halte ich für etwas unsicher.
Bottle: Micro Web Framework + Development Blog
Benutzeravatar
Defnull
User
Beiträge: 778
Registriert: Donnerstag 18. Juni 2009, 22:09
Wohnort: Göttingen
Kontaktdaten:

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
Bottle: Micro Web Framework + Development Blog
Benutzeravatar
snafu
User
Beiträge: 6740
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

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
Benutzeravatar
Defnull
User
Beiträge: 778
Registriert: Donnerstag 18. Juni 2009, 22:09
Wohnort: Göttingen
Kontaktdaten:

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
Bottle: Micro Web Framework + Development Blog
Benutzeravatar
Trundle
User
Beiträge: 591
Registriert: Dienstag 3. Juli 2007, 16:45

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.
"Der Dumme erwartet viel. Der Denkende sagt wenig." ("Herr Keuner" -- Bertolt Brecht)
Benutzeravatar
snafu
User
Beiträge: 6740
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Oder man wartet auf Python 5.3 8)
Benutzeravatar
snafu
User
Beiträge: 6740
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

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
ms4py
User
Beiträge: 1178
Registriert: Montag 19. Januar 2009, 09:37

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.
Benutzeravatar
Defnull
User
Beiträge: 778
Registriert: Donnerstag 18. Juni 2009, 22:09
Wohnort: Göttingen
Kontaktdaten:

Ich werde wohl oder übel zwei Dekoratoren nehmen. mroute() oder so. Blöd, das das nicht mit einem geht.
Bottle: Micro Web Framework + Development Blog
sma
User
Beiträge: 3018
Registriert: Montag 19. November 2007, 19:57
Wohnort: Kiel

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
Benutzeravatar
Defnull
User
Beiträge: 778
Registriert: Donnerstag 18. Juni 2009, 22:09
Wohnort: Göttingen
Kontaktdaten:

@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.
Bottle: Micro Web Framework + Development Blog
ms4py
User
Beiträge: 1178
Registriert: Montag 19. Januar 2009, 09:37

Defnull hat geschrieben:Genau das wollte ich. Danke :D
Und genau das wollte ich für bottle. Super! :)
Antworten