Python Design Fehler

Alles, was nicht direkt mit Python-Problemen zu tun hat. Dies ist auch der perfekte Platz für Jobangebote.
Benutzeravatar
pillmuncher
User
Beiträge: 1484
Registriert: Samstag 21. März 2009, 22:59
Wohnort: Pfaffenwinkel

BlackJack hat geschrieben:@fail: TCO allgemein unterstützen? [...] Den Namen werden erst spät aufgelöst, womit bei einem Funktionsaufruf erst klar ist welche Funktion da tatsächlich hinter steckt, wenn sie auch tatsächlich aufgelöst und aufgerufen wird. Eine Python-Implementierung müsste also zur Laufzeit ständig den Aufrufstapel im Auge behalten und schauen ob eine Funktion dort rekursiv aufgerufen wird und ob das ein optimierbarer, also endrekursiver Aufruf ist. Diesen Mehraufwand hätte man bei jedem Funktionsaufruf, obwohl die Mehrzahl der Aufrufe gar nicht rekursiv ist.
Es ist doch unerheblich, ob es sich um einen rekursiven Aufruf derselben Funktion oder einen nicht-rekursiven Aufruf einer anderen Funktion handelt, und auch, wann der Name der Funktion eines Tail Calls bekannt ist. Wichtig ist doch nur, dass der aktuelle Call Frame durch den des Tail Calls ersetzt werden kann, damit der Call Stack nicht wächst.

AFAIR hat Guido zwei Argumente gebracht, warum er keine Tail Calls möchte: erstens, weil Stack Traces nicht mehr mit dem tatsächlichen Ablauf des Programms übereinstimmen würden (was man aber wohl lösen könnte), und zweitens, dass man, wenn man möchte, Tail Calls bereits jetzt via eines Trampolins selbser bauen kann, weswegen man die Sprache nicht damit belasten muss. Dazu verweise ich auf eines meiner ersten Postings hier im Forum.

Davon abgesehen, hätte ich ebenfalls gerne Tail Calls.
In specifications, Murphy's Law supersedes Ohm's.
Leonidas
Python-Forum Veteran
Beiträge: 16025
Registriert: Freitag 20. Juni 2003, 16:30
Kontaktdaten:

Also ich weiß ja nicht, aber die Fälle wo ich rekursive Funktionen tatsächlich optimiert brauche sind super selten, auch in funktionalen Sprachen. Eben weil es mächtige Funktionen wie ``map``, ``filter``, ``reduce`` und Co. gibt, die es gar nicht notwenig machen dass ich die Sachen die ich machen will überhaupt als rekursive Funktion schreiben muss.

Vom Prinzip her ganz ähnlich wie PostScript vs. Joy/Factor. In ersterem bearbeitet man den Stack, in zweiteren hat man Kombinatoren die den Stack von selbst bearbeiten und man sich stattdessen auf die Programmlogik konzentrieren kann.
My god, it's full of CARs! | Leonidasvoice vs (former) Modvoice
lunar

@pillmuncher Das ist nicht unerheblich. Wenn Namen so wie in Python oder Java spät aufgelöst werden, kann der Interpreter nicht statisch entscheiden, welche Funktion rekursiv aufgerufen wird. Mithin kann er den Funktionsaufruf selbst nicht einfach wegoptimieren, sondern muss bei jedem Aufruf dynamisch prüfen, ob der Stapelrahmen wiederverwendet werden kann.

Nun ist die Mehrzahl der Aufrufe aber nicht rekursiv. Die meisten Aufrufketten enden vielleicht so nach maximal 50 Aufrufen. Ich sehe nicht, warum man alle Funktionsaufrufe teurer machen sollte, nur um einen Spezialfall effizienter zu lösen.

Mag sein, dass Deine Programme besonders sind und viel Rekursion nutzen, doch Prolog-Interpreter sind nun mal kein Standardanwendungsfall. Webanwendungen dagegen brauchen keine TCO, sondern viel eher gutes Multithreading, Unicode-Handling, etc. Es gibt ganz andere, und viel wichtigere Baustellen.

Im Übrigen halte ich es auch mit Leonidas: Wenn man explizite Rekursion nutzt, ist das auch und gerade in funktionalen Sprachen eigentlich kein gutes Zeichen.
Benutzeravatar
pillmuncher
User
Beiträge: 1484
Registriert: Samstag 21. März 2009, 22:59
Wohnort: Pfaffenwinkel

@lunar: Auch in Lisp werden Namen erst spät aufgelöst und trotzdem gibt es dort TCO (in Scheme verpflichtend). Es scheint also ein lösbares Problem zu sein, das sogar Vorteile bringt (wenigstens in Lisp), sonst wäre es wohl nicht implementiert worden. Der Stack Frame würde in Python wohl auch gar nicht wiederverwendet, sondern es würde ein neuer Frame angelegt, ein paar Zeiger verbogen, und der aktuelle Frame dem Garbage Collector überlassen werden.

