Seite 1 von 2

Ausgabe in Spalten wie beim Befehl "ls"

Verfasst: Samstag 21. November 2009, 15:58
von snafu
Ich will die Elemente eines iterierbaren Objekts spaltenweise ausgeben. So eine Art Mischung aus [mod]pprint[/mod] und dem Unix-Tool `ls` also.

Im Prinzip klappt das schon ganz gut. Ich ermittle das längste Element und wende anschließend `str.lstrip()` an, um den gleichen Abstand für alle Elemente zu bekommen.

Jetzt würde ich aber gerne, so wie bei `ls`, Spalten von unterschiedlicher Breite haben, d.h. wenn relativ kurze Zeichenketten in einer Spalte landen, dann reichen natürlich entsprechend kleinere Werte für die Breite dieser Spalte aus.

Mein Problem ist, dass ich vorher nicht weiß, wieviele Spalten am Ende bei der Ausgabe herauskommen, so dass ich leider auch keinem Element seine Spalte zuordnen könnte. Denn 1-2 schmale Spalten können ja durchaus dazu führen, dass eine Spalte zusätzlich entstehen kann und dementsprechend alle Elemente umverteilt werden müssen.

Bei meiner jetzigen Version mit fester Spaltenbreite sage ich einfach `Max. an Zeichen für eine Zeile / (Zeichen des längsten Elements + gewünschter Mindestabstand)`.

Damit habe ich eine feste Anzahl an Elementen, die ich gruppenweise herausnehme und später mit einem Zeilenumbruch von den anderen Gruppen getrennt ausgebe. Die Schleife bricht ab, sobald keine neue Zeile mehr zustande kommt.

Hat jemand einen Ansatzpunkt für mich, wie man das am Besten für Spalten in variabler Anzahl und mit variabler Breite umsetzen kann? Zur Veranschaulichung hier mein Code: http://paste.pocoo.org/show/152070/

Verfasst: Sonntag 22. November 2009, 11:34
von sma
Ich würde es so machen: Messe jeweils die Breiten für 1-spaltig, 2-spaltig, ... bis n-spaltig. Nimm die Spaltenzahl, die gerade noch in die gewählte Breite passt. http://paste.pocoo.org/show/152205/ zeigt es, doch ich schäme mich für die nichtssagenden Variablennamen.

Stefan

Verfasst: Sonntag 22. November 2009, 11:51
von snafu
Vielen Dank, sma. :)

Das sieht beim Testen wirklich gut aus. Nur mit dem Ablauf muss ich mich noch vertraut machen... ;)

Verfasst: Sonntag 22. November 2009, 13:01
von snafu
Ganz perfekt ist die Funktion noch nicht. Wenn ich sie in IPython mit `os.environ` bei einer Breite von 157 (entspricht bei mir einem maximierten Terminal-Fenster) teste, dann füllt er einige Spalten nicht bis ganz unten aus:

Code: Alles auswählen

COLORTERM                 GDM_KEYBOARD_LAYOUT       GTK_MODULES   LESSOPEN         PATH             SHLVL          TERM      XAUTHORITY        
DBUS_SESSION_BUS_ADDRESS  GDM_LANG                  GTK_RC_FILES  LOGNAME          PWD              SPEECHD_PORT   USER      XDG_DATA_DIRS     
DESKTOP_SESSION           GNOME_DESKTOP_SESSION_ID  HOME          LS_COLORS        SESSION_MANAGER  SSH_AGENT_PID  USERNAME  XDG_SESSION_COOKIE
DISPLAY                   GNOME_KEYRING_PID         LANG          ORBIT_SOCKETDIR  SHELL            SSH_AUTH_SOCK  WINDOWID  _                 
GDMSESSION                GNOME_KEYRING_SOCKET      LESSCLOSE

Verfasst: Sonntag 22. November 2009, 14:52
von HWK
Aufbauend auf sma und snafu habe ich mich auch einmal versucht:

Code: Alles auswählen

def pprint_elems(iterable, indent=2, line_len=40):
    list_len = len(iterable)
    for cols in xrange(1, len(lst) + 1):
        lines = list_len / cols
        if list_len % cols != 0:
            lines += 1
        # Letzte Spalte leer?
        if lines * cols >= list_len + lines:
            continue
        temp1 = [iterable[col * lines:col * lines + lines]
                 for col in xrange(cols)]
        temp2 = [max(len(item) for item in col) for col in temp1]
        if sum(temp2) + (cols - 1) * indent > line_len and cols > 1:
            break
        col_list = temp1
        width_list = temp2
    line_list = []
    for i in xrange(len(col_list[0])):
        line = []
        for col, width in zip(col_list, width_list):
            try:
                line.append(col[i].ljust(width + indent))
            except IndexError:
                break
        line_list.append(''.join(line).rstrip())
    return '\n'.join(line_list)

