Was ist eine globale Variable und warum soll man sie nicht benützen?

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
kbr
User
Beiträge: 1487
Registriert: Mittwoch 15. Oktober 2008, 09:27

Korrekt schaut es so aus:

Code: Alles auswählen

import test
test.test()
Das ist kurz, lesbar und sauber — so wie es sein soll.
Sirius3
User
Beiträge: 17749
Registriert: Sonntag 21. Oktober 2012, 17:20

@Alfons Mittelmeyer: Gratulation, Du hast entdeckt, dass es Namensräume gibt.

Erst einmal der Normalfall:

Code: Alles auswählen

def define():
    import tkinter as tk
    def test():
        print(tk.TOP)
    return locals()['test']

function = define()
function()
Python erkennt beim Compilieren, dass die Funktion »test« einen Namen aus der übergeordneten Funktion benutzt und legt ein entsprechendes Closure an. Alles funktioniert wie erwartet.

Jetzt Dein Beispiel so umgeschrieben, dass es einfacher nachzuvollziehen ist:

Code: Alles auswählen

def test_exec():
    exec("""
import tkinter as tk
def test():
    print(tk.TOP)
""")
    return locals()['test']
function = test_exec()
function()
Python compiliert den exec-Text unabhängig von der Funktion, in der er aufgerufen wird, die Funktion »test« braucht also den globalen Namen »tk«. Jetzt wird der Code von exec im lokalen Kontext der Function »test_exec« ausgeführt. tk und test landen also in locals(). Davon weiß aber die Funktion »test« nichts und erwartet ein globales »tk«.

Da also exec beim Compilieren nichts von seiner Umgebung wissen kann, kommt es zu solch unerwartetem Verhalten. Man muß also wissen, wie exec funktioniert, bevor man es benutzt.

Da alle Namen im lokalen Kontext geschrieben werden, muß man dafür sorgen, dass der globale dem lokalen Namensraum entspricht:

Code: Alles auswählen

def test_exec():
    exec("""
import tkinter as tk
def test():
    print(tk.TOP)
test()
""", locals())
    return locals()['test']
Dabei gibt es aber noch so einige anderen Stolperfallen, daher sollte man exec nicht benutzen.
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

BlackJack hat geschrieben:Wenn `exec()` nicht sowieso schon etwas wäre was man nicht benutzen sollte wenn es da keinen richtig guten Grund gibt, dann schon gar nicht so das dieses überraschende Verhalten entsteht. Das muss man entweder *sehr* deutlich dokumentieren, oder die `script_exec()`-Funktion so schreiben das sich die Ausführung nicht so unerwartet verhält.
Man kann das zu ladende Script so schreiben, dass es sich nicht besonders unerwartet verhält, etwa so:

Code: Alles auswählen

def main():
    import tkinter as tk

    print(tk.BOTTOM)
    a = 5

    def test():
        print(tk.TOP)
        print(a)
    test()

main()
Allerdings muss man dann imports innerhalb der betreffenden Main Funktion des Scrips durchführen

Und hier wird ganz besonders derr Unterschied zum Modul deutlich, warum man das evt. benützen möchte und warum man keinesfalls so etwas von anderen benützen sollte. Denn das ist das Gegenteil vom Modul. Damit wird nämlich das Mainscript zur Bibliothek für das geladene Script.

test.py:

Code: Alles auswählen

print(tk.BOTTOM)

def test():
    print(tk.TOP)

test()

print(parameters)
test_function()

global global_var
global_var = "Globale Variable"

returnwert = "Returnwert"
Und dazu das passende Main Script:

Code: Alles auswählen

import tkinter as tk

def script_exec(filename,parameters):
    exec(compile(open(filename, "r").read(), filename, 'exec'))
    if 'returnwert' in locals():
        return locals()['returnwert']

def test_function():
    print("Testfunktion")

global_var = 5

returnwert = script_exec('test.py',"Testparameter")
print(returnwert)

print(global_var)
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

Worauf ich mit der `World`-Klasse meinem Schnipsel oben eigentlich hinweisen wollte, war das `self`, was stellvertretend den globalen Kontext führt, wenn man Skripte so layouten würde und `import` dies respektieren würde. Alles objektweit Sichtbare wäre "global" und über `self` erreichbar.

