[linux-l] typdynamische Sprachfeatures

Volker Grabsch vog at notjusthosting.com
Do Sep 29 02:35:23 CEST 2005


On Wed, Sep 28, 2005 at 09:58:22PM +0200, Axel Weiß wrote:
> Volker Grabsch schrieb:
> > 	p = Person()
> > 	p.__dict__['name'] = "Hans" # Das geht!
> > 	hello(p)
> >
> > Nun funktioniert alles. Selbst wenn der Compiler also feststellen
> > könnte, dass "hello" das Attribut "name" von seinem Parameter
> > verlangt, und dass "Person" dieses Attribut nicht bieten kann, müsste
> > er den String-Parameter 'name' noch verstehen, und wenn man diesen zur
> > Laufzeit erst zusammenbaut, hat der Compiler keine Chance mehr.
> 
> Ja, Python hat ein paar gruselige Features. Interessant finde ich hier, 
> dass im Zusammenhang mit Python immer OO-Vokabular benutzt wird 
> (Attribute, Methoden, usw.), Python also offensichtlich eine 
> objektorientierte Sprache ist.

Ja, aber es ist ebenso eine funktionale Sprache. Syntaktisch gesehen
ist Python aber imperativer Stil, falls du das meinst.

> Zur Stärke der objektorientierten 
> Sprachen gehört jedoch gerade, dass für solche Konstrukte auch saubere 
> Varianten existieren.

Nein, das muss nicht sein. Obige Konstrukte können sauber eingesetzt
werden. Das Beispiel ist inhaltlich völlig bescheuert, natürlich macht
das niemand so. Ich wollte lediglich einige Möglichkeiten der Sprache
beleuchten - es gibt noch viel mehr - um die Problematik zu beleuchten,
dass man solch eine Dynamik mit statischen Typen kaum erreichen kann.

> Und das bedeutet, dass ich es eigentlich verbieten 
> will, dass man einem Objekt eine neue Methode oder ein neues Attribut 
> andichtet.

Introspektion kann nervende Tipp-Orgien ersparen. Gut eingesetzt
ermöglicht es eine starke Vereinfachung des Programm-Codes. Ein
typisches "sanftes" Beispiel sind Dispatcher:

	class Test:
		def call(self,action):
			getattr(self,"call_"+action)()

		def call_meth1(self):
			print "meth1"

		def call_meth2(self):
			print "meth2"


	t = Test()
	t.call("meth1")
	t.call("meth2")
	t.call("meth3") # wirft Exception


Man kann das natürlich auch anders lösen, aber würde intern doch
wieder ein Dictionary (Hashtable) von "action"-Strings -> Methoden
benutzen. Da aber Objekte in Python dieses im Wesentlichen sind,
kann man das auch direkt nutzen. Die call-Methode macht nichts
weiter, als die passende Methode aufzurufen. Ein Prefix wie "call_"
ist dabei natürlich Pflicht, sodass keine unerwarteten Methoden
aufgerufen werden können.

Natürlich macht das keinen Sinn, wenn man die Dynamik nicht braucht,
aber falls man z.B. ein Menü-System hat, wo "action" zur Laufzeit
eingelesen wird, dann ist dies eine sehr bequeme Möglichkeit.


Ein "medium" Beispiel wäre das Decorator-Muster. Statt einen
langweiligen Decorator zusammenzutippen, und stets mit dem Interface
synchron zu halten, obwohl sie Methoden doch nur weitergeleitet werden,
was jeweils eine einzige Zeile Code bedeutet. In Python packt man
ein sehr dynamisches Objekt dazwischen, das jeden Methodenaufruf
abfängt (kann man machen), und ihn selbst verwaltet, indem es
einfach von "dekorierten" Objekt die gleichnamige Methode mit
den entsprechenden Parametern aufruft. Letzteres entspricht übrigens
genau dem, was im GoF-Buch beim Decorator-Muster für Smalltalk empfohlen
wird.


Ein etwas "krasseres" Beispiel ist z.B. die SQLObject-Library.
Dort kann man etwas schreiben wie z.B.:

	__connection__ = "pgsql://..."  # irgendsoein Connection-String


	class Person(SQLObject):
		name = StringCol(default="")
		alter = IntegerCol(default=18)


