Schwierig? [Edit:] Wer rief die Methode auf?

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

Habe hier eine komische Sache vor mir: ich versuche das mal in Worte zu fassen...
  • zwei Klassen A und B
  • B erzeugt eine Instanz von A
  • B ruft in der A-Instanz eine Methode auf und verarbeitet dort drin quasi "lokal" einen Wert (hier: `print spam`)
Den Code der Klassen A und B darf ich nicht abändern, da dieser derzeit in mehrfacher Verwendung ist.

Nun benötige ich aber den Wert `spam` nun auch in der B-Instanz. Also Idee: Klassen-Methode überschreiben. Den Return-Wert der besagten A-Instanz-Methode darf ich aber auch nicht ändern (hier: `return True`).

Wie kann ich aus der Instanz `b_` heraus auf den Wert `spam` zugreifen? Ich möchte also das Objekt bestimmen, welches "mich" (aus Sicht der Methode) aufgerufen hat.

Code: Alles auswählen

class ClassA:
        def method_x(self):
                print "method_x()"
                self.method_y("eggs")
        def method_y(self, spam):
                print "method_y()"
                print spam
                return True

class ClassB:
        def method_z(self):
                print "method_z()"
                a = ClassA()
                a.method_x()

b = ClassB()
b.method_z()

print 79*"-"

# overwrite `method_y`
def new_method_y(self, spam):
        print "new_method_y()"
        # make `spam` available in `ClassB` instance (`b_`)
        """
        self.__myCallingInstance__.spam = spam
        """
        return True
ClassA.method_y = new_method_y

b_ = ClassB()
b_.method_z()
# I need this
"""
print b_.spam
"""
Zuletzt geändert von droptix am Donnerstag 19. April 2012, 09:04, insgesamt 1-mal geändert.
EyDu
User
Beiträge: 4881
Registriert: Donnerstag 20. Juli 2006, 23:06
Wohnort: Berlin

Meinst so so etwas:

Code: Alles auswählen

def new_method_y(self, spam):
    print "this is spam:", spam
    return self.method_y(self, spam)

ClassA.method_y = new_method_y
Da habe ich aber gleich noch ein paar Anmerkungen: Du brauchst den Wert in ``b``, darfst die Klasse aber nicht verändern? Das hört sich nach einer seltsamen Beschränkung an. Außerdem sollten deine Klassen sollten bei Python 2.x von ``object`` erben und mit vier Leerzeichen einrücken.

Viel wichtiger wäre es aber, wenn du sauber strukturierten und lesbaren Code zeigst. Das ist ein einziges Durcheinander und hilft nicht sonderlich gut bei einer bereits schlecht formulierten Problemstellung. Vielleicht verrätst du uns einfach, was du eigentlich machen möchtest und nicht wie du gedenkst es umzusetzen. Wahrscheinlich gibt es dafür eine bessere Lösung.
Das Leben ist wie ein Tennisball.
lunar

@droptix: Auf saubere Art und Weise kommst Du nicht an lokale Namen anderer Methoden und Funktionen. Versuche, an das Objekt zu gelangen, nicht an den Namen. Der fällt ja schließlich nicht vom Himmel, das daran gebundene Objekt kommt also auch irgendwo her. Wenn es keine sinnvolle Möglichkeit gibt, an dieses Objekt zu gelangen, dann musst Du eben versuchen, ohne dieses Objekt auszukommen und Dein Problem auf andere Art und Weise zu lesen. Wenn auch das nicht geht, dann spricht mit Deinem Chef.
deets

Ich würde das ganz einfach lösen: die Methode von A, welche B aufruft bekommt ein neues Argument - "return_spam" zb. Mit Default-Wert False. Aber an der stelle, wo du eben mit der Rückgabe arbeiten willst, rufst du die Methode entsprechend anders auf - und sie gibt SPAM zurück.

Alle anderen Lösungen laufen letztlich auf globale Variablen hinaus - und damit rutscht du ein gutes Stück Richtung Hölle...
droptix
User
Beiträge: 521
Registriert: Donnerstag 13. Oktober 2005, 21:27