import random
lst = ['x' * random.randrange(1, 12) for i in range(16)]
print lst
print pprint_elems(lst)
MfG
HWK

Edit: Wenn man die Ausgabe zeilen- statt spaltenweise haben möchte, braucht man nur temp1 zu ändern:

Code: Alles auswählen

        temp1 = [iterable[col:list_len + 1:cols]
                 for col in xrange(cols)]

Verfasst: Montag 23. November 2009, 19:10
von snafu
Besten Dank @ HWK. Die Funktion ist super. Die Namen muss ich noch etwas überarbeiten. Zudem sind zwei Fehler drin. In Zeile 3 brauchst du die Länge von `iterable`, nicht von `lst`. Das klappt hier nur, weil `lst` auf globaler Ebene definiert wurde. Zudem gibt es Verschiebungen bei Unicode-Zeichen (z.B. Umlaute), da er dort für die Repräsentation eine Länge von 2 pro Zeichen erhält und `print` dann halt nur 1 Zeichen anzeigt. Dies lässt sich beheben, wenn man am Anfang etwas wie `iterable = [str(x).decode('utf-8') for x in iterable]` einfügt. Sonst halt noch wie gesagt die Namensgebung und ein paar kleinere Optimierungen, aber ich denke insgesamt kann ich den Code sehr gut weiterverwenden. :)

Verfasst: Montag 23. November 2009, 21:49
von jerch
Und noch eine Variante mit funktionalerem Ansatz, was der Leserlichkeit nicht unbedingt zuträglich ist. ;)

Code

Verfasst: Dienstag 24. November 2009, 05:38
von snafu
@jerch, deine Version finde ich auf dem ersten Blick schon etwas leserlicher/effizienter, aber sie beachtet anscheinend nicht die Reihenfolge der Elemente. Zumindest nicht konsequent. Wende es z.B. mal auf ein `sorted(os.listdir('.'))` an. Bei mir steht dann `Öffentlich` ganz zum Schluss und auch vorher habe ich ein paar Sprünge drin.

Zum Auslesen der Terminalgröße: Ich weiß, dass einige Snippets im Netz das so machen, aber ich persönlich hätte lieber eine *Ausnahme*, wenn die Größe nicht ermittelt werden kann. Wie man damit umgeht, kann man dann selbst entscheiden.

Auch das plattformabhängige Importieren hätte ich lieber an den Anfang zu den anderen Imports gesetzt, zwecks Übersicht. Das mag aber alles Geschmackssache sein und lässt sich ja leicht anpassen. ;)

Verfasst: Dienstag 24. November 2009, 13:20
von jerch
snafu hat geschrieben:@jerch, deine Version finde ich auf dem ersten Blick schon etwas leserlicher/effizienter, aber sie beachtet anscheinend nicht die Reihenfolge der Elemente. Zumindest nicht konsequent. Wende es z.B. mal auf ein `sorted(os.listdir('.'))` an. Bei mir steht dann `Öffentlich` ganz zum Schluss und auch vorher habe ich ein paar Sprünge drin.
Hm, ich habs nicht ausgiebig getestet, kann sein dass mir da irgendwie ein slice-offset unterlaufen ist. Mit

Code: Alles auswählen

[str(i)+'a' * random.randrange(5,15) for i in xrange(29)]
als darzustellende Liste fällt mir allerdings erst einmal nichts auf. Mach doch mal aus Deiner Liste ein:

Code: Alles auswählen

[str(i)+j for i,j in enumerate(sorted(os.listdir('.')))]
und vergleiche die Inputliste mit dem Output.

Beachte bitte auch, dass sorted wie folgt sortiert:

Code: Alles auswählen

for i in sorted([u'ä',u'c',u'a',u'1',u'ü',u'ß',u'Ä',u'A',u'z',u'Z']):
    print i,