Ob ein Call ein Tail Call ist, ist mehr oder weniger eine syntaktische Frage. Ist das letzte Statement in einer Funktion ein Funktionsaufruf, ist es ein Tail Call. Ist das letzte Statement in einer Funktion ein if-Statement, und ist das letzte Statement in einem Branch ein Funktionsaufruf, ist es ein Tail Call. Und so weiter. Ich vermute, der Compiler könnte schlau genug :wink: gemacht werden, sowas zu erkennen. Und das unabhängig davon, wann der Name der aufgerufenen Funktion bekannt ist, denn ob ein Statement als Funktionsaufruf erkennbar ist, ist nicht davon abhängig, wie die Funktion heißt. Ob das zu signifikanten Performance-Einbußen führen würde, könnte man AFAICS nur durch Austesten einer tatsächlichen Implementierung feststellen.

Hier sind übrigens Guidos Überlegungen zum Thema:

http://neopythonic.blogspot.de/2009/04/ ... ation.html
http://neopythonic.blogspot.de/2009/04/ ... calls.html

Zwar hätte ich gerne TCO, aber ich verliere darüber keinen Schlaf. Wenn ich wählen müsste zwischen TCO oder der Abschaffung des GILs, würde ich letzteres wählen.
In specifications, Murphy's Law supersedes Ohm's.
lunar

@pillmuncher Was letzte Aufrufe sind, weiß ich, und mir ist auch durchaus klar, dass man diese aus dem AST ableiten kann. In Python zwar etwas schwieriger als in C, Haskell, oder ML, da man die Semantik einiger Schlüsselwörter kennen muss (die Aufrufe "with foo: bar()" oder "try: foo(); finally: x()" sind nicht letzt!), aber möglich.

Scheme ist die einzige Lisp-Implementierung, die TCO erzwingt. Ich kenne Scheme nicht gut, glaube aber, dass Namen dort früh aufgelöst werden, und endrekursive Funktionen mithin in Sprünge oder Schleifen auskompiliert werden.

CL-Implementierungen, die TCO unterstützen, tun dies dagegen ziemlich sicher auf die genannte Weise: Sie werfen zur Laufzeit den Stapelrahmen weg, wenn ein letzter Aufruf stattfindet. Der teure Funktionsaufruf selbst bleibt also, womit ein großer Vorteil der TCO in Haskell oder C verloren geht. Der einzige verbleibende Vorteil der TCO ist dann, dass der Aufrufstapel nicht wächst.

Ich kann gut verstehen, dass man für diesen, in einer Sprache, in der Rekursion nicht zum normalen Idiom gehört, verhältnismäßig kleinen Vorteil nicht den Interpreter verkomplizieren und lesbare Stacktraces zerstören möchte.

Edit: Top-Level Definitionen sind in Scheme offenbar statisch, und können zur Laufzeit nicht mehr verändert werden. Mithin kann der Compiler jeden Funktionsaufruf lexikalisch auflösen und mithin statisch binden.
Benutzeravatar
pillmuncher
User
Beiträge: 1484
Registriert: Samstag 21. März 2009, 22:59
Wohnort: Pfaffenwinkel

lunar hat geschrieben:CL-Implementierungen, die TCO unterstützen, tun dies dagegen ziemlich sicher auf die genannte Weise: Sie werfen zur Laufzeit den Stapelrahmen weg, wenn ein letzter Aufruf stattfindet. Der teure Funktionsaufruf selbst bleibt also, womit ein großer Vorteil der TCO in Haskell oder C verloren geht. Der einzige verbleibende Vorteil der TCO ist dann, dass der Aufrufstapel nicht wächst.
Dieser letzte Vorteil ist es, an dem mir gelegen ist. Dann könnte man bequem Continuation Style Passing verwenden. Ob das ein paar Zyklen mehr braucht, wäre mir egal.
lunar hat geschrieben:Ich kann gut verstehen, dass man für diesen, in einer Sprache, in der Rekursion nicht zum normalen Idiom gehört, verhältnismäßig kleinen Vorteil nicht den Interpreter verkomplizieren und lesbare Stacktraces zerstören möchte.
Ich kann das auch verstehen. Allerdings könnte man auch fragen, ob die Tatsache, dass Rekursion nicht zum normalen Idiom gehört, nicht vielleicht daran liegt, dass es kein TCO gibt?