Ich habe mal den Titel des Threads geändert, da meine Frage hätte lauten müssen: Wie kann ich das Objekt ermitteln, das die Methode aufrief?

Ich gebe den Kindern mal Namen... das ist hier ein Riesen-Code-Klumpen, den ich mal gekürzt vorstelle:

Es geht darum, dass ich die Zählerstände von Druckern auslese (Kopien und Drucke). Dabei kennt jeder Drucker "Konten", also muss man sich vor dem Drucken und Kopieren am Gerät anmelden. Dann wird jedes gedruckte Blatt/jede Kopie auf das Konto gezählt (Kostenstellen-Verhalten). Mit meinem Programm lese ich von allen Druckern ein Konto aus und summiere die Anzahl der Drucke/Kopien, um sie auszuwerten.

Der vorhandene Code liest immer alle Konten aus und schreibt eine CSV-Datei für jeden Drucker. Da soll für Auswertungszwecke auch so bleiben.

Ich möchte auf dieser Basis nur ein Konto auf allen Druckern abfragen und am Ende eine Summe anzeigen. Daher möchte ich den vorhandenen Code als Modul importieren und erweitern.

Das hier könnte mir evtl. helfen, aber ich verstehe es leider nicht. Vielleicht kann mir jemand das näher bringen?

http://stackoverflow.com/questions/1095 ... -in-python
http://blog.doughellmann.com/2007/11/py ... spect.html

Mit dem Unterschied, dass ich nicht den Namen des aufrufenden Objekts benötige, sondern das Objekt selbst.

Hier mein gekürzter Code:

Code: Alles auswählen

import os

class Printer:
        def __init__(self, ip):
                self.ip = ip
        
        def get_account(self, uid):
                # gets account information from the printer
                """
                # connect to the printer's IP address
                self.connect()
                # do some magic stuff...
                # return `False` if account not found on this printer
                """
                account = {
                        "uid": uid,
                        "prints": 4,
                        "copies": 13
                }
                return account
        
        def as_csv(self, account):
                # returns account information as CSV structure
                csv = []
                csv.append(account.keys())
                csv.append(account.values())
                return "\r\n".join([",".join([str(item) for item in line]) for line in csv])

class Accounting:
        def __init__(self, ips, accounts):
                self.ips = ips
                self.accounts = accounts
        
        def get_accounts(self):
                # get `Printer` objects from IP addresses
                printers = {}
                for ip in ips:
                        printers[ip] = Printer(ip)
                # for each account, ask every printer for its account information
                accounts = {}
                for uid in self.accounts:
                        # cumulate number of prints and copies
                        prints = 0
                        copies = 0
                        for ip in printers:
                              printer = printers[ip]
                              account = printer.get_account(uid)
                              prints = prints + account["prints"]
                              copies = copies + account["copies"]
                              # also get the CSV version
                              csv = printer.as_csv(account)
                        accounts[uid] = {"prints": prints, "copies": copies, "csv": csv}
                """
                # add lines in `csv` (extra information)
                # ... code here ...
                """
                # now write CSV files for each account
                for uid in accounts:
                        if "csv" in accounts[uid].keys():
                                csv = accounts[uid]["csv"]
                                if csv:
                                        filename = "%s.csv" % uid
                                        """
                                        f = open(filename, "w")
                                        f.write(csv)
                                        f.close()
                                        """
                                        print filename

# list of printer ip addresses
ips = ["192.168.0.1", "192.168.0.2"]
# list of account IDs (cost locations), e.g. unique login names
accounts = ["john", "jane"]
a = Accounting(ips, accounts)
a.get_accounts()

print 79*"-"

# overwrite `Printer.as_csv()`
# import inspect # useful?

def as_csv(self, account):
        print "who called me (object): "
        return None
Printer.as_csv = as_csv

# do it again
a = Accounting(ips, accounts)
a.get_accounts()
droptix
User
Beiträge: 521
Registriert: Donnerstag 13. Oktober 2005, 21:27

Also ich finde haufenweise Infos wie diese hier:

http://benno.id.au/blog/2011/01/06/python-get-caller

Aber all diese Beispiele beschreiben lediglich, wie ich u.a. an den Namen des aufrufenden Objekts gelange, nicht aber an das Objekt selbst.

-> Ist nah dran, aber wohl doch nicht dass was ich brauche.

[Stunden später:]

Code: Alles auswählen

import inspect

def whosdaddy():
        return inspect.stack()[2][0].f_locals["self"]
Somit kann ich dann folgendes:

Code: Alles auswählen

def as_csv(self, account):
        daddy = whosdaddy()
        print "who called me (object): ", daddy
        daddy.account = account
        return None
Printer.as_csv = as_csv
Ich teste das mal ausgiebig, sollte aber genau das machen was ich brauche :-)
BlackJack

@droptix: Das sollte aber nichts sein was Du in produktivem Code einsetzt. Das ist ziemlich krank und kann Dir schnell um die Ohren fliegen, wenn es zum Beispiel kein `self` beim Aufrufer gibt, oder das gar nicht an das Objekt gebunden ist an das Du eigentlich heran kommen möchtest. Das es keine einfache und zuverlässige Möglichkeit gibt im Namensraum des Aufrufers herum zu wurschteln würde ich als bewusste Entwurfsentscheidung der Sprache ansehen.
lunar

@droptix: Wenn Du das in produktivem Code einzusetzen gedenkst, dann hoffe besser, dass Dein Chef keine Ahnung hat von Python...
droptix
User
Beiträge: 521
Registriert: Donnerstag 13. Oktober 2005, 21:27

Darf ich den Hintergrund erfahren, wieso das nicht produktiv eingesetzt werden soll? Aus Sicht des Computers macht der doch immer dasselbe...

Der vorhandene Code ist von mir und tut auch stur immer dasselbe. Daher soll der auch so bleiben, weil das weiter benötigt wird. Also kann ich doch auch "eine Ebene nach oben" und im Caller eine Objekteigenschaft einpflanzen. Mein Chef bin in diesem speziellen Fall ich selbst, über die Art der Umsetzung muss ich entscheiden, Hauptsache am Ende funktioniert's und macht wenig Aufwand.

Wie gesagt, ich teste das, sehe aber keinen Grund das nicht so umzusetzen.
BlackJack

@droptix: Wenn Du „Hauptsache es funktioniert” ausreichend findest, dann kann man damit jeden Mist rechtfertigen, der halt irgendwie funktioniert.

Das koppelt Code auf eine sehr undurchsichtige Weise und erzeugt Abhängigkeiten und Einschränkungen die nicht in normalem Code vorkommen. Solche Hacks solltest Du gut dokumentieren. Niemand rechnet damit dass sich eine Funktion je nach *Aufrufer*, also im Grunde einem magischen, versteckt „übergebenen” Argument, anders verhalten könnte. In die Dokumentation von so einer Funktion gehört, dass der *direkte* Aufrufer einen lokalen Namen `self` haben muss und was das für ein Typ sein muss. Und wenn ich so eine Dokumentation lesen würde, hätte ich schon keine Lust mehr das zu verwenden, weil mir das viel zu undurchsichtig, magisch, und beschränkend wäre.

Was ich dabei nicht verstehe: Wenn der gesamte Code sowieso unter Deiner Kontrolle steht und Du der Chef bei der Umsetzung bist, warum löst Du es dann nicht *einfach* und normal statt so einen Hack zu verwenden?
Leonidas
Python-Forum Veteran
Beiträge: 16025
Registriert: Freitag 20. Juni 2003, 16:30
Kontaktdaten:

BlackJack hat geschrieben:Das koppelt Code auf eine sehr undurchsichtige Weise und erzeugt Abhängigkeiten und Einschränkungen die nicht in normalem Code vorkommen. Solche Hacks solltest Du gut dokumentieren. Niemand rechnet damit dass sich eine Funktion je nach *Aufrufer*, also im Grunde einem magischen, versteckt „übergebenen” Argument, anders verhalten könnte.
``runkit_return_value_used`` anyone? :) Das ist genauso seltsam und birkenfeld hat sogar rausgefunden wie man das nach Python portieren kann.
My god, it's full of CARs! | Leonidasvoice vs (former) Modvoice
droptix
User
Beiträge: 521
Registriert: Donnerstag 13. Oktober 2005, 21:27