1 A Z a c z Ä ß ä ü
und Du die korrekte deutsche Reihenfolge wahrscheinlich selbst implementieren müsstest (vgl. hierzu http://de.wikipedia.org/wiki/Alphabetische_Sortierung)

Nochwas zu dem Unicode/Bytestring-Problem (Deine Antwort auf HWKs Post). len('ä') ist 2 und len(u'ä') ist 1 ;). Die Funktion verhält sich korrekt, Problem sind die Ausgangsdaten und die Tatsache, dass Python zu dieser Zeit nichts davon weiß, dass die 2 Byte '\xc3\xa4' nur einem Glyphen 'ä' entsprechen und ein ljust() somit eine Leerstelle zu wenig für die Ausgabe einfügt. Abhilfe: Unicode verwenden.
snafu hat geschrieben: Zum Auslesen der Terminalgröße: Ich weiß, dass einige Snippets im Netz das so machen, aber ich persönlich hätte lieber eine *Ausnahme*, wenn die Größe nicht ermittelt werden kann. Wie man damit umgeht, kann man dann selbst entscheiden.
Da hab ich mir bei dem kleinen Schnipsel gar keine Gedanken dazu gemacht, mein Fehler. Grundsätzlich wäre meine Antwort hierzu Jein. Welche Fehler man sich zutraut, auf Modulebene zu behandeln, und welche durchzureichen sind, ist meiner Meinung nach eine Fall zu Fall Entscheidung. Im Zweifelsfalle würde ich eher durchreichen, um dem Benutzer die Kontrolle zu gewähren, was aber nicht für alle Fehler gelten sollte, da dies der Einfachheit von Python zuwider läuft und sich der Benutzer mehr Gedanken um Fehlerbehandlung machen muss als zu dem eigentlichen Problem (Da kann er ja gleich Java nehmen ;) )
Im konkreten Fall würd ich vielleicht so vorgehen: get_size() sollte Fehler präsentieren, da man davon ausgehen kann, dass der Benutzer um den Ressourcenzugriff weiss und damit rechnen muss, dass die Ressource nicht verfügbar ist und entsprechend darauf reagieren kann. (Achtung: der Windowscode ist abgeschrieben, da stecken wahrscheinlich auch noch Fehlermöglichkeiten drin.) column_format() wäre ein solcher Benutzer, die Fehlerbehandlung müsste dann hier erfolgen, sofern man sich nicht entschließt, `width` nur als Parameter zu übergeben (dann eben eine Ebene höher).
snafu hat geschrieben: Auch das plattformabhängige Importieren hätte ich lieber an den Anfang zu den anderen Imports gesetzt, zwecks Übersicht. Das mag aber alles Geschmackssache sein und lässt sich ja leicht anpassen. ;)
Strick Dir dass ruhig nach Deinen Vorstellungen um. Ich lagere normalerweise plattformabhängige Sachen in Untermodule aus und reagiere auf die Plattform via __init__.py.

Noch was zum Code. Mit

Code: Alles auswählen

...
from itertools import imap
from string import ljust
...
# Zeile 63
    return '\n'.join(sep.join(imap(ljust, it[i::o_rlen] if vertical else
                                   it[i*o_cols:i*o_cols+o_cols], o_col_w))
                     for i in xrange(o_rlen))
siehts noch etwas hübscher aus ;)
Und weil Du `ls` als Vorbild hattest, wie wärs mit farbigem Output? :D

Viel Spaß beim Tüfteln.

Verfasst: Dienstag 24. November 2009, 20:10
von snafu
jerch hat geschrieben:len('ä') ist 2 und len(u'ä') ist 1 ;).
IPython, worauf ich mich beim Testen verlassen habe, sagt:

Code: Alles auswählen

In [1]: len(u'ä')
Out[1]: 2
Der normale Python-Interpreter liefert hingegen `1`. Anscheinend ein Fehler in IPython, oder?
jerch hat geschrieben:Viel Spaß beim Tüfteln.
Momentan Schulung in neuem Job und demenstprechend wenig Zeit/Kopf für Hobbyprojekte (mache das Programmieren ja nicht beruflich). ;) Ich halt mich aber ran und danke nochmal für die Tipps.

Verfasst: Dienstag 24. November 2009, 20:38
von jerch
Ja, das scheint ein bekannter Inputbug zu sein: https://bugs.launchpad.net/ipython/+bug/339642

Code: Alles auswählen

In [1]: print u'ä'
-------------->print(u'ä')
ä

In [2]: `u'ä'`
Out[2]: "u'\\xc3\\xa4'" # falsch