Mit den letzten 3 Zeilen hat man eine Klasse für persistente Objekte,
die in einer PostgreSQL-Datenbank liegen. Die Basisklasse "SQLObject"
kann dabei nicht alles übernehmen. Folgendes ist z.B. möglich:

	p = Person()  # neuer Datensatz (Konstruktor von Person aufgerufen)

	p = Person.get(1)  # Suche via Primary Key (wird implizit erzeugt)

	p.name = "Volker"  # führt entsprechendes SQL-Kommando aus,
	                   # cached aber gleichzeitig

In dem Moment, wo die Klasse Person zuende definiert wird, erfolgt
Introspektion (über eine sog. Metaklasse, die in der Basisklasse
SQLObject festgelegt wird), wo die Attribute vom Typ
StringCol,IntegerCol, etc. gegen andere Attribute/Methoden ersetzt
werden, sodass man auf oben demonstrierte Weise darauf zugreifen kann.

Das Beispiel ist insofern schlecht, als dass auch hier eigentlich
zur Compilezeit alles feststeht. Jedoch kann man SQLObject auch sagen,
dass es zur Laufzeit die Spalten der SQL-Tabelle hernimmt und die
entsprechende Klasse erzeugt.


Bitte keine weiteren Detail-Fragen zu SQLObject, das ist auf deren Webseite
wunderbar alles erklärt. Mit geht es nur darum: Natürlich ist die
Introspektion ein "Hack", und sollte sehr sparsam eingesetzt werden.
Die Grundregel lautet natürlich, dass Introspektion eingesetzt wird,
um Programmcode lesbarer und übersichtlicher zu machen. Letztlich
ist es aber meistens syntaktischer Zucker, den man über seine API
streuen kann. Aber es erschlägt auch einige Muster. In Python, genau
wie in Ruby und damals schon in Smalltalk, wird die Sichtweise
hochgehalten, dass man eben Nachrichten an Objekte sendet. Dass
diese Nachrichten manchmal erst zur Laufzeit feststehen, und somit
im Konstrukt "p.name = ..." der Begriff "name" mehr als String statt
als Funktionsname angesehen werden kann, ist Absicht! Genauso wird
das in Python gemacht: Alle Variablen-Namen, Methoden-Namen, etc.
sind letztlich Einträge in Dictionaries (Hashtabellen), auf die man
zur Laufzeit Einfluss nehmen kann.


> Ich kenne bis heute kein Beispiel, was sich nicht mit sauberen 
> OO-Konzepten hinschreiben lässt, dafür aber mit Python's dynamischen 
> Features. Ich lasse mich aber gerne vom Gegenteil überzeugen...

Der Dispatcher oder der Decorator sind gute Beispiele. In C++ oder Java
muss man dafür extra Boilerplate-Code produzieren, d.h. sehr ähnlichen
Code an mehreren Stellen hinschreiben und synchron halten. Python
ermöglicht es einem, genau diesen Boilerplate-Code durch Introspektion
zu verhindern. Leider gehen dabei Typ-Informationen etc. per Definition
verloren, oder besser gesagt: sie stehen erst zur Laufzeit fest, daher
kann sie der Compiler nicht unbedingt verstehen. In Java ist
Introspektion übrigens auch möglich, und durch die Casts etc. ist
dort das Typsystem eh durchbrechbar. Aber in Python wird einem das
sehr leicht gemacht, sodass man es auch wirklich einsetzen kann,
ohne dass man mehr Aufwandt für die Introspektion betreiben muss
als für den Code, den man vereinfachen will.  :-)

Im Vergleich zu C++ oder Java werden also vorallem zahlreiche OO-Muster,
und das Schreiben von "object-relational wrappers" erleichtert. Man
kann die Probleme innerhalb der Sprache angehen, und Code-Duplikation
verhindern.

Mit Ocaml kann ich das leider nicht vergleichen, vielleicht hat man
dort bessere Möglichkeiten, solche Probleme *innerhalb* der Sprache
anzugehen, als es bei Java & Co. der Fall ist.