Also es funktioniert erstmal... und das gut zu dokumentieren ist sicher sinnvoll.

Auf "normale" Weise lösen würde bedeuten, den vorliegenden Code anpassen zu müssen, da ich keine Möglichkeit habe, an das Zwischenergebnis von ``Accounting.get_accounts()`` ranzukommen (Dictionary ``accounts``). Ich sehe nur über ``Printer.as_csv()`` eine Möglichkeit, das Ergebnis zum "Caller" zu übergeben, um es auszuwerten.

^^ Der Grund dafür ist, dass der vorliegende Code für die damalige Aufgabe optimal ist, aber schlecht erweitert werden kann. Er ist nicht ausreichend intelligent strukturiert.

Da der aber in der Urform weiter benötigt wird, möchte ich dafür jetzt keinen Fork beginnen und später 2x Code pflegen, wenn Änderungen nötig sind.
deets

Ich verstehe immer noch nicht, warum du as_csv nicht einfach ein weiteres Argument uebergeben kannst, abhaengig von dem du dann den Account setzt.

Unter Umstaenden musst du diesen Parameter ein bisschen durchschleifen, aber was ist daran schlimm? Und es ist in jedem fall besser, da schon von der Signatur der Funktion her klar ist, dass da ein weiterer Parameter uebergeben werden kann.
BlackJack

@droptix: Ich verstehe immer noch nicht warum Du nicht den vorhandenen Code anpassen kannst. Du kannst das ja in einer Art und Weise tun, dass er für den bisherigen Zweck weiterhin funktioniert. Eine Version mit solchen Hacks pflegen zu müssen ist IMHO noch nicht einmal besser als zwei *ordentliche* Versionen zu pflegen, die ohne solche Überraschungen auskommen. Wobei man wie gesagt da immer noch bestrebt sein sollte *eine* Version daraus zu machen.

Aus dem von Dir gezeigten Quelltext wird das Problem übrigens IMHO in keinster Weise ersichtlich. Du hast da eine `as_csv()`-Funktion, der Du `account` *übergibst* und behauptest die einzige Möglichkeit dort heran zu kommen wäre dieses Stackframe-Voodoo, das den Wert an das Exemplar, an das der Aufrufer gebunden ist, bindet. Der Aufrufer *hat* dieses Objekt doch aber schon, sonst hätte er es nicht als Argument übergeben können!?
lunar

@droptix: Durch den Zugriff auf Namen in einem höheren Aufrufkontext hängt Deine Funktion vom Aufrufstack und vom Namensraum des Aufrufers ab. Diese Abhängigkeiten sind implizit, und bricht das Versprechen, dass eine Funktion als „Black Box“ behandelt werden kann, deren Berechnung allein von den explizit übergebenen Parametern abhängt. Implizite Abhängigkeiten erhöhen die Komplexität des Quelltexts, und binden die Funktion an ihre Umgebung, da Du den Namensraum des Aufrufers nicht mehr ohne weiteres ändern kannst.

Glücklicherweise ist das Dein eigener Quelltext, und alle schwer zu findenden Fehler, die sich auf diese „Lösung“ zurückführen lassen, sind dann auch Dein eigenes Problem :) Nichts für ungut…
droptix
User
Beiträge: 521
Registriert: Donnerstag 13. Oktober 2005, 21:27

