Blockweises Lesen einer Datei in Python...

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.
Antworten
Benutzeravatar
akis.kapo
User
Beiträge: 127
Registriert: Freitag 1. September 2006, 12:58

Hi Jungs (und Mädels),

ich erwische mich immer wieder dabei, wie ich in meinem Python Code
schlechten (nicht-python) Stil verwende, zum Beispiel, wenn es um
das blockweise Lesen von Dateien geht.

Hier eines, meiner vielen Negativbeispiele:

Code: Alles auswählen

f = open('hallo.txt','r')
blocksize = 4096
chunk = f.read(blocksize)
while len(chunk) > 0:
    do_something(chunk)
    chunk = f.read(blocksize)
f.close()
Wie löst man das in Python? (2.7.x und 3.x)

Python 3.x hat ja das IO komplett ungekrempelt.
Wie baut man das im obigen Code richtig ein?

Was mich stört:
1. blocksize will ich nicht selber wählen, sondern ausm System ziehen
2. chunk ist oben ein Objekt, das immer wieder erzeugt und zerstört wird.
(lieber _ein_ StringIO(2.7.x)/io.BytesIO(3.x) Objekt nehmen? Keine Ahnung...)
3. natürlich der while loop mit der hässlichen condition und der
Tatsache, dass ich chuck "initialisieren" muss, um in die while Schleife zu kommen.
Das kommt mir zu old-school C-Style vor. Das ist kein "schönes" Python. :-/

Bin um jeden konstruktiven Beitrag froh.

Vielen Dank im Voraus.

PS: sagt bloß nicht, ihr würdet es alle auch so schreiben, des kann's doch net sein... :shock:
Benutzeravatar
Hyperion
Moderator
Beiträge: 7478
Registriert: Freitag 4. August 2006, 14:56
Wohnort: Hamburg
Kontaktdaten:

Dateien sollte man mit dem ``with open as handler``-Idiom öffnen:

Code: Alles auswählen

with open(...) as handler:
    # handler ist hier File-Objekt
    # handler wird automatisch geschlossen
Das wäre schon mal die erste sinnvolle Verbesserung.

Als nächstes: Was stellst Du Dir denn unter einer "System weiten Blockgröße" vor? Meinst Du die block size des Dateisystems? Wenn ja, was soll das bringen?

Wenn Du Textdateien nicht komplett in den Speicher laden willst, so kannst Du bei diesen auf direkt über die Zeilen iterieren - das spart ja auch massiv Speicher und bietet sich bei Textdateien ja auch inhaltlich geradezu an. Aber vielleicht hast Du da ja auch eine andere Motivation?
encoding_kapiert = all(verstehen(lesen(info)) for info in (Leonidas Folien, Blog, Folien & Text inkl. Python3, utf-8 everywhere))
assert encoding_kapiert
lunar

@akis.kapo: Wenn Daten blockweise gelesen werden müssen, handelt es sich meist um Binärdaten, und mithin muss der Modus zum Öffnen "rb" lauten. Um die "while"-Schleife zu vermeiden, kann man eine partielle Funktion ("functools.partial()") in Verbindung mit der eingebauten Funktion "iter()" verwenden:

Code: Alles auswählen

with open(filename, 'rb') as source:
    for block in iter(partial(source.read, blocksize)):
        process(block)
Was meinst Du mit "blocksize aus dem System ziehen"? Falls Du darauf hinaus willst, irgendeine Systemeinstellung zur Standard-Blockgröße zu verwenden: Eine solche Einstellung gibt es nicht. Die richtige Blockgröße hängt von den verarbeiteten Daten, der Umgebung und vielem mehr ab, die richtige Größe gibt es mithin nicht. Du musst selbst die passende Blockgröße wählen, oder - im Falle einer Bibliothek - eventuell dem Aufrufer Deiner Funktion mithilfe eines Parameters die Wahl der richtigen Blockgröße ermöglichen.