In [3]: `'ä'`
Out[3]: "'\\xc3\\xa4'"

In [5]: `('ä').decode('utf-8')`
Out[5]: "u'\\xe4'" # das muesste es sein

In [6]: import sys

In [7]: print sys.stdin.encoding, sys.stdout.encoding
------> print(sys.stdin.encoding, sys.stdout.encoding)
('UTF-8', 'UTF-8')
:shock:

Verfasst: Mittwoch 2. Dezember 2009, 21:28
von snafu
Habe jetzt zwei Module zu dem Thema gefunden:

http://code.google.com/p/prettytable/

http://code.google.com/p/pycolumnize/

...wobei mir `prettytable` dann doch etwas ausgereifter erscheint. ;)
(gibts übrigens auch als Debian-Paket)

Verfasst: Mittwoch 2. Dezember 2009, 22:35
von jerch
Hab jetzt beide nur mal überflogen. columnize.py macht so ziemlich dasselbe, nur umständlicher. Wäre mal interssant, die beiden zu timern.

prettytable ist aber doch was anderes, wie der Name sagt, zum Tabellendarstellen. Sieht nach ziemlich viel Code aus für so eine (scheinbar?) simple Sache.

Verfasst: Donnerstag 3. Dezember 2009, 06:59
von snafu
jerch hat geschrieben:prettytable ist aber doch was anderes, wie der Name sagt, zum Tabellendarstellen.
Wenn ich von `ls -l` ausgehe und Überschriften haben möchte (die ja nicht zwingend sind) ist es schon ziemlich ähnlich.

Für `columnize` wurde als Grundlage die nicht dokumentierte Methode `Cmd.columnize()` aus dem [mod]cmd[/mod] verwendet. Die brauchen da in der Tat viel mehr Code.

`prettytable` hab ich mir im Detail noch nicht angeguckt. Hab erstmal nur gesehen, dass es über 900 Zeilen sind. Hier mag ich vor allem die Flexibilität. Gut vorstellen könnte ich mir das auch für etwas wie `csv2table()`:

Code: Alles auswählen

import csv
from prettytable import PrettyTable


def csv2table(filename):
    with open(filename, 'rb') as csvfile:
        sample = csvfile.read(1024)
        csvfile.seek(0)

        sniffer = csv.Sniffer()
        dialect = sniffer.sniff(sample)
        has_header = sniffer.has_header(sample)

        reader = csv.reader(csvfile, dialect)
        header = reader.next() if has_header else []
        table = PrettyTable(header)

        for row in reader:
            table.add_row([s.decode('utf-8') for s in row])

    return table


def test():
    print csv2table('stundenplan.csv').get_string()
Für die CSV-Datei habe ich das Beispiel von Wikipedia genommen.

Verfasst: Donnerstag 3. Dezember 2009, 10:06
von mkesper
jerch hat geschrieben:Beachte bitte auch, dass sorted wie folgt sortiert:

Code: Alles auswählen

for i in sorted([u'ä',u'c',u'a',u'1',u'ü',u'ß',u'Ä',u'A',u'z',u'Z']):
    print i,
1 A Z a c z Ä ß ä ü
und Du die korrekte deutsche Reihenfolge wahrscheinlich selbst implementieren müsstest.
Das Zauberwort könnte locales heißen, siehe http://wiki.python.org/moin/HowTo/Sorting ganz unten.

Verfasst: Freitag 4. Dezember 2009, 13:03
von jerch
@snafu:
Für prettytable mußt Du doch vorher schon die Spaltenaufteilung kennen, oder?
Für csv ist das eine schöne Sache, mit curses kannst Du gleich mal Multiplan nachbauen ;)

@mkesper:
Ja, das geht mit locale, liefert aber meines Wissens nach nur eine der möglichen Sortiernormen.
Wobei ich das dann eher mit sorted(['zum', 'sortieren'], key=locale.strxfrm) machen würde.

Verfasst: Montag 8. März 2010, 16:08
von snafu
Sorry, ich hab diesen Beitrag wohl übersehen...
jerch hat geschrieben:@snafu:
Für prettytable mußt Du doch vorher schon die Spaltenaufteilung kennen, oder?
Nö, man jagt die Inhalte in die Klasse und hofft, dass die Terminalemulation breit genug für eine vernünftige Ausgabe ist. ;P
jerch hat geschrieben:Für csv ist das eine schöne Sache, mit curses kannst Du gleich mal Multiplan nachbauen ;)
Naja, von ``curses`` hab ich auch nicht gesprochen.