BlackJack hat geschrieben:@droptix: Ich verstehe immer noch nicht warum Du nicht den vorhandenen Code anpassen kannst. Du kannst das ja in einer Art und Weise tun, dass er für den bisherigen Zweck weiterhin funktioniert.
Theoretisch ja, aber ich müsste einen Haufen Code ändern. Das obige Bsp. zeigt ja nur einen kleinen Ausschnitt. Ich kann nicht ohne Weiteres einfach nur ein weiteres Argument an die Funktion übergeben, das müsste ich aber nochmal genau prüfen.
BlackJack hat geschrieben:Aus dem von Dir gezeigten Quelltext wird das Problem übrigens IMHO in keinster Weise ersichtlich. Du hast da eine `as_csv()`-Funktion, der Du `account` *übergibst* und behauptest die einzige Möglichkeit dort heran zu kommen wäre dieses Stackframe-Voodoo, das den Wert an das Exemplar, an das der Aufrufer gebunden ist, bindet. Der Aufrufer *hat* dieses Objekt doch aber schon, sonst hätte er es nicht als Argument übergeben können!?
Nicht ganz: der Aufrufer (Instanz von `Accounting`) kennt ein gleichnamiges Objekt. Das ist aber eine Liste. In `as_csv` wird ein Dictionary erzeugt, an welches ich heran kommen muss.

Code: Alles auswählen

accounts = ["john", "jane"]
a = Accounting(ips, accounts)
# in `as_csv()`:
accounts[uid] = {"prints": prints, "copies": copies, "csv": csv}
@lunar: Ja, das kann ich nachvollziehen, wenn ich Code für andere schreibe. Da ich hier eine sehr spezielle Aufgabe mit einem nur dafür vorgesehenem Mittel lösen muss, ziehe ich "quick 'n' dirty" vor, weil mir dadurch viele Zeilen Code-Anpassung erspart bleiben... nicht nur weil ich von nataur aus faul bin :-) sondern v.a. weil viel Anpassung auch viele neue Fehlermöglichkeiten bedeuten.

Krass übrigens, dass ich dafür so viel heftige Kritik einsammle... hätte nicht gedacht, dass man da so viel Wirbel drum macht ;-) Nichts für Ungut, kann damit umgehen, bin einfach nur überrascht. Ich hätte die Frage auch weniger kompliziert stellen können, aber dann wären mir die Auswirkungen vllt. nicht so einprägsam vermittelt worden. Danke also, ich werde das auch jeden Fall zukünftig berücksichtigen.
Leonidas
Python-Forum Veteran
Beiträge: 16025
Registriert: Freitag 20. Juni 2003, 16:30
Kontaktdaten:

droptix hat geschrieben:Krass übrigens, dass ich dafür so viel heftige Kritik einsammle... hätte nicht gedacht, dass man da so viel Wirbel drum macht ;-) Nichts für Ungut, kann damit umgehen, bin einfach nur überrascht.
Naja, was hast du erwartet? In der Python-Community wird stärker Wert gelegt auf gute Lösungen, nicht nur welche die irgendwie funktionieren, wie etwa in vielen PHP Communities. Siehe etwa die Threads mit problembär.
My god, it's full of CARs! | Leonidasvoice vs (former) Modvoice
BlackJack

@droptix: In dem Quelltext den Du *gezeigt* hast, wird das Objekt übergeben und kein neues erzeugt.

Du bekommst so viel Kritik weil Du doch Code für andere schreibst. Entweder bist Du der ”andere”, wenn Du das Programm nach längerer Zeit wieder neu verstehen musst, oder es passiert etwas was angeblich nie passieren kann: Es muss tatsächlich mal jemand anders etwas an dem Quelltext machen. Viele von uns kennen das wahrscheinlich aus leidvoller Erfahrung, dass wir quick'n'dirty Quelltext von uns selbst nach einem Jahr nicht mehr wirklich verstehen, oder dass man Code von jemandem „erbt” der aussieht wie Kraut und Rüben. Da kommen dann Gefühle zwischen Hass und Verzweiflung auf.

Ausserdem tendieren solche Hacks dazu sich im Laufe der Zeit Schicht für Schicht anzusammeln. Und irgendwann hat man sich dann in eine Ecke manövriert wo der Quelltext schlicht nicht mehr wart- und erweiterbar ist, man das aber trotzdem machen muss. Was dann nicht selten auf wegwerfen und neuschreiben unter extremen Zeitdruck oder schlicht aufgeben hinaus läuft.
Leonidas
Python-Forum Veteran
Beiträge: 16025
Registriert: Freitag 20. Juni 2003, 16:30
Kontaktdaten:

BlackJack hat geschrieben:oder dass man Code von jemandem „erbt” der aussieht wie Kraut und Rüben. Da kommen dann Gefühle zwischen Hass und Verzweiflung auf.

Ausserdem tendieren solche Hacks dazu sich im Laufe der Zeit Schicht für Schicht anzusammeln. Und irgendwann hat man sich dann in eine Ecke manövriert wo der Quelltext schlicht nicht mehr wart- und erweiterbar ist, man das aber trotzdem machen muss. Was dann nicht selten auf wegwerfen und neuschreiben unter extremen Zeitdruck oder schlicht aufgeben hinaus läuft.
Haha, ich habe dazu einige kleine Anekdoten, und dabei habe ich bisher nur selten mit bestehenden proprietären Codebases arbeiten müssen.

Ich sollte eine Lib schreiben, die als Backend für ne GUI arbeitet, die ein Binärformat liest und schreibt. Dieser Teil wurde von Construct wunderbar übernommen (wenige dutzend Zeilen Code). Jedoch gibt es als Alternative zu diesem Binärformat auch ein etwas seltsames Textformat, zu dem es einen firmeninternen Parser gab und den ich nutzen sollte, da es zum Textformat keine Dokumentation gibt. Stellt sich aber raus, dass dieser Parser zu einer anderen Software gehört und diese Lizenzkeys verlangt, auch wenn man nur den Parser benötigt. Nachdem ich aufgegeben habe, verstehen zu wollen wie dieser Parser auf abenteuerliche Weise funktioniert und es auch nicht besonders schön fand dem Programm ein Lizenzfile mitzugeben, welches eben auch die andere Software "freischalten" würde, wurde letztendlich das die ganze Funktionalität raus-gemonkey-patched, so dass die meine Verifikations-Methode immer sagte, dass die Lizenz da ist, und ich diesen Parser verwenden konnte.

Das kommt davon wenn man zu seltsamen Code schreibt, der irgendwie über ganz seltsame Art ineinanderverwickelt ist.

Die andere Story möchte ich auch nicht vorenthalten: dieser Parser konnte nämlich auch diese Dateien in dem Format rausschreiben. Genau einmal. Danach war das Objekt auf irgendeine Weise modifiziert, irgendwelche Felder würden gelöscht oder resettet, so das das Objekt unbrauchbar war. Die einfache Lösung wäre, einfach ein neues Objekt anzulegen, aber leider war nicht nur das Objekt danach kaputt sondern noch mindestens die zugehörige Klasse oder auch der ganze Interpreter. Ich habe aufgegeben, das zu analysieren (weil das fixen der Codebase war nicht mein Job) und dann multiprocessing rausgepackt und diese Funktionalität immer an einen "frischen" Interpreter delegiert.

Ja, beides bizarre und furchtbare Hacks. Sowas sollte man auf jeden Fall vermeiden, irgendwelche "klugen" Optimierungen, die sich dann später als Fluch herausstellen.
My god, it's full of CARs! | Leonidasvoice vs (former) Modvoice
lunar

@droptix: Ergänzend zu BlackJack: Suche mal nach der "Broken Windows"-Theorie im Bezug auf Software Engineering. Das Phänomen, dass kleine, unschöne Lösungen über die Zeit kumulieren und schlussendlich die Qualität des gesamten Quelltexts stark absinken lassen, ist sogar empirisch untersucht und wissenschaftlich beschrieben. Wenn wir Dir nachdrücklich vor solchen Lösungen abraten, geschieht das mithin weder, um Dich zu ärgern noch um Dich zu provozieren.

Es ist letztlich Deine Entscheidung, doch erwarte nicht allzu viel Verständnis dafür, spätestens, wenn Du hier in einem halben Jahr wegen eines Problems nachfragst, welches durch diesen Hack verursacht wurde.
Antworten