knifflige introspection

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
droptix
User
Beiträge: 521
Registriert: Donnerstag 13. Oktober 2005, 21:27

Ich übergebe im Konstruktor einer Klasse eine Funktion wie unten zu sehen. Die Funktion wird durch eine andere Klassenmethode aufgerufen. Die aufgerufene Funktion soll die Instanz der ihr zugewiesenen Klasse kennen. Geht das, ohne dass ich `self` als Parameter explizit an die Funktion beim Aufruf übergebe, also *nicht* so: `self.func(self)`?

Code, der scheitert, aber das Prinzip zeigt:

Code: Alles auswählen

class Spam:
    def __init__(self, func):
        self.func = func
        
    def do(self):
        self.func()

def main():
    def eggs():
        print "name of my class instance:"
        print self # results in an error
    spam = Spam(eggs)
    spam.do()

if __name__ == "__main__":
    main()
EyDu
User
Beiträge: 4881
Registriert: Donnerstag 20. Juli 2006, 23:06
Wohnort: Berlin

Was willst du denn konkret machen, dass du so etwas brauchst? Momentan sieht dein Code einfach nach einem Designfehler aus.
Das Leben ist wie ein Tennisball.
Sirius3
User
Beiträge: 17738
Registriert: Sonntag 21. Oktober 2012, 17:20

@droptix: was stört Dich an self.func(self)?
BlackJack

@droptix: Bei Methoden gibt es in Python schon kein magisch existierendes `self` sondern man muss es explizit in der Methodensignatur auflisten, und jetzt möchtest Du plötzlich eine magische Variable in *Funktionen*, die an das Objekt gebunden ist von dem die Funktion zuletzt abgerufen wurde‽

Man könnte in der `__init__()` eine Funktion erstellen welche die übergebene `func` mit `self` als erstem Argument aufruft:

Code: Alles auswählen

#!/usr/bin/env python
from functools import partial


class Spam:
    def __init__(self, func):
        self.func = partial(func, self)
        
    def do(self):
        self.func()


def main():
    def eggs(self):
        print self
    spam = Spam(eggs)
    spam.do()


if __name__ == '__main__':
    main()
Allerdings schliesse ich mich der Frage von Sirius3 an: Innerhalb der Methode hast Du doch `self` zur Verfügung, also kannst Du es dort doch auch übergeben.
Benutzeravatar
pillmuncher
User
Beiträge: 1484
Registriert: Samstag 21. März 2009, 22:59
Wohnort: Pfaffenwinkel

@droptix: Konstruktoren im eigentlichen Sinn gibt es in Python überhaupt nicht. __init__() ist ein Initializer, also eine Methode, die nicht ein neues Objekt konstruiert, sondern ein bestehendes Objekt initialisiert. Deswegen heist sie __init__(), andernfalls hieße sie wohl __constr__(). Es gibt auch die statische Methode __new__(), aber auch diese ist im eigentlichen Sinn kein Konstruktor, weil dazu die eigenhändige Allokation von Speicher gehören würde, was in Python aber nicht nötig ist.

Wenn du "Klassenmethode" schreibst, meinst du einfach nur Methode. Eine Klassenmethode ist eine Methode einer Klasse selbst, im Gegensatz zu einer Methode eines Objekts der Klasse. Klassen sind nämlich selbst Objekte in Python:

Code: Alles auswählen

class Foo(object):
    def bar(self):
        print self
    @classmethod
    def baz(cls):
        print cls

f = Foo()
f.bar()
Foo.baz()
Ergebnis:

Code: Alles auswählen

<__main__.Foo object at 0x7fee136c>
<class '__main__.Foo'>
Methoden - egal ob normale oder Klassenmethoden - werden im __dict__ der Klasse gehalten. Attribute, die in __init__() einem Objekt angetackert werden, werden im __dict__ dieses Objekts gespeichert. Was dort gespeichert wird, ist egal. Wenn es sich dabei um eine Funktion handelt, passiert dabei nichts anderes, als wenn es sich um einen String oder ein Integer handelt. Wenn du auf eine so gespeicherte Funktion zugreifen möchtest, dann passiert nichts besonderes. Deswegen wird in diesem Fall auch nicht die Referenz auf das Objekt als erstes Argument übergeben, wie es der Fall wäre, wenn es sich um eine Methode - also eine Funktion, die im __dict__ der Klasse gehalten wird - handeln würde. Deswegen must du das selbst erledigen.

