Verwirrende Zeichen

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.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Hallo Leute,

wenn wir einmal den unten angegebenen und aus dem Kontext gerissenen Quelltext anschauen, dann fallen mit zwei markante SAchen auf, die ich noch nicht kenne, und auch noch nicht angewendet habe. Das sind die Stellen mit dem @-Zeichen. Was bedeuten für mich als Anfänger die
@property und @running.setter ? (Den vollständigen Quelltext könnt ihr hier einsehen). Und was mich auch verwundert, ist, dass diese Funktionen, die mit dem @-Zeichen zusammenhängen, zwischen class und der def __init__(self, model, main_ctrl): (Konstruktor). Dies kenne ich eher bei einem QThread, wo die pyqtSignal()-Funktionen sind.

Also zwei Fragen. Was besagen mir diese @-Zeichen und zweite Frage (endlich wird mir dann auch QThread etwas klarer) wieso werden die Funktionen zwischen class und der def__init__-Methode - also im class-Block gesetzt?

Code: Alles auswählen

from PySide import QtGui
from gen.ui_MainView import Ui_MainView

class MainView(QtGui.QMainWindow):

    # properties to read/write widget value
    @property
    def running(self):
        return self.ui.pushButton_running.isChecked()
    @running.setter
    def running(self, value):
        self.ui.pushButton_running.setChecked(value)

    def __init__(self, model, main_ctrl):
        self.model = model
        self.main_ctrl = main_ctrl
        super(MainView, self).__init__()
        self.build_ui()
        # register func with model for future model update announcements
        self.model.subscribe_update_func(self.update_ui_from_model)

    def build_ui(self):
        self.ui = Ui_MainView()
        self.ui.setupUi(self)
        # connect signal to method 
        self.ui.pushButton_running.clicked.connect(self.on_running)

    def on_running(self):
        self.main_ctrl.change_running(self.running)

    def update_ui_from_model(self):
        self.running = self.model.running
Sirius3
User
Beiträge: 18335
Registriert: Sonntag 21. Oktober 2012, 17:20

@Sophus: das @ heißt Decorator und wird hier mit der property-Klasse benutzt.
Die Reihenfolge von Definitionen in einer Klasse ist völlig egal, weil sowieso alles in ein ungeordnetes Wörterbuch (__dict__) wandert.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Und alles was innerhalb eines class-Blockes steht sind was? Also Bei QThread stehen die pyqtSignal()-Funktionen gleich direkt unterhalb der Klasse, und noch oberhalb der Initialisierungsmethode (__init__()). Das bewirkt im Hintergrund....?
BlackJack

@Sophus: Das die zwischen ``class`` und ``def __init__()`` stehen ist Zufall und auch eher ungewöhnlich. Mit @ werden Dekoratoren angewendet. Das sind Funktionen die eine Funktionen als Argument bekommen und eine Funktion als Ergebnis liefern. Das mit dem @ ist einfach nur syntaktischer Zucker für folgendes:

Code: Alles auswählen

@expr
def f(...):
    # ...

# <->

def f(...):
    # ...
f = expr(f)
Wobei `expr` ein Ausdruck ist, der zu einer entsprechenden Funktion ausgewertet wird. Im einfachsten Fall ist das einfach ein Name, wie bei `property`, es können aber auch komplexere Ausdrücke sein, wie Funktionsaufrufe oder Attribute von anderen Objekten.

Mit Klassen geht das auch, also:

Code: Alles auswählen

@expr
class A(object):
    # ...

# <->

class A(object):
    # ...
A = expr(A)
In dem Fall muss die Funktion die der Ausdruck `expr` liefert natürlich eine Klasse als Argument erwarten und eine Klasse als Ergebnis liefern.
BlackJack

@Sophus: Das die `pyqtSignal()`-Funktionen zwischen ``class`` und ``def __init__()`` stehen bewirkt das es lesbar/verständlich ist. Die könnte man auch irgendwo zwischen den ``def``\s in der Klasse verstecken, oder ans Ende schreiben. Was in der Klasse steht sind Anweisungen die der Reihe nach ausgewertet werden. Und wenn dabei Namen definiert werden, landen die am Ende im Namensraum der Klasse. Das ist alles.
Sirius3
User
Beiträge: 18335
Registriert: Sonntag 21. Oktober 2012, 17:20