Verfasst: Montag 8. März 2010, 16:41
von jerch
snafu hat geschrieben:Nö, man jagt die Inhalte in die Klasse und hofft, dass die Terminalemulation breit genug für eine vernünftige Ausgabe ist. ;P
Was macht prettytable denn mit einer Liste? Hatte es mir nicht näher angeschaut...
snafu hat geschrieben:Naja, von ``curses`` hab ich auch nicht gesprochen.
Da hab ich wohl die Ironietags vergessen ;) ;)

Grüße jerch

Verfasst: Montag 8. März 2010, 16:58
von snafu
Ach, ich hab dich vermutlich missverstanden. Ja, du musst vorher Feldnamen vergeben, womit für PT dann auch die Anzahl der Spalten fest steht.

Insgesamt ist die Bedienung im Detail aber doch nicht so ganz das, was man von einem guten Tool erwarten würde. Gefühlt bestehen außerdem mindestens 50% des Codes aus "privaten" Getter-/Setter-Funktionen & Fehlerbehandlung. Irgendwie ist das alles schon etwas wirsch gemacht.

Der Autor hat das Projekt auch circa ein 3/4 Jahr nicht mehr angefasst, obwohl den "Issues" nach zu urteilen durchaus ein paar Leute Interesse an dem Projekt haben. Mal sehen, ob sich da noch was tut. Die Standardausgabe finde ich jedenfalls ganz nett gemacht für Fälle, wo man sich mal schnell einen Überblick über eine tabellenartige Struktur verschaffen möchte.

Verfasst: Montag 8. März 2010, 21:06
von bords0
@sma, HWK:

Ich weiß nicht, ob das Absicht ist, aber eure Programme liefern nicht immer die geringste Zeilenanzahl. Versucht es mal mit

Code: Alles auswählen

['x' * 14] * 5 + ['x' * 6] * 11
, da kommen bei euch 6 Zeilen raus:

Code: Alles auswählen

xxxxxxxxxxxxxx  xxxxxx  xxxxxx
xxxxxxxxxxxxxx  xxxxxx  xxxxxx
xxxxxxxxxxxxxx  xxxxxx  xxxxxx
xxxxxxxxxxxxxx  xxxxxx  xxxxxx
xxxxxxxxxxxxxx  xxxxxx  xxxxxx
xxxxxx                        
Es geht aber auch mit 5 Zeilen, nämlich:

Code: Alles auswählen

xxxxxxxxxxxxxx  xxxxxx  xxxxxx  xxxxxx
xxxxxxxxxxxxxx  xxxxxx  xxxxxx        
xxxxxxxxxxxxxx  xxxxxx  xxxxxx        
xxxxxxxxxxxxxx  xxxxxx  xxxxxx        
xxxxxxxxxxxxxx  xxxxxx  xxxxxx
(Das liegt daran, dass es zwei Möglichkeiten mit 4 Spalten gibt, und ihr untersucht in diesem Fall nur die, die nicht klappt...)

Außerdem brecht ihr ab, sobald es nicht mehr klappt, das führt bei

Code: Alles auswählen

['x', 'x', 'x' * 20, 'x' * 20, 'x', 'x']
zu

Code: Alles auswählen

x                   
x                   
xxxxxxxxxxxxxxxxxxxx
xxxxxxxxxxxxxxxxxxxx
x                   
x   
, obwohl

Code: Alles auswählen

x  xxxxxxxxxxxxxxxxxxxx  x
x  xxxxxxxxxxxxxxxxxxxx  x
möglich ist.
So, jetzt aber der Code (im Nachhinein eigentlich straight-forward :-) )

Code: Alles auswählen

def table(seq, sep="  ", line_len=40):
    num_items = len(seq)
    col_lens = sorted(set(num_items // (i + 1) for i in range(num_items)))

    for col_len in col_lens:
        cols = [seq[i:i + col_len] for i in range(0, num_items, col_len)]
        col_widths = [max(map(len, col)) for col in cols]
        width = sum(col_widths) + len(sep) * (len(cols) - 1)
        if width < line_len:
            break
    else:
        raise ValueError("Won't fit")

    cols[-1].extend([""] * (col_len - len(cols[-1])))
    cols = [[item.ljust(w) for item in col] for w, col in zip(col_widths, cols)]
    rows = zip(*cols)
    return "\n".join(sep.join(row) for row in rows)