Falls dir der Unterschied zwischen einer Klasse und einem Objekt einer Klasse nicht klar ist, hilft vielleicht dies: Die Klasse str ist die Klasse aller Pythonstrings, aber sie ist selbst kein String:

Code: Alles auswählen

print 'hallo'
print str
print 'hallo'.__class__
Ergebnis:

Code: Alles auswählen

hallo
<type 'str'>
<type 'str'>
Hier nochmal ein anderes Beispiel:

Code: Alles auswählen

def test():
    pass

class Zigzag(object):
    def __init__(self):
        self.x = 1
        self.g = test
    def f(self):
        pass

z = Zigzag()
print Zigzag.__dict__
print '-------------------------------------------------------'
print z.__dict__
Ergebnis:

Code: Alles auswählen

{'__module__': '__main__', 'f': <function f at 0x7fed57d4>, '__dict__': <attribute '__dict__' of 'Zigzag' objects>, '__weakref__': <attribute '__weakref__' of 'Zigzag' objects>, '__doc__': None, '__init__': <function __init__ at 0x7fed5454>}
-------------------------------------------------------
{'x': 1, 'g': <function test at 0x7fed5224>}
In specifications, Murphy's Law supersedes Ohm's.
droptix
User
Beiträge: 521
Registriert: Donnerstag 13. Oktober 2005, 21:27

Um `self` zu übergeben, müsste ich nun einiges im Quellcode umschreiben. Das wollte ich mir sparen.

Ich möchte innerhalb der Function auf `self` zugreifen können, um auf andere Objekte der Klasse zuzugreifen (innerhalb von `eggs` wird eine Schleife durchlaufen, die jedoch abbricht, wenn sich in der Klasse eine Variable ändert, die quasi ein Stopp-Signal darstellt). Ich wollte es vermeiden, dass ich eine Instanz der Klasse erzeugen und eine Methode überschreiben muss, weil das mehr Code erfordert und unübersichtlich wird. Das würde ja gehen:

Code: Alles auswählen

class Spam:
    def eggs(self):
        print "something"
        
    def do(self):
        self.eggs()
    
def main():
    class MySpam(Spam):
        def eggs(self):
            Spam.eggs(self)
            print self
    spam = MySpam()
    spam.do()

if __name__ == "__main__":
    main()
Nun dachte ich, man könnte vielleicht `eggs` einfach überschreiben, indem man das in `__init__` tut, aber das geht leider nicht aus den besagten Gründen, die u.a. pillmuncher ausführlich beschrieben hat:

Code: Alles auswählen

class Spam:
    def __init__(self, func):
        self.eggs = func
    
    def eggs(self):
        print "something"
        
    def do(self):
        self.eggs()

def main():
    def eggs(self):
        Spam.eggs(self)
        print self
    spam = Spam(eggs)
    spam.do()

if __name__ == "__main__":
    main()
@BlackJack: funktioniert! Ist doch genau das, was ich brauche! Spricht was dagegen?
droptix
User
Beiträge: 521
Registriert: Donnerstag 13. Oktober 2005, 21:27

Hum, jetzt habe ich hier eine Kombination aus dem eben Gefragten und concurrent.futures.ThreadPoolExecutor: das Dumme ist nur, dass der vereinfachte Code unten funktioniert: bei `self.executor.submit(self.func, self, job)` übergebe ich einfach `self` als zstzl. Argument und die Worker-Funktion `eggs` kann dann auch wie gewünscht auf `self` zugreifen und ein Status-Flag aus der Klassenvariable abfragen:

Code: Alles auswählen

from concurrent.futures import ThreadPoolExecutor