@Sophus: pyqtSignal erzeugt einfach ein Property. Dass es ganz am Anfang steht, liegt daran, damit es schnell beim Lesen gefunden wird, da Signale bei QT *die* Schnittstelle zwischen den Widgets ist. Für Python ist es im Normalfall völlig egal, in welcher Reihenfolge Definitionen stehen.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Ok, das mit den PytSignals habe ich wohl verstanden. Aber was mich noch nicht ganz gestillt hat, ist das mit dem Dekorator. Was bezeugt das, wenn ich eine bestimmte Funktion mit einem @ kennzeichne? Die Funktion würde auch ohne dieses @-Zeichen funktionieren richtig? Damit ihr meinte naive Frage versteht, habe ich mal zwei kleine Ausschnitte kreiert. Bei einem ist das @-Zeichen und beim anderen nicht. Aber in beiden sind ein und die selbe running()-Funktion. Ergibt sich daraus nun einen Unterschied?

Code: Alles auswählen

class MainView(QtGui.QMainWindow):
 
    def running(self):
        return self.ui.pushButton_running.isChecked()

Code: Alles auswählen

class MainView(QtGui.QMainWindow):
 
    # properties to read/write widget value
    @property
    def running(self):
        return self.ui.pushButton_running.isChecked()
Sirius3
User
Beiträge: 18335
Registriert: Sonntag 21. Oktober 2012, 17:20

@Sophus: würdest Du Deine beiden Varianten ausprobieren, würdest Du merken, dass es sich nicht um die selben running-Objekte handelt:

Code: Alles auswählen

print(variante1.MainView.running)
# <function MainView.running at 0x1029dbb70>
print(variante2.MainView.running)
# <property object at 0x1029e3e58>
Der property-Decorator erzeugt aus einer Funktion ein Property, also ein Objekt dessen Wert wie bei einem Attribut lesen und schreiben kann, die Lese- bzw. Schreiboperation führt aber zu einem Funktionsaufruf. Wenn man es einfach weglassen könnte, bräuchte man es erst gar nicht einführen.

Konkret heißt das in Deinem Beispiel mit einer mainview-Instanz:

Code: Alles auswählen

print(mainview_variante1.running())
print(mainview_variante2.running)
liefern beide das selbe Ergebnis.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@Sirius3: Vielen Dank. Du schreibst das durch die Property die Finktion dann wie ein Attribut behandelt wird, auf das ich dann lesend und schreibend zugreifen kann. Wenn ich aber mit diesen Operatoren die Funktion nicht lese und schreibe, sondern aufrufe, wozu dann die Property? Oder habe ich einen Denkfehler?
BlackJack

@Sophus: „eim einen ist das @-Zeichen und beim anderen nicht” ist keine ausreichende Beschreibung des Unterschieds. Bei der Variante mit dem @-Zeichen ist ja auch noch ``property`` als Ausdruck hinter dem @-Zeichen und dass ist ja der Teil der *wichtig* ist für den Unterschied, also was das dann tatsächlich semantisch bedeutet. Wie ich weiter oben ja schon geschrieben habe ist das @-Zeichen einfach nur syntaktischer Zucker und da habe ich ja auch gezeigt wie man den gleichen Code ohne das @ schreiben müsste. Ganz konkret für das gezeigte Beispiel:

Code: Alles auswählen

class MainView(QtGui.QMainWindow):
     
    def running(self):
        return self.ui.pushButton_running.isChecked()
    running = property(running)
Und weil das bei längeren Methoden, und insbesondere bei `property()`, wo man oft Getter und Setter vorher definiert hatte, leicht zu übersehen ist, das die eigentliche öffentliche API erst weiter unten, nach der Definition der Funktion/Methode, noch einmal verändert wird, wurde die @-Syntax eingeführt. Auf diese Weise kann man beim Lesen schon beim ``def`` sehen, dass die Funktion/Methode/Klasse nach der Definition noch einmal manipuliert wird, oder irgend etwas anderes wichtiges damit getan wird.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

Wenn ich den Unterschied richtig verstanden habe

Mit Properties

Code: Alles auswählen

class AddUp(object):

    def __init__(self, i):
        self.i=i

    @property
    def do_calculate(self):
        return self.i+1

result = AddUp(2)
print result.do_calculate
# --> 3
Dadurch, dass ich lesend zugreife, erhalte ich das Ergebnis.