Hilft uns das irgendwie im Umgang mit globalen Zuständen? Nein, da es äquivalent wäre zum jetzigen Vorgehen mit einem kleinem Unterschied - `import` importiert Module als Singleton, d.h. ein gibt nur ein Modul-Exemplar, welches beim ersten Import erstellt und bei weiteren Imports zurückgegeben wird. Diese Festlegung halte ich für sinnvoll, da man idR nur den Quelltext eines Moduls als Abhängigkeit haben möchte (steigert die Nachvollziehbarkeit). Die Tatsache, das Module Singletons sind, erzwingt aber die "alles auf Modulebene muss konstant bleiben"-Regel, da man sonst kontextabhängige Imports hätte, welche sich auch noch auf frühere Imports durchschlagen. Ganz böse, weil überhaupt nicht mehr nachvollziehbar ist, wo plötzlich der geänderte Wert herkommt.

Das Beispiel zeigt noch was anderes auf. Das strenge Weitergeben per Parameterübergabe und Rückgabewerte hilft auch nicht bei der Vermeidung solcher Zustände - solange der Zustand die lokale Ausführung überdauert ist er relativ gesehen immer noch global. Das Problem haben alle mutuable Datentypen (also bis auf ein paar Primitive alles in Python). In OOP versucht man daher die Übersichtlichkeit/Nachvollziehbarkeit hochzuhalten, indem man relevante Zustände möglichst nah an den Aktionen hält - die Erfindung von Klassen. In einer so gekapselten Welt werden Zustandsübertragungen dann lästig/aufwendig (ein Großteil der Entwurfsmuster beschäftigt sich eigentlich nur mit dieser Frage).

Edit: Vllt. wird damit klarer, dass das eigentliche Problem die Kontrollierbarkeit globaler Zustände ist.
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

jerch hat geschrieben:Edit: Vllt. wird damit klarer, dass das eigentliche Problem die Kontrollierbarkeit globaler Zustände ist.
Jede Aktion des Benuters ändert Zustände. Sind dann alle Zustände global, oder geht es vielmehr nicht um globale Zustände, sondern im interne Zustände von Objekten die sich verändern.

So ändert sich etwa die GUI, bei ihrer Initialisierung, das heißt beim GUI Aufbau. Änderungen haben wir immer. Und ob die Objekte als Parameter übergeben werden oder global verfügbar sind, macht da keinen Unterschied.

Es geht nicht um globale Zustände, sondern ob Objekte, welche ihren internen Zustand ändern, global zur Verfügung stehen sollen. Daß sich der interne Zustand kontrolliert ändert, dafür sorgt das Methodeninterface. Wenn so ein Objekt nur einmal zur Verfügung stehen darf, dafür sorgt in python der import und macht dadurch Singletons überflüssig. Eine Klasse daraus zu machen und zu sagen, die darf aber nur einmal instanziert werden, macht keinen Sinn.

Außerdem könnten die internen Klassen für diese Objekte systzemspezifische Parameter erwarten, wie etwa die Bildschirmauflösung, und könnten vielleicht auch erwarten, dass eine Methode aufgerufen wird, wenn der Benutzer im Betriebssystem die Bildschirmauflösung verändert. Solche Interna gehen aber den Benutzer nichts an. Daher erhält er ein fertig initialisiertes Objekt als Interface, bei dem er sich um die Interna nicht zu kümmern braucht.

Viele Bibliotheken stellen ein fertig initialisiertes Objekt zur Verfügen, wie etwa auch matplotlib:

import matplotlib.pyplot as plt

matplotlib liefert das Objekt pyplot. Und daß es ein Objekt ist, sieht man daran:

plt.show()

Was ist jetzt falsch daran, daß dieses Objekt pyplot global zugreifbar ist. Was ist falsch daran dass nicht jeder der es benutzt, pyplot nicht neu instanziert? Was ist falsch daran, wenn sich durch Zeichnen diverser Elemente der interne Zustand verändert, so dass man diesen am Ende mit plt.show ausgeben kann? Ist da ein Problem, daß der interne Zustand von pyplot schlecht kontrollierbar wäre, weil man auf pyplot global zugreifen kann?

PS: Es kann auch nicht angehen, dass ein System beim Hochfahren oder Booten, die dabei erzeugten Objekte oder Interfaces etwaigen Anwendungen , die noch gar nicht da sind, als Parameter übergibt, nur weil es keine globalen Variablen geben sollte.