Es ist nunmal ein Jammer, dass man nur die Wahl hat zwischen einem
strengen Compiler, der einem sinnlose Tipp-Orgien (sehr viel Redundanz)
aufhalst, und einem viel zu laschen Compiler (Python), der einem aber
hilft, einfachen, gut verständlichen, kompakten Code zu produzieren.
Codegeneratoren helfen da IMHO auch nur bis zu einem gewissen Grad,
und sind eigentlich lediglich ein Indiz dafür, dass eine Sprache (Java)
zu schwach ist, und man gleich eine mächtigere Sprache hernehmen sollte.

In sicherheitskritischen Projekten ist ersteres das geringere Übel, aber
möglichst wenig Beschäftigungstherapie beim Programmieren zu haben,
halte ich für wichtiger, insbesondere für meine Projekte.

Aber diese Wahl treffen andere Leute vielleicht anders, und insbesondere
Ocaml könnte ein sehr guter Mittelweg sein, denn dort kann man
vielleicht diese Beschäftigungstherapien verhindern, ohne auf statische
Typprüfung zu verzichten. Dazu kenn ich es aber zu wenig.

> > Macht man hingegen das hier:
> >
> > 	p = Person()
> > 	p.name = "Hans" # Das geht!
> > 	hello(p)
> 
> Meiner Meinung nach verleitet die (ernsthafte) Anwendung solcher 
> Konstrukte zur Herstellung von unübersichtlichem, wenn nicht sogar 
> nichtvorhersehbarem Code, ala 'self-modifying code', der in den 80er 
> Jahren ungeahnte Möglichkeiten verhieß.

Nein, mein Code ist einfach nur bescheuert, das ist alles. Sowas würde
kein ernsthafter Python-Programmierer schreiben. Es diente nur der
Demonstration einiger Features, die statische Typisierung nahezu
unmöglich machen. Introspektion wird natürlich sparsam eingesetzt,
und hat den einzigen Zweck, den Code einfacher und übersicherlicher
zu machen. Wenn man diese Features in seinem Modul einsetzt, dann nur,
damit der Programmcode, der das Modul benutzt, dadurch vereinfacht wird,
bzw. dass man dort Redundanz vermeidet.

> Wie gesagt, das geht auch 
> sauber. Wenn ich a priori weiß, dass ich Person-Objekte an die Funktion 
> hello übergeben will und hello auf ein Attribut name zugreift (mehr weiß 
> man über die Argumente von hello nicht?), dann kann ich das etwa so 
> hinschreiben:
> 
> 	class Helloable:
> 		String name;
> 
> 	def hello (Helloable h):
> 		print "Hello ", h.name, "!"
> 
> 	class Person: inherits Helloable
> 		#pass
> 
> und alles ist wohlgeordnet, sprich: der Compiler kann die Typen prüfen. 
> Und wenn ich nicht a priori weiß, dass ich Person-Objekte an die 
> Funktion hello übergeben will, dann sollte ich das lassen!

Natürlich! Und so sollte man auch in Python programmieren. Die
dynamischen Features werden nur eingesetzt, wo sie sinnvoll sind.
Allerdings werden sie in Python auch dann eingesetzt, wenn man diese
Features eigentlich gar nicht als *Laufzeit* - Features braucht.
Das ist in der Tat ein Nachteil, aber immer noch besser, als sie
gar nicht zu haben.

(in sicherheitskritischen Anwendungen sieht das natürlich anders
aus: Da verzichtet man im Zweifelsfall lieber auf sowas)

Wenn mir Ocaml aber *beides* bieten kann, ist das in der Tat ein
Fortschritt, aber das muss ich noch heraus finden. Ich habe ja erst
vor kurzem angefangen, Ocaml zu lernen.

> > 1) gibt es ein Compiler-Projekt für Python, wo eben diese Sorte von
> >    Vorhersagen irgendwie ermöglicht werden. Will man den nutzen, darf
> >    man natürlich nicht mehr die ganz krassen Python-Features nutzen,
> >    aber du braucht man eh selten. Den Namen des Projektes weiß ich
> >    leider nicht mehr.
> 
> Python ist nicht mächtiger als C++, bezogen auf die zu lösenden Probleme. 

Bezogen auf strukturelle Probleme eröffnet Python ganz andere
Möglichkeiten als C++, seinen Code zu organisieren.

