expertenaustausch > comp.lang.* > comp.lang.python

Ole Streicher (15.09.2010, 13:59)
Hallo,

ich möchte meine Keyword-Parameternamen gerne gruppieren, also z.B

def myfunc(**ndata):
print('x.y ist %s' % ndata['x.y'])

myfunc(x.y = 'ergolgreich')

Leider klappt das nicht; Python beschwert sich darüber:

SyntaxError: keyword can't be an expression

Warum ist das so, kann man das umgehen und wie würde man seine Parameter
hier sonst sinnvoll gruppieren?

Das hier:

def myfunc(**ndata):
print('x_y ist %s' % ndata['x_y'])

myfunc(x_y = 'ergolgreich')

funktioniert, sieht aber nicht besonders elegant aus.

Der Grund ist, dass "myfunc" bei mir ein callable Klasse ist, mit der
ich eigentlich sowas machen will:

myfunc = MyFunc()
myfunc.x.y = 'Defaultwert'
myfunc() # verwendet x.y
myfunc(x.y = 'Anderer Wert') # überschreibt x.y

Es gibt dabei mehrere (Einsatzabhängig) "x"-Objekte, die jeweils viele
(>5) "y"-Attribute haben. Vielleicht gibt es hier ja einen cleveren
"Python"-Weg.

Viele Grüße

Ole
Diez B. Roggisch (15.09.2010, 14:19)
Ole Streicher <ole-usenet-spam> writes:

> Hallo,
> ich möchte meine Keyword-Parameternamen gerne gruppieren, also z.B
> def myfunc(**ndata):
> print('x.y ist %s' % ndata['x.y'])
> myfunc(x.y = 'ergolgreich')
> Leider klappt das nicht; Python beschwert sich darüber:
> SyntaxError: keyword can't be an expression
> Warum ist das so, kann man das umgehen und wie würde man seine Parameter
> hier sonst sinnvoll gruppieren?


Das kann man nicht umgehen, weil fuer Parameter nunmal dieselben Regeln
gelten wie fuer Attributzugriff - es muss ein identifier sein.

Sonst koenntest du ja sowas machen:

def foo(parameter_name_abhaenging_von_relativer_mondfe uchte()):
# und was genau schreibst du jetzt hier um auf deinen berechneten
# namen zuzugreifen? siehste.

Sinnvolle Gruppierungen ergeben sich durch Objekte, wie zB Bunch oder dictionaries.

[..]
> myfunc.x.y = 'Defaultwert'
> myfunc() # verwendet x.y
> myfunc(x.y = 'Anderer Wert') # überschreibt x.y


myfunc.x(y="Anderere Wert")

laesst sich sicher machen, wenn man eben x als Objekt begreift.

Diez
Ole Streicher (15.09.2010, 14:52)
Hallo Diez,

deets (Diez B. Roggisch) writes:
> Ole Streicher <ole-usenet-spam> writes:
>> myfunc(x.y = 'ergolgreich')
>> SyntaxError: keyword can't be an expression


> myfunc.x(y="Anderere Wert")


> laesst sich sicher machen, wenn man eben x als Objekt begreift.


x ist auch ein Objekt.

Etwas konkreter: Ich habe eine Funktion, die mir aus einer
Eingangsgröße, eine Reihe von Kalibrationsparametern und einigen
Verarbeitungsparametern ein Resultat berechnet. Die
Kalibrationsparameter und die Verarbeitungsparameter sind dabei zu
jeweils einem eigenen Objekt zusammengefasst, welches ein Attribut des
callable ist. Die Funktion selbst (und die möglichen Parameter) werden
dabei aus einer shared library ausgelesen.

Ein beispiel wäre:

input_data = ...

mufunc = MyFunction('mufunc')
mufunc.param.x_min = 12
mufunc.param.x_max = 22
mufunc.calib.gain = 1.02
mufunc.calib.offset = 0.7

# Ergebnis mit Standardwerten
res1 = mufunc(input_data)

# Zum Vergleich das gleiche mit anderem Gain
res2 = mufunc(input_data, calib.gain = 1.07)

Das sieht sehr intuitiv aus und wäre m.E. das für den Nutzer optimale
Interface. Leider geht das eben nicht, sondern nur