Wenn ein Objekt nur auf C++ Ebene existieren würde, und in der python API darauf das Objekt nicht sichtbar wäre, dass dürfte wohl schon sein, weil es keine Python Variable wäre?
BlackJack

@Alfons Mittelmeyer: Das es sich um ein Objekt handelt sieht man daran das es ein Wert ist der an einen Namen gebunden werden kann. Alles was man in Python an einen Namen binden kann ist ein Objekt. Also auch Module. Das ist also eine eher triviale Erkenntniss. `plt.show` ist auch ein Objekt.

Mit `pyplot` hast Du ein Beispiel das nicht wirklich als Beispiel taugt, denn das verstösst in Teilen der API massiv gegen die Richtlinie keine globalen Zustände zu haben. Was daran falsch ist mag ich jetzt nicht noch mal erörtern, wir haben das ja nun echt zu genüge durchgekaut.

Die API von `pyplot` ist unter anderem als ”Ersatz” für den Plotting-Teil von Matlab gedacht, das sozusagen die Vorgabe darstellt. Das Modul soll es Umsteigern von Matlab einfach machen. Damit übernimmt es auch die Entwurfsentscheidung, den Standardfall wenn man damit interaktiv arbeitet, also an einem globalen Plot zu einem gegeben Zeitpunkt zu arbeiten, als Funktionen die auf eben diesem globalen Plot zu operieren, anzubieten. Das ist praktisch wenn man mit einer Shell, Spyder, oder einem Juyter-Notebook arbeitet, hat aber die ganzen Probleme die man sich mit globalem Zustand einhandelt wenn man das in einem Programm verwenden möchte das sich nicht an diesen simplen Programmfluss halten kann oder will. Und dann bietet die Bibliothek eine ziemlich modular aufgebaute Struktur bei der alles mögliche austauschbar ist und durchgereicht wird, so wie das in einem sauberen Programm sein sollte. Die Bibliothek eignet sich also eher weniger als Beleg für Deinen Ansatz. Man hat zwar quasi *ein* Exemplar das global ist, aber das besteht aus sauber aufgebautem Code der einem auch erlaubt nichtglobale Zustände zu erstellen und zu verwenden.
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

BlackJack hat geschrieben:Die Bibliothek (matplotlib) eignet sich also eher weniger als Beleg für Deinen Ansatz. Man hat zwar quasi *ein* Exemplar das global ist, aber das besteht aus sauber aufgebautem Code der einem auch erlaubt nichtglobale Zustände zu erstellen und zu verwenden.
Genau darum geht es aber, um sauber aufgebauten Code, der gerade ein Interface zur Entkopplung von Interna der Implementierung erlaubt und so erst richtig saubere Programmierung ermöglicht. Es kann nämlich eine kreuz und quer Programmierung nicht angehen. Kein GUI Element hat nach einem anderen zu suchen, um dort einzutragen, welche Methode dieses bei ihm aufrufen soll. Da wäre auch das Problem, welches der beiden GUI Elemente zuerst existiert, auf welchem Pfad man da gehen muß, und ob die Namen nicht irgendwann geändert werden. Das gäbe einen ganz schönen Verhau. Ich habe natürlich eine Klasse, die auch mehrmals instanziert werden darf und auch dafür gedacht ist. Nur die GUI soll genau auf ein und dieselbe Instanz zugreifen, die speziell für die GUI vorgesehen ist. Bisher hatte ich da ein globales Objekt erzeugt. Das Objekt an alle GUI Container als Parameter bei der Instanziereung zu übermitteln, erscheint mir keine gute Idee zu sein. Aber jetzt ist mir eine Lösung eingefallen.

Wie bisher wird das Objekt bei der Instanzierung der root ebenso instanziert, nämlich bei:

mytk.Tk()

Nur könnte man statt das Objekt global zu haben, es an die Root binden. Und bei weiterer Instanzierung anderer Gui Elemente, wird dann die Referenz jeweils vom Parent übernommen. Jedes GUI Element braucht dann nicht ein globales object_for_gui.methode() aufrufen, sondern kann aufrufen:

self.object_for_gui.methode()

Wäre das jetzt die richtige Art und Weise?
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

@Blackjack: bisher hatte ich allerdings gedacht, es wäre eine gute Idee, global vorhancdene Objekte zu verbergen, indem man nur Funktionen nach außen hat, welche auf dem Objekt arbeiten.