Die wiederholte Initialisierung und Zerstörung temporärer Objekte wie beispielsweise "chunk" lässt sich nicht so ohne Weiteres verhindern, das ist einfach Bestandteil der Semantik von Python. Wieso stört Dich das? Wenn Du über Allokation und Initialisierung von Objekten nachdenkst, dann ist Python möglicherweise nicht die richtige Sprache für Dich. Anders gesagt, über sowas denkt man in C oder C++ nach, aber nicht in Python (oder anderen Hochsprachen wie C# oder Java).
Benutzeravatar
akis.kapo
User
Beiträge: 127
Registriert: Freitag 1. September 2006, 12:58

@lunar

Wow, super. Ich weiss selber, dass ich zu sehr "C" denke, daher frag ich ja,
wie man es nicht nur in Python schreibt, sondern auch in Python denkt!

Code: Alles auswählen

import io
from functools import partial

with open(filename, 'rb') as source:
    for block in iter(partial(source.read, io.DEFAULT_BUFFER_SIZE)):
        do_something(block)
Genauso in der Art hätte ich es mir vorgestellt.

@Hyperion
Das "with" construct war mir vorher schon bekannt, darin lag mein Fokus jetzt nicht, aber trotzdem ein guter Reminder! Danke.

@all

ich weiss ja nicht im Detail, wie es in CPython implementiert ist, aber ich kann mir durchaus vorstellen,
dass Schleifenobjekte "for <var> in ..." anders gehandhabt werden, als manuelle Konstrukte a la:

Code: Alles auswählen

my = "hello"
while len(hello) > 0:
    do_something(my)
    my = my * 2
Ich wette, das wirkt sich auf die Garbage Collection & Performance "anders" aus.
Kann aber auch nur ne falsche Annahme sein, ... vielleicht bin ich auch paranoid. :)
lunar

@akis.kapo: Ich kenne mich mit der Implementierung von CPython nicht aus, doch ich kann mir gut vorstellen, dass CPython einen internen Speicherpool verwaltet, und für solche temporären Objekte nicht immer erneut Speicher allozieren muss, sondern alte, nicht mehr referenzierte Objekte wiederverwendet. Ähnliches gilt sicherlich auch für alternative Interpreter wie PyPy, die eine fortschrittlichere Garbage Collection besitzen.
Benutzeravatar
akis.kapo
User
Beiträge: 127
Registriert: Freitag 1. September 2006, 12:58

whoopsy, zu früh gefreut:

Code: Alles auswählen

TypeError: 'functools.partial' object is not iterable
Benutzeravatar
cofi
Python-Forum Veteran
Beiträge: 4432
Registriert: Sonntag 30. März 2008, 04:16
Wohnort: RGFybXN0YWR0

Ad ``io.DEFAULT_BUFFER_SIZE``: Das hier ist die Implementierung:

Code: Alles auswählen

# open() uses st_blksize whenever we can
DEFAULT_BUFFER_SIZE = 8 * 1024  # bytes
Du brauchst dafuer also einen manuellen Aufruf von ``os.stat``, noch dazu ist die blksize nur auf Unix verfuegbar. die 8 KiB sind fuer die meisten Systeme afaik falsch, der gaengige Block ist 4KiB.

Ad ``iter`` Fehler, du hast den Sentinal vergessen, den man bei einem Callable braucht, versuch mal:

Code: Alles auswählen

iter(partial(source.read, io.DEFAULT_BUFFER_SIZE), '')
Benutzeravatar
akis.kapo
User
Beiträge: 127
Registriert: Freitag 1. September 2006, 12:58

@cofi

many thanks! gerade so noch die Kurve gekriegt... darauf wäre ich alleine nicht gekommen. Hab schon angefangen, ne pseudoklasse drumherum zu basteln, nur damit das Ding iter() kann. (Und es sah unendlich hässlich aus...)

PS: habe soeben in der libref nachgelesen, wozu dieser Sentinel gut sein soll. Wenn der Rückgabewert == dem Sentinel ist, weiss iter(), wann es aufzuhören hat. Schade eigentlich, ... ich hatte da an X-Men gedacht. . :P
Benutzeravatar
snafu
User
Beiträge: 6861
Registriert: Donnerstag 21. Februar 2008, 17:31
Wohnort: Gelsenkirchen

Du kannst dir folgendermaßen die Schleife sparen (ungetestet):

Code: Alles auswählen

''.join(iter(partial(source.read, io.DEFAULT_BUFFER_SIZE), ''))
Bedenke aber, dass dann auch der komplette Dateiinhalt im Speicher ist und die weitere Bearbeitung natürlich erst stattfindet, wenn die Datei / der Stream komplett eingelesen wurde. Wirklich viel Peformance bringt das aber nicht und die sollte hier ohnehin zweitrangig bleiben. So gesehen muss ja allein schon `read()` immer wieder neu aufgerufen werden (Funktionsaufrufe sind "teuer"). Das sind aber alles Gedanken, die in die falsche richtig gehen, d.h. Optimierungen, wo eigentlich keine Optimierungen nötig sind.
Benutzeravatar
akis.kapo
User
Beiträge: 127
Registriert: Freitag 1. September 2006, 12:58

@snafu
Ja nee, das ist ja auch explizit nicht gewollt, eben weil ich nicht so viel Speicher auf einen Schlag habe und weil do_something() mit kleinen Häppchen besser arbeitet...
lunar

@akis.kapo: Oh, Entschuldigung, ich hatte beim Aufruf das zweite Argument vergessen. Tut mir leid.

@cofi: Danke für die Korrektur.
Antworten