2009-05-14 2 views
26

Mi chiedo se sia possibile creare un metodo che si comporta in modo diverso quando viene chiamato come metodo di classe rispetto a quando viene chiamato come metodo di istanza.Un metodo di classe che si comporta diversamente quando viene chiamato come metodo di istanza?

Ad esempio, come progetto di miglioramento delle capacità, sto scrivendo una classe Matrix (sì, so che ci sono classi matrix perfettamente buone già disponibili). Ho creato un metodo di classe per questo chiamato identity che restituisce una matrice di identità di una dimensione specificata.

Ora, quando viene richiamata l'istanza di Matrix, sembra logico che la dimensione non debba essere specificata; dovrebbe restituire una matrice di identità della stessa dimensione di Matrix chiamata.

In altre parole, mi piacerebbe definire un metodo che può determinare se è stato chiamato tramite un'istanza e, in tal caso, accedere agli attributi di quell'istanza. Sfortunatamente, anche dopo aver esplorato la documentazione e alcune ricerche su Google, non ho trovato nulla che suggerisca che ciò sia possibile. Qualcuno lo sa diversamente?

Edit:

Wow! Chiaramente, non sono ancora abbastanza abituato alle funzioni di prima classe. Ecco cosa ho ottenuto - grazie a Unknown per aver fornito la chiave!

class Foo(object): 
    def __init__(self, bar): 
     self.baz = bar 
     self.bar = MethodType(lambda self: self.__class__.bar(self.baz), self, self.__class__) 

    @classmethod 
    def bar(cls, baz): 
     return 5 * baz 

Foo.bar(3) # returns 15 

foo = Foo(7) 
foo.bar() # returns 35 

Edit 2:

Solo una breve nota - questa tecnica (e la maggior parte di quelli presentati qui di seguito) non funziona su classi che definiscono __slots__, in quanto non è possibile riassegnare il metodo.

+0

La domanda richiedeva un singolo oggetto chiamabile che potesse rilevare come veniva chiamato. (Questo dovrebbe essere possibile usando il modulo inspect per ottenere il frame di chiamata.) La 'soluzione' presentata è quella di creare un secondo callable, un metodo associato, che è associato a ogni istanza con lo stesso nome del primo callable. Questo potrebbe risolvere il problema di come avere classi e "metodi di istanza" con lo stesso nome. Ma non è la stessa cosa di avere un solo callable che può agire come entrambi. –

+0

@TerryJanReedy è possibile ispezionare il frame di chiamata in fase di esecuzione? – aayush

+0

Sì, questo è ciò che intendevo con "usa il modulo inspect per ottenere il frame di chiamata". Ci sono una o più funzioni per questo. –

risposta

38

Gli hack di Python discutibilmente utili sono il mio forte.

from types import * 

class Foo(object): 
    def __init__(self): 
     self.bar = methodize(bar, self) 
     self.baz = 999 

    @classmethod 
    def bar(cls, baz): 
     return 2 * baz 


def methodize(func, instance): 
    return MethodType(func, instance, instance.__class__) 

def bar(self): 
    return 4*self.baz 


>>> Foo.bar(5) 
10 
>>> a=Foo() 
>>> a.bar() 
3996 
+9

I kudos e un upvote, per lo sforzo nell'esempio di codice, e specialmente per la frase "gli hacker di Python discutibilmente utili sono il mio forte" :-) –

+0

@ Jarret, non è solo una citazione per lo show. Guarda i miei altri come: http://stackoverflow.com/questions/813882/is-there-a-way-to-check-whether-function-output-is-assigned-to-a-variable-in- pyth/813938 # 813938 – Unknown

+1

Ok, questo è bello. Ora, come lo metti sotto? – Neoecos

3

Penso che il problema più grande è che si sta sovraccaricando il nome "bar" sulla classe "Foo", qualcosa che Python non consente. La seconda definizione di "bar" contiene la prima definizione di "barra".

Prova a pensare a nomi univoci per il tuo metodo classmethod e istanza. Ad esempio

@classmethod 
def create(cls, baz): 
    ... 

def rubber_stamp(self): 
    ... 
+1

+1: E - ovviamente - il fattore di confusione intrinseco del comportamento della variante, anche se "strettamente correlato". –

+0

E il tuo suggerimento di fissare il "codice di concetto non funzionante" che l'OP ha già identificato come un grosso problema con cui ha bisogno di aiuto è? –

+2

IMO, il codice di concetto non funzionante è una cattiva idea - la soluzione è ripensare l'API. Non c'è "correzione". –

7

[modificato: uso attributo da una risposta più diretta; vedere il commento utili dai John Fouhy]

È possibile utilizzare un descrittore di fare ciò che si vuole:

class cls_or_inst_method(object): 
    def __init__(self, class_method, instance_method): 
     self.class_method = class_method 
     self.instance_method = instance_method 

    def __get__(self, obj, objtype): 
     if obj is None: 
      return self.class_method 
     else: 
      return lambda: self.instance_method(obj) 

def my_class_method(baz): 
    return baz + 1 

def my_instance_method(self): 
    return self.baz * 2 

class Foo(object): 
    baz = 10 
    bar = cls_or_inst_method(my_class_method, my_instance_method) 

Utilizzando quanto sopra:

>>> print Foo.bar(5) 
6 
>>> my_foo = Foo() 
>>> print my_foo.bar() 
20 
+1

Per essere una soluzione, il metodo di istanza dovrebbe fare riferimento a self.baz e non prendere alcun argomento (eccetto self), penso .. –

7

@Unknown Qual è la differenza tra le di e this:

class Foo(object): 

    def _bar(self, baz): 
     print "_bar, baz:", baz 

    def __init__(self, bar): 
     self.bar = self._bar 
     self.baz = bar 

    @classmethod 
    def bar(cls, baz): 
     print "bar, baz:", baz 

In [1]: import foo 

In [2]: f = foo.Foo(42) 

In [3]: f.bar(1) 
_bar, baz: 1 

In [4]: foo.Foo.bar(1) 
bar, baz: 1 

In [5]: f.__class__.bar(1) 
bar, baz: 1 
+0

Oh! Grazie a Mr. Unknown :-) – Ale

+1

Beh, la differenza è che puoi legare qualsiasi funzione tu voglia come metodo. Ho appena usato questa opportunità per mettere in mostra quel trucco. E anche che non c'è penzoloni f._bar() – Unknown

+4

@Unknown: se il "dangling" 'f._bar()' è un problema, basta definirlo usando una funzione lambda o nidificata all'interno di '__init __()' Questo il codice qui è intuitivamente ovvio come funziona, mentre l'altra soluzione coinvolge oscure voodoo Python, per non parlare delle funzioni di livello superiore visibili 'methodize()' e 'bar()' –

2

È possibile riassegnare il metodo di identità in init con funzione lambda breve:

class Matrix(object): 
    def __init__(self): 
     self.identity = lambda s=self:s.__class__.identity(s) 

     #...whatever initialization code you have... 
     self.size = 10   

    @classmethod 
    def identity(self, other): 
     #...always do you matrix calculations on 'other', not 'self'... 
     return other.size 


m = Matrix() 
print m.identity() 
print Matrix.identity(m) 

Se non hai familiarità con lambda, si crea una funzione anonima. È raramente necessario, ma può rendere il tuo codice più conciso. La riga lambda sopra potrebbe essere riscritta:

def identity(self): 
     self.__class__.indentity(self) 
    self.identity = identity