class Spam():
    def __init__(self, func):
        self.func = func
        self.executor = ThreadPoolExecutor(1) # for demo purposes only
        self._stopped = None

    def start(self, job):
        self._stopped = False
        future = self.executor.submit(self.func, self, job)
        future.add_done_callback(self.done)

    def done(self, future):
        result = future.result()
        print result

    def stop(self):
        self._stopped = True
        self.executor.shutdown()

def main():
    def eggs(self, job):
        print self._stopped
        return job.upper()
    spam = Spam(eggs)
    spam.start("a")
    spam.stop()

if __name__ == "__main__":
    main()
In meinem nicht-vereinfachten Code kommt diese Meldung (gleicher Aufbau, gleicher Aufruf bei `executor.submit`):

Code: Alles auswählen

exception calling callback for <Future at 0x2e98748L state=finished raised TypeError>
Traceback (most recent call last):
  File "C:\Python27\lib\site-packages\futures-2.1.3-py2.7.egg\concurrent\futures\_base.py", line 299, in _invoke_callbacks
    callback(self)
  File "C:\Temp\threadpool.py", line 104, in _done
    result = future.result()
  File "C:\Python27\lib\site-packages\futures-2.1.3-py2.7.egg\concurrent\futures\_base.py", line 397, in result
    return self.__get_result()
  File "C:\Python27\lib\site-packages\futures-2.1.3-py2.7.egg\concurrent\futures\_base.py", line 356, in __get_result
    raise self._exception
TypeError: eggs() takes exactly 2 arguments (1 given)
droptix
User
Beiträge: 521
Registriert: Donnerstag 13. Oktober 2005, 21:27

Ah, das kommt durch Vererbung/Überschreiben, verstehe es aber nicht. Ich möchte den Original-Quellcode meines einen Python-Moduls nicht verändern (1 Datei, enthält 2 Klassen). Stattdessen möchte ich ein weiteren Modul schreiben, in dem ich Teile der einen Klasse überschreibe. So sehen beide Teile aus, dann ist auch der Fehler reproduzierbar:

Hinweis: das ist vereinfachter Code, der den gleichen Fehler zeigt. Ich verstehe nicht wieso der auftritt. Für jegliche Tipps bin ich dankbar!

Datei 1: Eggs.py (funktioniert in sich, aber `self` wird noch nicht bei `self.executor.submit` übergeben)

Code: Alles auswählen

from concurrent.futures import ThreadPoolExecutor

class Spam:
    def __init__(self, func):
        self.func = func
        self.executor = ThreadPoolExecutor(1) # for demo purposes only
        self._stopped = None
        self.result = None

    def start(self, job):
        self._stopped = False
        future = self.executor.submit(self.func, job) # old one! see `MySpam`
        future.add_done_callback(self._done)

    def _done(self, future):
        self.result = future.result()

    def stop(self):
        self._stopped = True
        self.executor.shutdown()

class Eggs:
    def __init__(self, tasks):
        self._spams = []
        self.tasks = tasks
        
    def add(self, func):
        spam = Spam(func)
        self._spams.append(spam)
        job = self.tasks.pop(0)
        spam.start(job)

    def stop_all(self):
        for spam in self._spams:
            spam.stop()

    def get_results(self):
        return [spam.result for spam in self._spams]

def main():
    from time import sleep
    def func(job):
        return job.upper()
    tasks = ["a"]
    eggs = Eggs(tasks)
    eggs.add(func)
    sleep(1)
    eggs.stop_all()
    print eggs.get_results()

if __name__ == "__main__":
    main()
Datei 2: Eggs2.py erzeugt den Fehler den ich nicht verstehe. Sicherlich mache ich beim Überschreiben etwas falsch?

Code: Alles auswählen

from Eggs import Spam
from Eggs import Eggs
        
# overwrite `Spam.start`
class Spam(Spam):
    def start(self, job):
        self._stopped = False
        future = self.executor.submit(self.func, self, job) # new one!
        future.add_done_callback(self._done)