res2 = mufunc(input_data, calib_gain = 1.07)

was bei weitem nicht so intuitiv aussieht: warum sollte man hier einen
underscore verwenden?

> def foo(parameter_name_abhaenging_von_relativer_mondfe uchte()):
> # und was genau schreibst du jetzt hier um auf deinen berechneten
> # namen zuzugreifen? siehste.


Der Teil ist ja schon fertig und funktioniert zufriedenstellend.
Etwa so:

def __call__(self, *data, **ndata):
params = dict([ (par.name, par.value) for par in self.params ]) #1
for key, value in ndata.iteritems():
if key.startswith('param_'):
params[key.split('_',1)[1]] = value #2
self._call_the_library_func(params)

Zuerst wird ein dict mit allen Default-Parametern erzeugt (#1), dann
werden alle Parameter überschrieben, die beim Aufruf direkt angegeben
wurden (#2). In der verkürzten Form fehlt natürlich die Fehlerbehandlung.

Ich könnte die beiden Underscores ohne weiteres durch einen Punkt
ersetzen und hätte das gewünschte Verhalten, wenn mir der Python-Parser
hier keinen unnötigen Strich durch die Rechnung machen würde.

Viele Grüße

Ole
Diez B. Roggisch (15.09.2010, 15:30)
> Ich könnte die beiden Underscores ohne weiteres durch einen Punkt
> ersetzen und hätte das gewünschte Verhalten, wenn mir der Python-Parser
> hier keinen unnötigen Strich durch die Rechnung machen würde.


Es ist eben kein unnoetiger Strich, sondern die notwendige Verhinderung
einer semantischen Ambiguitaet.

foo.bar.baz

ist nunmal das Attribut baz aus dem Attribut bar von foo. Nicht das
Attribut "bar.baz" aus foo.

Du gehst bei deinen Ueberlegungen nur von deinem Ausschnitt der
Geschichte aus, aber eine Funktion, die kwargs nimmt, muss eben auch
ohne **-argument aufrufbar sein. Und damit muesste dann der Forderung
nach eine gepunkteten Parameter-Namens-Notation stattgegeben werden, und
dann sind wir bei obigem Beispiel.

Wenn du gruppieren willst, dann kann man eben sowas machen

def foo(x=p(y=10, z=20)):
...

wobei p ein Bunch-maessiges Objekt ist. Mehr geht nicht.

Diez
Ole Streicher (15.09.2010, 15:45)
Hallo Diez,

deets (Diez B. Roggisch) writes:
> Du gehst bei deinen Ueberlegungen nur von deinem Ausschnitt der
> Geschichte aus, aber eine Funktion, die kwargs nimmt, muss eben auch
> ohne **-argument aufrufbar sein.


Sie ist ja auch ohne aufrufbar, wie mein Default-Beispiel zeigt.

> Und damit muesste dann der Forderung nach eine gepunkteten
> Parameter-Namens-Notation stattgegeben werden, und dann sind wir bei
> obigem Beispiel.


Warum das? Klar kann ich den Aufruf

y = foo(x, calib.gain = 5.2)

nur dann machen, wenn foo kwargs unterstützt, aber ich sehe nicht, wo da
das Problem sein sollte?

> Wenn du gruppieren willst, dann kann man eben sowas machen
> def foo(x=p(y=10, z=20)):
> ...
> wobei p ein Bunch-maessiges Objekt ist. Mehr geht nicht.


Das sieht IMO nicht weniger häßlich aus als meine Variante mit den
Underscores.

:-(

Viele Grüße

Ole
Diez B. Roggisch (15.09.2010, 16:40)
Ole Streicher <ole-usenet-spam> writes:

> Hallo Diez,
> deets (Diez B. Roggisch) writes:
>> Du gehst bei deinen Ueberlegungen nur von deinem Ausschnitt der
>> Geschichte aus, aber eine Funktion, die kwargs nimmt, muss eben auch
>> ohne **-argument aufrufbar sein.

> Sie ist ja auch ohne aufrufbar, wie mein Default-Beispiel zeigt.


Ich weiss nicht, was genau du mit Default-Beispiel meinst. Aber

foo(x.y=10)

ist nicht moeglich. Denn dann aendert sich die Semantik von

x.y = 10

abhaenging davon, ob du dich in einem Funktions-Scope befindest, der
eben genau diesen Namen deklariert hat, oder nicht.

Und das waere *Wahnsinn*.

> Warum das? Klar kann ich den Aufruf
> y = foo(x, calib.gain = 5.2)
> nur dann machen, wenn foo kwargs unterstützt, aber ich sehe nicht, wo da
> das Problem sein sollte?


Das hat mit kwargs oder nicht kwargs ueberhaupt nichts zu tun in dem
Sinne. Es geht darum, dass Python einem Ausdruck der Form

x.y

nunmal nur *eine* Bedeutung zuordnet. Warum, siehe oben.

> Das sieht IMO nicht weniger häßlich aus als meine Variante mit den
> Underscores.
> :-(


Ja, das Leben kann grausam und gemein sein.

Diez
Ole Streicher (15.09.2010, 22:12)
Hallo Diez,

deets (Diez B. Roggisch) writes:
> Ole Streicher <ole-usenet-spam> writes:
> foo(x.y=10)
> ist nicht moeglich. Denn dann aendert sich die Semantik von
> x.y = 10
> abhaenging davon, ob du dich in einem Funktions-Scope befindest, der
> eben genau diesen Namen deklariert hat, oder nicht.


Das verstehe ich nicht. Kannst Du das mal genauer erklären? Auch von
"y = 10" ändert sich die Semantik, je nachdem ob man sich in einem
Funktions-Scope befindet, der genau diesen Namen deklariert hat.

Wo ist der Unterschied?

> Das hat mit kwargs oder nicht kwargs ueberhaupt nichts zu tun in dem
> Sinne. Es geht darum, dass Python einem Ausdruck der Form


> x.y


> nunmal nur *eine* Bedeutung zuordnet. Warum, siehe oben.


Sie ordnet ja dem Ausdruck "x" auch mehrere Bedeutungen zu.

>>> Wenn du gruppieren willst, dann kann man eben sowas machen
>>> def foo(x=p(y=10, z=20)):

>> Das sieht IMO nicht weniger häßlich aus als meine Variante mit den
>> Underscores. :-(


> Ja, das Leben kann grausam und gemein sein.


.... und Programmiersprachen sind dafür da, Abhilfe zu schaffen. Die
Frage ist nur, wie man das am besten anstellt ;-)

Viele Grüße

Ole
Florian Diesch (15.09.2010, 23:11)
Ole Streicher <ole-usenet-spam> writes:

[..]
> Das sieht sehr intuitiv aus und wäre m.E. das für den Nutzer optimale
> Interface. Leider geht das eben nicht, sondern nur
> res2 = mufunc(input_data, calib_gain = 1.07)


Ich würde vermutlich dicts nehmen, z.B.

res2 = mufunc(input_data, calib={'gain': 1.07, 'offset': 0.7},
param={'x_min': 12, 'x_max': 22})

Florian
Diez B. Roggisch (15.09.2010, 23:11)
Ole Streicher <ole-usenet-spam> writes:

> Hallo Diez,
> deets (Diez B. Roggisch) writes:
> Das verstehe ich nicht. Kannst Du das mal genauer erklären? Auch von
> "y = 10" ändert sich die Semantik, je nachdem ob man sich in einem
> Funktions-Scope befindet, der genau diesen Namen deklariert hat.


Nein, das tut sie nicht. Die Semantik von

n = <ausdruck>

ist immer "weise den Wert <ausdruck> dem Namen n im aktuellen scope
zu".

x.y = <ausdruck>

weist dem attribut y des objektes das an x gebunden ist <ausdruck> zu.

Im Gegensatz dazu moechtest du jetzt mal den Namen "x.y" im aktuellen
Scope aendern, und mal das Attribut y auf x setzen. Das ist ein
gigantischer Unterschied.

>> Ja, das Leben kann grausam und gemein sein.

> ... und Programmiersprachen sind dafür da, Abhilfe zu schaffen. Die
> Frage ist nur, wie man das am besten anstellt ;-)


Ja, aber da gibt es Design-Entscheidungen zu faellen, und vor allem: mit
denen zu leben, die gefaellt wurden....

Diez
Georg Brandl (18.09.2010, 09:19)
Am 15.09.2010 14:52, schrieb Ole Streicher:

> Ich könnte die beiden Underscores ohne weiteres durch einen Punkt
> ersetzen und hätte das gewünschte Verhalten, wenn mir der Python-Parser
> hier keinen unnötigen Strich durch die Rechnung machen würde.


Wie Diez schon sagt, muss das ganze auch ohne **kwds-Aufruf funktionieren,
denn **kwds sind eine (sehr nützliche) Erweiterung zu normalen Parametern,
nicht andersherum.

Wenn also nun dies möglich sein soll:

def foo(x.y):
print x.y

muss "x" ein Objekt mit Attribut "y" sein. D.h. es muss eine neue Klasse
eingeführt werden, die speziell für solche Argumente verwendet wird. Der
Compiler muss alle Argumentlisten durchgehen und speziellen Code für solche
Funktionen generieren, der die "gepunkteten" Argumente in eine oder mehrere
Instanzen dieser Klasse packt. Desweiteren muss bei jedem Aufruf einer
Funktion abgeklärt werden, ob diese die übergebenen gepunkteten Argumente
auch versteht.

Das ganze ist also alles andere als unnötig; es bedeutet einen gewaltigen
Mehraufwand für den Interpreter für ein mehr als zweifelhaftes Feature,
nämlich: beliebige Datenstrukturen via **kwds zu übergeben.

Georg
Ole Streicher (18.09.2010, 12:18)
Hallo Georg,

Georg Brandl <g.brandl-nospam> writes:
>> Ich könnte die beiden Underscores ohne weiteres durch einen Punkt
>> ersetzen und hätte das gewünschte Verhalten, wenn mir der Python-Parser
>> hier keinen unnötigen Strich durch die Rechnung machen würde.


> Wie Diez schon sagt, muss das ganze auch ohne **kwds-Aufruf funktionieren,
> denn **kwds sind eine (sehr nützliche) Erweiterung zu normalen Parametern,
> nicht andersherum.


Sie mag historisch als Erweiterung entstanden sein; ich sehe aber
einfach zwei Möglichkeiten der Parameterübergabe: einmal über formale
Parameter, einmal über **kwargs. Beide haben spezielle Vor- und
Nachteile und Beschränkungen, eine davon könnte die Punktnotation sein.

> Wenn also nun dies möglich sein soll:
> def foo(x.y):
> print x.y


Darum geht es ja nicht. Es reicht ja, dass möglich ist:

def foo(**kwargs):
print kwargs['x.y']

> Das ganze ist also alles andere als unnötig; es bedeutet einen gewaltigen
> Mehraufwand für den Interpreter für ein mehr als zweifelhaftes Feature,
> nämlich: beliebige Datenstrukturen via **kwds zu übergeben.


Ich will ja nicht beliebige Datenstrukturen haben, sondern die
Möglichkeit, einen Punkt im Namen anzugeben. Wenn man am Parser vorbei
programmiert, geht das ja:

def foo(**kwargs):
print kwargs.get('z')
print kwargs.get('x.y')

def bar(z = None):
print z

data1 = {'z':'Hello'}
data2 = {'x.y':'world', 'z':'Hello'}

foo(**data1)
bar(**data1)
foo(**data2)

Würde Dein Argument zutreffen, dass formale Parameter und kwargs gleich
behandelt, wir müsste ich dann wohl "bar" definieren, um den gleichen
Output wie "foo(**data2)", aber mit formalen Parametern zu erzielen?

Viele Grüße

Ole
Ole Streicher (18.09.2010, 12:23)
Hallo Florian,

Florian Diesch <diesch> writes:
>> res2 = mufunc(input_data, calib_gain = 1.07)

> Ich würde vermutlich dicts nehmen, z.B.
> res2 = mufunc(input_data, calib={'gain': 1.07, 'offset': 0.7},
> param={'x_min': 12, 'x_max': 22})


Gute Idee; werde ich wahrscheinlich zusätzlich einbauen. Das Problem
ist, dass man i.allg. nur 1-2 Parameter direkt ändert, und da wirkt
Deine Schreibweise noch etwas aufgeblähter als der Underscore:

for gain in (0.9, 0.96, 0.33):
# res0 = mufunc(input_data, calib.gain = gain)
res1 = mufunc(input_data, calib = { 'gain':gain })
res2 = mufunc(input_data, calib_gain = gain)

Für einen "Gelegenheitsnutzer", eventuell vielleicht sogar noch
interaktiv, ist das schwer nachzuvollzieuen bzw. zu merken.

Viele Grüße

Ole
Georg Brandl (18.09.2010, 12:42)
Am 18.09.2010 12:18, schrieb Ole Streicher:
> Hallo Georg,
> Georg Brandl <g.brandl-nospam> writes:
> Sie mag historisch als Erweiterung entstanden sein; ich sehe aber
> einfach zwei Möglichkeiten der Parameterübergabe: einmal über formale
> Parameter, einmal über **kwargs. Beide haben spezielle Vor- und
> Nachteile und Beschränkungen, eine davon könnte die Punktnotation sein.
> Darum geht es ja nicht. Es reicht ja, dass möglich ist:
> def foo(**kwargs):
> print kwargs['x.y']


Das ist es ja auch; allerdings eher aus dem Umstand heraus, dass es sich
nicht lohnt, die Keys in einem **dict auch noch darauf zu überprüfen, ob
sie Identifier sind. (Darauf, dass sie Strings sind, werden sie überprüft.)
Wollte man korrekt sein, müsste man das tun; der Performance willen werden
beliebige Strings "geduldet", funktionieren halt aber auch nur, wenn
"auf der anderen Seite" ein formaler **kwds-Parameter vorhanden ist.

**kwds sind nunmal dazu da, *Parameter* (im Aufruffall) gesammelt zu
übergeben, bzw. (im Definitionsfall) zu sammeln (um sie z.B. gesammelt
wieder an eine andere Funktion zu übergeben). Nicht, um beliebige Daten
zu übergeben, was ein einzelnes dict-Argument einwandfrei leistet.

Georg
Ole Streicher (18.09.2010, 12:51)
Hallo Diez,

deets (Diez B. Roggisch) writes:
> x.y = <ausdruck>
> weist dem attribut y des objektes das an x gebunden ist <ausdruck> zu.


Das stimmt schon bei Properties nicht mehr. Nicht gerade schön, aber
ohne weiteres möglich:

class Foo(object):
def _set_x(self, v):
self._x = v
def _get_x(self):
return self._x
def _set_y(self, v):
self._y = v
def _get_y(self):
return self._y

a = property(get_x, set_y)
b = property(get_y, set_x)

bar = Foo()
bar.a = 'a'
bar.b = 'b'
print bar.a, bar.b
'b' 'a'

:-)

> Im Gegensatz dazu moechtest du jetzt mal den Namen "x.y" im aktuellen
> Scope aendern, und mal das Attribut y auf x setzen. Das ist ein
> gigantischer Unterschied.


Es ist dabei aber sehr klar, wann was passiert: in einem Funktionsaufruf
erfolgt ja keine Zuweisung von Objekten, sondern eine Zuordnung formaler
und aktualer Parameter. 'foo(x = 5)' weist ja für sich genommen nicht
einem Objekt x den Wert 5 zu; dies passiert erst im Zusammenspiel mit
'def foo(x = None):'. In 'def foo(**kwardg):' gibt es diese Zuweisung
gar nicht; da erfolgt die Bildung eines dict, das erstmal keine
Beschränkungen des Keys kennt.

> Ja, aber da gibt es Design-Entscheidungen zu faellen, und vor allem: mit
> denen zu leben, die gefaellt wurden....


Dass man damit leben muss, ist schon klar. Allerdings kann man sie
infrage stellen. Mir ist noch nicht ganz klar, wo eigentlich die
Gefahren bei meinem Vorschlag bestehen.

Dagegen sehe ich einige Vorteile: Es gibt eine Reihe von Paketen (mir
fällt z.B. gerade matplotlib ein), wo eine Funktion eine Reihe weiterer
Funktionen aufruft. Wenn man da z.B. eine Linie plotten will, muss man
(in einem Aufruf) die Möglichkeit der Spezifikation der Linienfarbe,
Linienstärke, Markerfarbe, Markerrandfarbe, Markerrandstärke
usw. angeben können. Das bedingt eine eigene Verwaltung von
Namensräumen:

axes.plot(x, y, linecolor = 'b', linewidth = 2, markercolor = 'r',
markeredgewidth = 1, ...)

was verwischt, dass "linecolor" und "linewidth" eigentlich
zusammengehören. Das macht die Beschreibung lang und unanschaulich, weil
man bei axes.plot ALLE Möglichkeiten für alle möglichen Komponenten
angeben muss.

Würde man das strukturieren können:

axes.plot(x, y, line.color = 'b', line.width = 2, marker.color = 'r',
marker.edge.width = 1, ...)

könnte man in der API schreiben:

Arguments:
line.*: line specification, see (link)
marker.*: marker specification, see (link)

Das geht zwar auch mit einem Underscore (oder ohne jedes Trennzeichen),
aber ein Punkt macht eben eine Strukturierung deutlich.

Die Parameter lassen sich in verschiedene Namensräume einteilen; warum
sollte das nicht dem User auch mitgeteilt werden? Syntaktischer Zucker
ist doch gerade eine der Stärken von Python.

Viele Grüße

Ole
Georg Brandl (18.09.2010, 13:09)
Am 18.09.2010 12:51, schrieb Ole Streicher:
> Hallo Diez,
> deets (Diez B. Roggisch) writes:
>> x.y = <ausdruck>
>> weist dem attribut y des objektes das an x gebunden ist <ausdruck> zu.

> Das stimmt schon bei Properties nicht mehr. Nicht gerade schön, aber
> ohne weiteres möglich:


Doch, das stimmt schon. Nur dass eben das Objekt x eine Einflussmöglichkeit
darauf hat, was genau dieses "zuweisen" bedeutet. Der springende Punkt ist,
dass x ein Objekt sein muss.

> Es ist dabei aber sehr klar, wann was passiert: in einem Funktionsaufruf
> erfolgt ja keine Zuweisung von Objekten, sondern eine Zuordnung formaler
> und aktualer Parameter. 'foo(x = 5)' weist ja für sich genommen nicht
> einem Objekt x den Wert 5 zu; dies passiert erst im Zusammenspiel mit
> 'def foo(x = None):'. In 'def foo(**kwardg):' gibt es diese Zuweisung
> gar nicht; da erfolgt die Bildung eines dict, das erstmal keine
> Beschränkungen des Keys kennt.
> Dass man damit leben muss, ist schon klar. Allerdings kann man sie
> infrage stellen. Mir ist noch nicht ganz klar, wo eigentlich die
> Gefahren bei meinem Vorschlag bestehen.


Er verletzt nunmal die Symmetrie zwischen Definition und Aufruf.

[..]
> Würde man das strukturieren können:
> axes.plot(x, y, line.color = 'b', line.width = 2, marker.color = 'r',
> marker.edge.width = 1, ...)


Und wie sieht die Funktionsdefinition dazu aus?

> könnte man in der API schreiben:
> Arguments:
> line.*: line specification, see (link)
> marker.*: marker specification, see (link)
> Das geht zwar auch mit einem Underscore (oder ohne jedes Trennzeichen),
> aber ein Punkt macht eben eine Strukturierung deutlich.


Der Vorteil ist also einfachere Dokumentation? Das ist nicht gerade ein
Wahnsinnsargument.

> Die Parameter lassen sich in verschiedene Namensräume einteilen; warum
> sollte das nicht dem User auch mitgeteilt werden? Syntaktischer Zucker
> ist doch gerade eine der Stärken von Python.


Das würde ich nicht so sagen. Eher von vornherein einfache, klare Syntax.
Mit Zucker hat das nichts zu tun, denn das wäre die Einführung von vielen
Spezialfällen für häufig benutzten Code. Das gibt es natürlich schon
(z.b. List Comprehensions fallen darunter), ist aber kein Designprinzip.

Georg

Ähnliche Themen