Ohne Properties

Code: Alles auswählen

class AddUp(object):

    def __init__(self, i):
        self.i=i

    def do_calculate(self):
        return self.i+1

result = AddUp(2)
print result.do_calculate()
# --> 3
Da ich nicht lesen zugreifen kann, rufe ich die Funktion einfach direkt auf.

Jetzt meine letzte Frage:
Welchen Sinn haben diese Properties? Ich stelle es nicht in Frage. Aber anhand meines Beispiels wird mir nicht so ganz klar, welchen Sinn die Properties haben. In beiden Fällen bekomme ich das Ergebnis zurück - nur eben auf unterschiedliche Wege.
Sirius3
User
Beiträge: 18335
Registriert: Sonntag 21. Oktober 2012, 17:20

@Sophus: es macht auf verschiedene Arten Sinn: wenn das Interface ein Attribut erwartet, Kompatibilität zu älteren Versionen, weil es leichter lesbar ist, ...
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@Sirius3: Leider erreiche ich noch nicht den "AHA"-Effekt. Du schreibt von Kompatibilität. Das heißt, eine Funktion, die mit @Property versehen ist, ist zukunftssicherer? Da mir die geschichtliche Erfahrung in Python fehlt, hänge ich etwas. Wurde nicht schon immer in Python die Funktionen mit einer runden Klammer, also () aufgerufen? Und ich denke, dies würde sich auch nicht ändern oder? Daher stecke ich gerade in deinem Beruf fest. Denn meine beiden Beispiele würden wahrscheinlich auch in Python 3 funktionieren - ich habe es nicht getestet.

Und wenn ein Interface ein Attribut erwartet, dann setzt man vor dem Funktionsaufruf ein Attribut, und der Rückgabewert wird dann in diesem Wert gespeichert und übergibt dann. So habe ich das Attribut mit dem Inhalt, die mir die Funktion zurückgegeben hat.

Ich will dich nicht nerven oder ärgern. Mir ist es persönlich wichtig, dass ich wirklich jeden Schritt kapiere.
BlackJack

@Sophus: Mit kompatibilität ist gemeint das man wenn man eine API hat wo hinter einem einfachen Attributzugriff ein Wert erwartet wird (der keine Methode ist), den aber gerne bei jedem Zugriff berechnen möchte, dann kann man das mit `property()` erreichen. Oder wenn beim Zuweisen mehr passieren soll als eine einfache Zuweisung eines Wertes an ein Attribut. Und man muss sich bei einfachen Attributen keine Gedanken machen dass die später vielleicht mal durch etwas berechnetes ersetzt werden könnten, weil man dann einfach ein Property daraus machen kann und sich für den restlichen Code nichts ändert. Für den sieht das weiterhin aus wie ein einfaches Attribut.

Wenn ein Interface ein Attribut erwartet, dann muss das auch als Attribut vorhanden sein. Vorher eine Funktion aufrufen ist nicht immer möglich weil man nicht immer weiss wann das Attribut abgefragt wird, und das muss ja *immer* den richtigen Wert haben und nicht nur ab und zu wenn man vorher eine Methode aufgerufen hat, und in der Zwischenzeit könnte es auch den falschen Wert haben.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@BlackJack: Das klingt mir zu sperrig und auch zu abstrakt. Kannst du mir ein kleines Beispiel lieferb wo der Sinn einer Property sinnvoll ist und wo nicht? Ich hätte es gern konkreter und nicht so abstrakt.
Benutzeravatar
/me
User
Beiträge: 3561
Registriert: Donnerstag 25. Juni 2009, 14:40
Wohnort: Bonn

Nehmen wir einmal folgende Klasse.

Code: Alles auswählen

class Rectangle:
    def __init__(self):
        self.width = 0
        self.height = 0
        self.area = 0

    def calculate_area(self):
        self.area = self.width * self.height
Damit kann man jetzt Folgendes tun.

Code: Alles auswählen

wall = Rectangle()
wall.height = 2.5
wall.width = 4
wall.calculate_area()
print(wall.area)
Danach macht man dann weiter mit

Code: Alles auswählen

wall.height = 10
print(wall.area)
Mist, Berechnung vergessen. Außerdem wollten wir doch Wände in der Höhe gar nicht zulassen.