def main():
    from time import sleep
    def func(self, job):
        print "stopped:", self._stopped
        return job.upper()
    tasks = ["a"]
    eggs = Eggs(tasks)
    eggs.add(func)
    sleep(1)
    eggs.stop_all()
    print eggs.get_results()

if __name__ == "__main__":
    main()
lunar

@pillmuncher Wo steht geschrieben, dass ein Konstruktor Speicher eigenhändig allozieren müsste? Und ist nicht die Erzeugung des Objekts über den Aufruf der geerbten "__new__"-Methode letztlich nichts anderes als eine Speicherallokation?
Sirius3
User
Beiträge: 17738
Registriert: Sonntag 21. Oktober 2012, 17:20

@droptix: Du überschreibst nicht Spam.start sondern erzeugst eine neue Klasse, __main__.Spam, in Eggs benutzt Du aber ausschließlich Eggs.Spam.
droptix
User
Beiträge: 521
Registriert: Donnerstag 13. Oktober 2005, 21:27

Sirius3 hat geschrieben:@droptix: Du überschreibst nicht Spam.start sondern erzeugst eine neue Klasse, __main__.Spam, in Eggs benutzt Du aber ausschließlich Eggs.Spam.
Wie müsste ich das richtig überschreiben?
BlackJack

@droptix: Da `Eggs` `Eggs.Spam` benutzt, geht das gar nicht sauber. Denn *dort* müsste ja Dein abgeleitetes `Eggs2.Spam` verwendet werden, was die `Eggs.Eggs`-Klasse aber gar nicht kennt.

Es sieht so ein bisschen so aus als wolltest Du mit einem fiesen Hack darum herum kommen die Struktur Deines Programms grundlegender ändern zu müssen.
droptix
User
Beiträge: 521
Registriert: Donnerstag 13. Oktober 2005, 21:27

Im Original-Code ist ja kein Fehler drin. Ich möchte den Code halt anpassen, ohne dass gesamte Programm neu zu schreiben wegen einer einzigen Zeile... Ist nicht im Sinne von POP.

Ist anscheinend aber der einzige Weg... Also werde ich einfach den Code 1:1 kopieren und die eine Zeile ändern :( ich will ja auch was dabei lernen, sonst würde ich nicht so viele Detail-Fragen stellen :) hab ja auch wieder einiges Neuwissen erworben, danke schon mal dafür!
BlackJack

@droptix: Wenn das was Du machen möchtest nicht *einfach* geht, könnte man schon sagen, dass in dem Original-Code ein Fehler ist, nämlich ein Entwurfsfehler, weil der Code nicht so flexibel ist, wie er sein müsste. Und in solchen Fällen ändert man den Entwurf. Man muss ihn ja nicht komplett über den Haufen werfen und alles neu schreiben, sondern nur so weit anpassen, dass er den neuen Anforderungen genügt. Im günstigsten Fall ohne dass das Auswirkungen auf bereits bestehenden Quelltext hat, der das ganze verwendet. Den Code kopieren um eine Zeile zu ändern ist sicher eine der schlechteren Lösungen, weil sie das DRY-Prinzip (Don't Repeat Yourself) verletzt.

Das tust Du übrigens mit der `main()`-Funktion schon.

Eine einfache Lösung ist „dependency injection” (DI). Gib dem Benutzer der `Eggs.Eggs`-Klasse die Wahl welchen Typ er für `Spam` verwenden möchte. Zum Beispiel so:

Code: Alles auswählen

class Eggs(object):
    def __init__(self, tasks, spam_type=Spam):
        self._spams = []
        self.tasks = tasks
        self.spam_type = spam_type
        
    def add(self, func):
        spam = self.spam_type(func)
        self._spams.append(spam)
        job = self.tasks.pop(0)
        spam.start(job)
Damit ändert sich absolut nichts für bereits bestehenden Code, aber Du gibst dem Benutzer die Möglichkeit eine eigene, andere Klasse anstelle von `Spam` zu verwenden, beziehungsweise verwenden zu lassen.
Antworten