Funkcja join()

2011-04-29 21:24

Będąc w zgodzie z podzielanym przez siebie poglądem o kluczowej a często niedocenianej roli “małych” algorytmów, dzisiaj wezmę pod lupę funkcję do łączenia napisów, znaną większości jako join. Ta przydatna operacja występuje w wielu językach i bibliotekach, a jej brak w pozostałych jest zwykle wyraźnie odczuwalny (tak, Java, o tobie mówię). Dobrze użyty join – zwłaszcza w połączeniu z pewnymi specyficznymi mechanizmami językowi – potrafi zapobiec pisaniu niepotrzebnych pętli, znacząco redukując code bloat.

Ale po kolei. join to operacja polegająca na złączeniu kolekcji napisów w jeden, odpowiednio sklejony łańcuch. Łączenie polega na tym, że w wyniku pomiędzy elementami kolekcji wstawiony jest pewien określony ciąg (“klej”). Najlepiej widać to na przykładzie:

  1. array = ["Ala", "ma", "kota"]
  2. text = str.join(" ", array)
  3. assert text == "Ala ma kota"

Łatwo zauważyć, że join jest w gruncie rzeczy przeciwieństwem funkcji split, którą nieprzypadkowo kiedyś już opisywałem :)

W czym przejawia się przydatność tej operacji? Przede wszystkim rozwiązuje ona “problem ostatniego przecinka” przy wypisywaniu list. Tradycyjnie obchodzi się go mniej więcej tak:
for (int i = 0; i < (int)strings.length(); ++i) { std::cout << strings[i]; if (i + 1 < (int)strings.length()) std::cout << ", "; }[/cpp] Instrukcja if w tej pętli nie jest oczywiście szczytem elegancji. Gdybyśmy mieli tu funkcję join wszystko byłoby o wiele czytelniejsze:
std::cout << join(", ", strings);[/cpp] Drugą zaletą joina jest jego dobra współpraca z modnymi ostatnio, funkcyjnymi rozszerzeniami wielu języków, pozwalająca w zwięzły sposób przetwarzać kolekcje obiektów. Jeśli na przykład mamy słownik (tudzież mapę/hash), to zamiana go na tekstowy odpowiednik klucz=wartość jest prosta:

  1. import os
  2. def join_dict(d):
  3.      # os.linesep to separator wierszy właściwy dla systemu
  4.     return str.join(os.linesep, map(lambda item: "%s=%s" % item, d.items()))
  5.  
  6. data = { "fullscreen": 1, "width": 800, "height": 600 }
  7. print join_dict(data)
  8. # fullscreen=1
  9. # width=800
  10. # height=600

Oczywiście jest tak wówczas, gdy na widok słowa kluczowego lambda nie uciekamy z krzykiem ;-)

Na koniec tej krótkiej pogadanki wypadałoby jeszcze zaprezentować przykładową implementację omawianej funkcji. Ponieważ – jak napomknąłem wcześniej – doskwierał mi ostatnio jej brak w Javie, więc kod będzie w tym właśnie języku:

  1. public static String join(final Collection<?> s, final String delimiter) {
  2.  
  3.     final StringBuilder builder = new StringBuilder();
  4.     final Iterator<?> iter = s.iterator();
  5.     while (iter.hasNext()) {
  6.         builder.append(iter.next());
  7.         if (!iter.hasNext()) break;
  8.         builder.append(delimiter);
  9.     }
  10.     return builder.toString();
  11. }

Z dokładnością do szczegółów generycznych kolekcji i operacji na stringach, powyższą implementację powinno się dać łatwo przetłumaczyć także na C++.

Tags: , , , ,
Author: Xion, posted under Programming »


7 comments for post “Funkcja join()”.
  1. Jarek:
    April 29th, 2011 o 21:29

    Osobiście uważam że to jednak lambda i map są bohaterami tego wpisu ;)
    Dzięki nim join/split (mógłbyś też dać przykład z filter()) czy używanie *args jest tak przyjemne (choć potrafi to zaciemnić kod jeśli ktoś nie ma wprawy w czytaniu lambd).

  2. Xion:
    April 29th, 2011 o 21:40

    Przykład z map można było też zapisać bez tej funkcji:

    1. def join_dict(d):
    2.     return str.join(os.linesep, ["%s=%s" % item for item in d.items()])

    Nie mogłem się jednak zdecydować, który z tych dwóch wariantów dla osób niezbyt biegłych w Pythonie będzie mniej niezrozumiały ;) Skłoniłem się ku map, bo list comprehension nie jest tak oczywiście funkcyjnym ficzerem :)

  3. Dab:
    April 29th, 2011 o 21:44

    Na wielkiego jeża, mogłeś chociaż przenieść tego ifa przed drukowanie elementu i zapisać go po prostu jako if (i) :)

  4. Dab:
    April 29th, 2011 o 21:52

    Notabene C++ ma coś takiego jak join, oczywiście na miksie szablonów, iteratorów i strumieni:
    http://www.sgi.com/tech/stl/ostream_iterator.html

    1. vector<string> V;
    2. copy(V.begin(), V.end(), ostream_iterator<string>(cout, ", "));

    Brr. Idę umyć ręce :)

  5. zwierzak:
    April 29th, 2011 o 22:44

    Istnieje ciekawsze rozwiązanie join bez ifa za każdym razem.

    1. public static String join(final<?> Collection s, final String delimiter) {
    2.      final StringBuilder builder = new StringBuilder();
    3.      final Iterator<?> iter = s.iterator();
    4.      while (iter.hasNext()) {
    5.          builder.append(iter.next());
    6.          builder.append(delimiter);
    7.      }
    8.      builder.setLength(builder.length() - delimiter.length());
    9.      return builder.toString();
    10. }

    Jeżeli przeszkadza Ci narzut O(n) przy uruchamianiu length, to możesz to rozłożyć w pętli.

  6. gryf:
    April 30th, 2011 o 16:15

    em…
    bardziej pythonicznie by było (bez lambdy i mapy):

    import os
    data = {"fullscreen": 1, "width": 800, "height": 600}
    print os.linesep.join(["%s=%s" % (key, val) for key, val in data.items()])

  7. higrys:
    April 30th, 2011 o 17:51

    A ja od jakiegoś czasu stosuję takie, wg. mnie dość eleganckie rozwiązanie, które napotkałem podczas przeglądania znalezionego kodu opensourceowego. Szybkie, sprawne, zamiast if-a jest przypisanie literału, więc chyba efektywnie szybsze. Bez niepotrzebnego iteratora i sprawdzania długości:

    1. public static String join(final Collection s, final String delimiter) {
    2.   final StringBuilder builder = new StringBuilder();
    3.   String delimiter="";
    4.   for(Object current: s) {
    5.      builder.append(delimiter);
    6.      builder.append(current);
    7.      delimiter=",";
    8.   }
    9.   return builder.toString();
    10. }
Comments are disabled.
 


© 2017 Karol Kuczmarski "Xion". Layout by Urszulka. Powered by WordPress with QuickLaTeX.com.