Man kann jetzt das Setzen der Werte in eine Funktion auslagern, die dann auch noch die Berechnung automatisch ausführt und vielleicht sogar noch die Werte auf Zulässigkeit überprüft. Damit schafft man allerdings Redundanzen in der Schnittstelle und verhindert immer noch nicht die "falsche" Benutzung. Daher "@property to the rescue!"

Code: Alles auswählen

class Rectangle:
    def __init__(self):
        self._width = 0
        self._height = 0
        self.area = 0

    def calculate_area(self):
        self.area = self._width * self._height

    @property
    def width(self):
        return self._width

    @width.setter
    def width(self, value):
        self._width = value
        self.calculate_area()

    @property
    def height(self):
        return self._height

    @height.setter
    def height(self, value):
        if value > 5:
            raise ValueError('Wall to high ({})'.format(value))
        self._height = value
        self.calculate_area()
Die Schnittstelle nach außen ändert sich nicht und trotzdem wird die Fläche automatisch berechnet und eine Prüfung des für die Höhe übergebenen Wertes findet auch noch statt.

Natürlich sind hier auch andere Implementierungen denkbar. Man könnte in den Settern für width und height auch einfach die area auf None setzen und die Berechnung erst dann bei Bedarf durchführen, wenn der Wert für area abgerufen wird. Dafür müsste dann allerdings auch area ein Property werden, das dann beim Zugriff entsprechend reagiert.
BlackJack

@/me: Wobei ich hier wohl eher nur `height` und `area` zum Property machen würde. `width` braucht dann nichts berechnen, `height` nur seine die Höhe prüfen, und `area` wäre dann nur für seine eigene Berechnung zuständig.
BlackJack

@Sophus: Property macht immer dann Sinn wenn man eine einfache Eigenschaft (englisch „property“) hat, die beim abfragen und/oder setzen eine irgendwie geartete Berechnung benötigt die nicht allzu kompliziert ist. Also überall dort wo man eigentlich ``obj.attribute`` oder ``obj.attribute = something`` schreiben möchte, es aber nicht kann, weil da noch irgendwas passieren muss.

In Deinem Beispiel war dass die Eigenschaft `running` die letztendlich auf den „checked“-Zustand einer Qt-Schaltfläche zurückgreift, die man mit zwei verschiedenen Methoden abfragen und setzen kann/muss.

Praktisches Anwendungsbeispiel mit Rechtecken wäre pygame.Rect. Diese Objekte haben eine ganze Menge von Properties um auf verschiedene Punkte des Rechtecks zuzugreifen oder denen neue Werte zuzuweisen.
Benutzeravatar
Sophus
User
Beiträge: 1109
Registriert: Freitag 25. April 2014, 12:46
Wohnort: Osnabrück

@ \me: Kann es sein, dass in deinem Quelltext ein "Fehler" unterlaufen ist? Egal welche Werte ich eingebe, es kommt immer die Zahl 0 raus :shock:

Ich habe deinen Quelltext genommen um zu analysieren, und deinen Quelltext etwas "erweitert".

Code: Alles auswählen

class Rectangle_1:
    def __init__(self):
        self._width = 0
        self._height = 0
        self.area = 0
 
    def calculate_area(self):
        self.area = self._width * self._height
 
    @property
    def width(self):
        return self._width
 
    @width.setter
    def width(self, value):
        self._width = value
        self.calculate_area()
 
    @property
    def height(self):
        return self._height
 
    @height.setter
    def height(self, value):
        if value > 5:
            raise ValueError('Wall to high ({})'.format(value))
        self._height = value
        self.calculate_area()

if __name__ == "__main__":
    wall = Rectangle_1()
    input_height = raw_input("Enter height: ")
    input_width = raw_input("Enter width: ")
    
    wall.height = float(input_height)
    wall.width = float(input_width)
    
    wall.calculate_area()
    
    print "Area:", wall.area
BlackJack

@Sophus: Der `calculate_area()`-Aufruf ist unnötig. Wenn Du das *so* machen willst, dann brauchst Du keine Properties, aber es ist dann auch fehleranfällig weil `area` den falschen Wert haben kann wenn man vergisst die Neuberechnung aufzurufen. Der Fehler liegt in Deinem Code weil Du Python 2 verwendest, musst Du von `object` erben. /me verwendet offensichtlich Python 3, da erben die Klassen automatisch von `object`.
Antworten