So wird in der Gui nicht aufgerufen:

mytk.objekt_for_gui.methode()

sondern nur:

mytk.function_for_gui()

Natürlich steht das für andere instanzierte Objekte der gleichen Bauart zur Verfügung:

object_for_anderes.methode()

Ich hatte gedacht so etwas wäre eine gute Idee, einfach Funktionen zu haben, wie etwa auch:

os.fchmod(fd, mode)

Natürlich existiert dazu auch ein globales Objekt, das allerdings in der Systemsoftware verborgen ist, nämlich das Filesystem. Würde es Sinn machen, dieses Objekt mit anzugeben, etwa:

os.system_filesystem.fchmod(fd, mode)

Oder sollte man das dann besser instanzieren, etwa:

my_filesystem=os.FileSystem()

Und trägt dann im Anschluß die Files der Festplatte ein? Das wäre dann wohl richtig objektorientiert?
BlackJack

@Alfons Mittelmeyer: Wie gesagt wird da nicht wirklich etwas verborgen denn Module sind auch Objekte. Also ist grundsätzliche erst einmal weder das eine noch das andere mehr oder weniger, oder richtiger objektorientiert.

Der Nachteil wenn man ein Modul so aussehen lässt als wäre es zusätzlich noch ein Objekt eines bestimmten Typs, ist dass man das nur einmal machen kann beziehungsweise sollte, um nicht mehr als eine API auf dem gleichen Objekt zu mischen. Selbst das stimmt ja schon nicht wenn das Modul zusätzlich noch andere Funktionen und Klassen als Attribute zur Verfügung stellt. Dann hat man mehr als eine API auf gleicher Ebene und der Benutzer muss auseinanderhalten können was nun ”Methode” oder beispielsweise eine unabhängige Funktion ist, die nichts mit dem ”Exemplar” des Objekts zu tun hat, dessen Rolle das Modul spielt.

Das `os`-Modul hat wieder nichts mit eigenständigen Entscheidungen der Entwickler bezüglich der API zu tun und auch nichts mit OOP. Das sind einfach recht dünne Wrapper um einen Haufen C-Funktionen die üblicherweise auf Betriebssystemen vorhanden sind.

Objektorientierter Umgang mit Pfaden/Dateien wäre beispielsweise das `pathlib`-Modul aus der Standardbibliothek (Python 3 oder Backport für Python 2) oder das externe Unipath-Package.
jerch
User
Beiträge: 1669
Registriert: Mittwoch 4. März 2009, 14:19

@Alfons Mittelmeyer:
Die Funktionen im os-Modul sind sehr speziell, da sie fast alle nicht auf prozessinterne Zustände sondern OS Ressourcen zugreifen. Defacto sind es globals, welche sich komplett Deiner Kontrolle entziehen (jederzeit den Zustand ändern könnten) und müssen daher mit besonderer Sorgfalt benutzt werden.

Simples Bsp. - Schreiben in eine Datei. Du öffnest einmal in Deinem Programmcode eine Datei zum Schreiben und nutzt das Dateiobjekt später im Code:

Code: Alles auswählen

f = open('/tmp/blabla', 'w')

# some code
...
f.write(foo)
...
f.write(bar)
...
f.close()
Während Dein Programm läuft, löscht nun das OS die Datei (geht unter POSIX-Systemen, unter Windows sollte das blockieren, nicht getestet). Unter Linux merkt Dein Programm nichts davon, der Output ist aber verloren. Weil der Output aber wichtig ist, verhält sich Dein Programm fehlerhaft. Die Ressource, auf die `f` zeigt, hat quasi den Zustand geändert und Dein Programm hat es nicht bemerkt. Wie Du das Problem umschiffen bzw. minimieren kannst, fällt Dir bestimmt selbst ein ;)
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

Habe jetzt einen Weg gefunden, etwas zu speichern ohne globale Variable:

Code: Alles auswählen

def do_liste(value,liste=[]):
    liste.append(value)
    print(liste)

do_liste(1)
do_liste(2)
do_liste(2)
Und was noch besser ist: niemand kann von außen darauf zugreifen, es sei denn man implementiert noch ein Interface, dann aber nur über das Interface.

Und kaputt geht es auch nicht:

Code: Alles auswählen

def do_liste(value,liste=[]):
    liste.append(value)
    print(liste)