Bezogen auf das reine Lösen von Problemen, egal wie umständlich, ist
Python natürlich genauso mächtig wie C++ oder Assembler. In Python
legt man den Schwerpunkt darauf, übersichtlichen kompakten Code
zu produzieren.

> Ein Python-Compiler wird die Sprache auf die Konstrukte reduzieren, die 
> eine Typüberprüfung zur Compilezeit erlauben

Ja, genau das wird versucht.

> Laut einer Erhebung von 1997 in den USA enthalten ausgelieferte 
> Softwareprojekte durchschnittlich (!) 7 Fehler pro 100 Codezeilen...

Mein Code nicht.  ;-)

Nee, im Ernst: das ist natürlich ein Problem. Aber wenn ich für eine
äquivalente Lösung in Java doppelt oder dreimal so viel schreiben muss
wie in Python, was durchaus nicht selten passiert, und dann doch wieder
mit Casts etc. herumhampel, dann treibt das die Fehlerquote zwar herunter,
aber in die falsche Richtung (nämlich durch mehr Code statt weniger
Bugs). Ocaml könnte eine Ausnahme sein ... man hat mehr Sicherheit als
in Java, und dennoch muss man nicht alles mit Typbezeichnungen und
Interfaces zukleistern, außer da, wo es der Compiler *wirklich* nicht
selbst wissen kann.

> > Ohne Testsuites ist Python aber ein ziemliches Wagnis, das stimmt. :-(
> >
> > > Wenn schon kein Compilierungs-Schritt da ist, sollte wenigstens eine
> > > statische Analyse stattfinden, um den Programmierer zu unterstützen.
> >
> > Was bedeutet das? Kannst du das mal an einem Beispiel erläutern?
> 
> Hand aufs Herz: wie erzähl' ich's meinem Compiler? Ich versuche doch 
> erstmal hinzuschreiben, was ich meine. Der Compiler sagt mir aber, dass 
> er mich nicht versteht. Also sag' ich es ihm anders, und ich brauche 
> einige Versuche, um ihn zufriedenzustellen. Dann guck' ich mir an, ob 
> das, was ich jetzt gesagt habe, auch immer noch das ist, was ich meine. 
> So entsteht ein Konsens zwischen mir und meinem Compiler, noch bevor 
> einer von beiden auf die Idee kommt, irgendwas zu debuggen. Was würde 
> ich einem Compiler/Interpreter erzählen, der jeden Unsinn akzeptieren 
> würde?

Klar ist das wünschenswert, und das weiß ich auch zu schätzen. Momentan
gibt es für mich aber ein höheres Gut:

Wenn ich weniger schreiben muss (und ich meine, *wesentlich* weniger),
um ihm klar zu machen, was ich will, und wenn ich nicht haufenweise
Banalitäten aufschreiben muss, die der Compiler auch selbst herausfinden
kann, dann erspart das ne Menge Zeit und Nerven, und ich kann mich auf
das Wesentliche konzentrieren. Etwas mehr Debugging nehme ich dafür
gerne in Kauf. Vorallem die Strukturierung von größerem Programm-Code
und das Refactoring, profitieren sehr davon, wenn der Code knapp und
leicht durchschaubar gehalten werden kann.

Eine Sprache, die beides bietet, wäre natürlich noch besser. Aber wenn
ich nen (relativ bugfreien) Prototyp in Python schreibe, wo insbesondere
Experimente mit der Programm-Struktur und dem Organisieren des Codes
(insbesondere: suchen & entwickeln einer geeigneten Datenstruktur)
sehr flott voran gehen, und es danach in Java reimplementiere, bin ich
immer noch schneller dran, als hätte ich mich von Anfang an mit Java
gequält. Also kann dieser "Dialog" zwischen mir und dem Java-Compiler ja
doch nicht allzu effizient sein.

> Humboldt-Universität zu Berlin
> Institut für Informatik
> Signalverarbeitung und Mustererkennung
> Dipl.-Inf. Axel Weiß

Ich sehe, du arbeitest an der Uni, auf der ich studiere. Woran
arbeitest du denn? Welche Sprachen / Werzeuge benutzt du dafür?


Viele Grüße,

	Volker

-- 
Volker Grabsch
---<<(())>>---
Administrator
NotJustHosting GbR



Mehr Informationen über die Mailingliste linux-l