Man könnte übrigens auch zB. ein return from Statement o.ä. einführen, dann kann der Compiler dumm bleiben und der Programmierer kann dadurch explizit machen, dass ihm mehr daran gelegen ist, einen Stack Overflow zu vermeiden, als aussagekräftige Stack Traces zu haben.
lunar hat geschrieben:Edit: Top-Level Definitionen sind in Scheme offenbar statisch, und können zur Laufzeit nicht mehr verändert werden. Mithin kann der Compiler jeden Funktionsaufruf lexikalisch auflösen und mithin statisch binden.
Ich denke, das folgt nicht daraus, da man ja auch Funktionen als Parameter übergeben kann oder lokal definieren kann. TCO sollte auch in solchen Fällen funktionieren. Wie das genau in Scheme geregelt ist, muss ich nachlesen.
In specifications, Murphy's Law supersedes Ohm's.
lunar

@pillmuncher Ich glaube nicht, dass CPS in Python jemals „bequem“ wäre, selbst mit TCO. Dazu fehlt es an vernünftigen Lambdas, syntaktischer Unterstützung, v.a. ein Operator zur Funktionsanwendung, und entsprechenden Bibliotheken, u.a. zur Resourcenverwaltung. Python ist für CPS einfach nicht entworfen.

Ich glaube nicht, dass das Fehlen der TCO Rekursion als Idiom ausschließt. Ich nehme an, dass es eher umgekehrt ist. Python ist einfach keine funktionale Sprache, und bevorzugt Schleifen vor endrekursiven Funktionen. Statt TCO gibt es eben mächtige Iteratoren und Generatoren.

Ein "return from" wäre imho keine gute Idee. Diese Anweisung lädt geradezu ein, Fehler zu machen, e.g. "with foo: return from bar()". Python ist so abstrakt, dass oft nicht unmittelbar klar ist, ob im aktuellen Kontext nach der Rückkehr der Funktion noch Code auf dem Stack ausgeführt wird.

Was Scheme anbelangt: Nun ja, letzte Aufrufe anonymer, lokaler Funktionen, oder übergebener Funktionen sind ja ziemlich offensichtlich keine endrekursiven Aufrufe. Da kann Scheme nur den Stapelrahmen verwerfen (was es sicherlich auch tun wird, TCO funktioniert also).

Ich bezog mich lediglich darauf, dass Scheme endrekursive Aufrufe statisch erkennen, und dann in Sprünge auskompilieren kann, so dass endrekursive Funktionen tatsächlich äquivalent zu Schleifen verwenden kann (was in einer funktionalen Sprache der wesentliche und wichtige Vorteil der TCO ist).

Ich sehe jetzt nicht, warum übergebene Funktionen oder lokale Funktionen TCO widersprechen sollten. Beides sind letztlich nichts anderes als Funktionszeiger.
Üpsilon
User
Beiträge: 222
Registriert: Samstag 15. September 2012, 19:23

Ich werf auch mal was in den Raum, was mich an Py nervt:

Man muss immer das self den Klassenmethoden hinterherschmeißen (als Parameter deklarieren), das vergess ich immer. :-/ Aber mir fällt spontan auch nicht ein, wie das besser ginge.

------
Und zur Frage, was ich gerne in Py4 hätte: eine Möglichkeit des "switch-ens", wie man es in den meisten anderen Programmiersprachen machen kann.

-----
EDIT: Ich weiß jez nicht, ob es vlt. etwas zu radikal wäre, aber wenn man die normalen gedingsten Klammern für Codeblöcke nutzen könnte... das wär schon... neee wär doch scheiße aber egal! :D
PS: Die angebotene Summe ist beachtlich.
BlackJack

@Üpsilon: Irgendwie hast Du jetzt an zwei Fronten das selbe Thema aufgemacht. Ich wiederhole es hier trotzdem noch mal kurz: Viele ``switch``\e kann man in Python mit einem Wörterbuch erledigen und einem ``switch`` haftet in einer objektorientierten Programmiersprache auch immer ein „code smell” an.
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

BlackJack hat geschrieben:... einem ``switch`` haftet in einer objektorientierten Programmiersprache auch immer ein „code smell” an.
Das vermittel mal einem "User Unkown" bei uu.de ... :evil:
encoding_kapiert = all(verstehen(lesen(info)) for info in (Leonidas Folien, Blog, Folien & Text inkl. Python3, utf-8 everywhere))
assert encoding_kapiert
Benutzeravatar
snafu
User
Beiträge: 6740
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Hyperion hat geschrieben:
BlackJack hat geschrieben:... einem ``switch`` haftet in einer objektorientierten Programmiersprache auch immer ein „code smell” an.
Das vermittel mal einem "User Unkown" bei uu.de ... :evil:
:mrgreen:
Antworten