do_liste(1)
do_liste(2)
do_liste(3,[])
do_liste(4)
BlackJack

@Alfons Mittelmeyer: Juhuu, Du hast noch einen Weg gefunden globalen Zustand zu speichern. Toll. Und dann auch noch einen der ziemlich sinnfrei ist, weil man nicht mehr so einfach an den Zustand heran kommt. Cool. :lol:
__deets__
User
Beiträge: 14539
Registriert: Mittwoch 14. Oktober 2015, 14:29

:o :o :o :o :o :o :o :o :o :o :o

Alfons: GLOBALER ZUSTAND IST ALLER ZUSTAND, DER EINE FUNKTION NICHT ALS UEBERGEBENER PARAMETER ERREICHT. DIE VERWENDUNG DES SCHLUESSELWORTES "GLOBAL" HAT NICHTS DAMIT ZU TUN. UND EIN DEFAULTWERT IST NICHT UEBERGEBEN.

Ausdrucken und ueber das Bett an die Decke haengen.
Benutzeravatar
kbr
User
Beiträge: 1487
Registriert: Mittwoch 15. Oktober 2008, 09:27

Alfons Mittelmeyer hat geschrieben:Und was noch besser ist: niemand kann von außen darauf zugreifen, es sei denn man implementiert noch ein Interface, dann aber nur über das Interface.
Das geht auch ohne Interface:

Code: Alles auswählen

do_liste.__defaults__[0]
Da steht alles drin und von überall, wo 'do_liste' bekannt ist, lässt sich darauf zugreifen. Nicht nur lesend:

Code: Alles auswählen

>>> do_liste.__defaults__[0]
[1, 2] 
>>> do_liste.__defaults__[0].pop()
2
>>> do_liste(3)
[1, 3]
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

@kbr: anscheinend legt es python darauf an, daß etwas zu verbergen kaum geht. Aber verbergen kann man natürlich schon:

Code: Alles auswählen

def should_be_known_by_two_classes(key,parameters,obj=SomeClass()):
    # ...
    return something

ShouldKnow1()
ShouldKnow2()

should_be_known_by_two_classes = None

ShouldntKnow()
Wäre die Funktion da noch global?
Sirius3
User
Beiträge: 17749
Registriert: Sonntag 21. Oktober 2012, 17:20

@Alfons Mittelmeyer: nein, verbergen geht in Python definitiv NICHT! Funktionen sind meist global, das ist aber auch kein Problem, da sie konstant sind. Im Normalfall. BEI DIR ABER NICHT. Das Beispiel ist nicht lauffähig, man kann daher nicht sagen, was Du damit bezwecken willst. Ich vermute aber, nichts sinnvolles.
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

Sirius3 hat geschrieben:@Alfons Mittelmeyer: ... man kann daher nicht sagen, was Du damit bezwecken willst. Ich vermute aber, nichts sinnvolles.
Ich will ein globales Objekt vermeiden. Das wäre ja wohl ein globales Object oder, oder nicht?

Code: Alles auswählen

obj = SomeObject()

ShouldKnow1(obj)
ShouldKnow2(obj)

obj = None
ShouldntKnow()
Sirius3
User
Beiträge: 17749
Registriert: Sonntag 21. Oktober 2012, 17:20

@Alfons Mittelmeyer: auch alle Deine anderen Ideen führen zu globalen Objekten.
Alfons Mittelmeyer
User
Beiträge: 1715
Registriert: Freitag 31. Juli 2015, 13:34

Jetzt habe ich es einmal anders probiert, nämlich in einer Funktion als lokale Variable übergeben, aber es ist immer noch global da:

Code: Alles auswählen

class ShouldKnow():
    def __init__(self,obj):
        self.obj = obj

    def output(self):
        print(self.obj)


def function():
    # ich definiere das objekt in einer Funktion
    obj = [1,2,3]
    return ShouldKnow(obj)

a = function()

# aber es ist immer noch global da
a.output()
Benutzeravatar
kbr
User
Beiträge: 1487
Registriert: Mittwoch 15. Oktober 2008, 09:27

Alle Objekte leben irgendwo im Speicher. Entscheidend ist, in welchem Namensraum sich die Referenzen zu diesen Objekten befinden. Du erzeugst eine Referenz auf globaler Ebene und wunderst Dich, das eine globale Verfügbarkeit besteht.
Antworten