Formatowanie łańcuchów znaków w Pythonie

7 minut(y)

W języku Python istnieją cztery podstawowe sposoby na formatowanie łańcuchów znaków (od wersji 3.6 wzwyż). W niniejszym wpisie chciałbym je pokrótce omówić, przedstawić ich wady i zalety, a w podsumowaniu udzielić wskazówek, którą z metod wybierać i w jakiej sytuacji.

Zanim przejdę do szczegółowego omówienia metod przyjrzyjmy się następującej sytuacji. Załóżmy że mamy następujące zmienne:

>>> messages_count = 12
>>> user_name = 'Peter'

i naszym zadaniem jest wygenerować poniższy łańcuch znaków w formie komunikatu:

'Hello Peter, you have 12 unread message(s)'

Zrealizujmy to zadanie wszystkimi czterema metodami, tak aby te metody lepiej poznać.

1. Formatowanie łańcuchów “starą” metodą

Łańcuchy znaków w Pythonie są reprezentowane przez obiekty clasy str. Są niezmienne (immutable) i tworzymy je poprzez umieszczenie danego tekstu w cudzysłowiach:

  • pojedynczych: ‘this is a string’
  • podwójnych: “this is a string”
  • potrójnych: ‘'’this is a string’’’ lub “"”this is a string”””

Potrójne cudzysłowy pozwalają także na tworzenie łańcuchów wielolinijkowych.

Na łańcuchach znaków w Pythonie możemy wykonywać specjalne działanie z wykorzystaniem operatora %. Dzięki temu operatorowi możemy w łatwy sposób formatować wynikową zawartość. To działanie jest bardzo podobne do działania funkcji printf z języka C. Zobaczmy na przykładzie:

>>> 'Hello %s' % user_name
'Hello Peter'

Rezultatem wykonanie tego wyrażenia jest łańcuch znaków, w którym specjalnea sekwencja formatująca %s została zastąpiona wartością znajdującą się w zmiennej user_name.

Do dyspozycji mamy wiele specyfikatorów formatu, np. dla liczb całkowitych jest to %d, a dla zmiennoprzecinkowych %f. Po szczegóły dotyczące dostępnych specyfikatorów i ich użycia odsyłam do dokumentacji języka Python.

W sytuacji, gdy potrzebujemy zastosować więc niż jeden specyfikator formatu, musimy zastosować nieco inną składnię:

>>> 'Hello %s, you have %d unread messages(s)' % (user_name, messages_count)
'Hello Peter, you have 12 unread message(s)'

Możemy też tworzyć nazwane specyfikatory formatu i jawnie przekazywać pożądane wartości za pomocą słownika:

>>> 'Hello %(name)s, you have %(count)d unread messages(s)' % {'name': user_name, 'count': messages_count}
'Hello Peter, you have 12 unread message(s)'

To ostatnie rozwiązanie jest bardziej elastyczne, gdyż umożliwia łatwiejsze rozbudowanie takiego łańucha w przyszłości, a także nie wymusza na nas dbania o prawidłową kolejność przekazywanych zmiennych.

2. Formatowanie łańcuchów “nową” metodą

Wraz z wersją Pythona z linii 3 wprowadzono “nową” metodę do formatowania łańcuchów znaków. Ta “nowa” metoda pozwala nam zrezygnować z używania operatora %, przez co składnia naszego kodu staje się bardziej przejrzysta i czytelniejsza. Do klasy str dodano dodatkową metodę format(), dzięki wywołaniu której możemy wpływać na wynikową postać naszego sformatowanego łancucha. Wspomnieć tutaj należy, że metoda ta została później backportowana do Pythona 2 aby i w tej wersji języka można było z niej korzystać. Zobaczmy na prostym przykładzie jak wygląda formatowanie:

>>> 'Hello {}'.format(user_name)
'Hello Peter'

Tworzymy w nim obiekt klasy str i wywołujemy jego metodę format(), której zadaniem jest w tym przypadku, zastąpienie sekwencji {} wartością argumentu metody format. Wracając do naszego zadania kod mógłby wyglądać następująco :

>>> 'Hello {}, you have {} unread messages(s)'.format(user_name, messages_count)
'Hello Peter, you have 12 unread message(s)'

Kolejne wystąpienia sekwencji {}, są odpowiednio zastępowane wartościami przekazywanymi do metody format. Możemy skorzystać, tak jak w przypadku starej metody, z podstawiania zmiennych, aby nie musieć martwić się o kolejność argumentów. W łatwy też sposób możemy zamieniać kolejność wyswietlanych elementów, bez zmiany kolejności argumentów w metodzie format():

>>> 'Hello {name}, you have {count} unread messages(s)'.format(count=messages_count, name=user_name)
'Hello Peter, you have 12 unread message(s)'

W przypadku tej metody także nie mogło zabraknąć możliwości określania sposobu wyświetlania wartości w sekwencji formatującej. Aby np wyświetlić liczbę całkowita jako jej reprezentację szesnastkową możemy napisac:

>>> '0x{count:X}'.format(count=messages_count)
'0xC'

Składnia sekwencji formatującej jest dużo bardziej rozbudowana i daje większe możliwości niż w starej metodzie ale jednocześnie jest łatwa do używania w prostszych przypadkach. W celu zgłębienia szczegółów zachęcam do zapoznania się z dokumentacją.

Pisząc programy w Pythonie 3 oficjalna dokumentacja zaleca korzystać z nowszych metod formatowania łańcuchów kosztem starej metody. Stara metoda nadal może być wykorzystywana, nadal jest (i będzie) wspierana w kolejnych wydaniach Pythona.

3. Interpolacja łańcuchów

Interpolacja łańcuchów znaków (f-strings) została wprowadzona po raz pierwszy w Pythonie wraz z wersją 3.6. Pisząc krótko, ten nowy sposób pozwala nam osadzać wyrażenia Pythona wewnątrz stałych łańcuchów znaków. Zobaczmy jak wygląda najprostszy przykład:

>>> f'Hello, {user_name}!!!'
'Hello, Peter!!!'

Jak widzimy nowością jest literka f przed łańcuchem znaków (stąd nazwa f-strings). Jest to bardzo elastyczny sposób, choć niosący pewne ograniczenia. Z racji, że można w łańcuchah znaków osadzać wyrażenia, możemy wykonywać operacje arytmetyczne wewnątrz:

>>> f'{messages_count+3} or {10+5}'
'15 or 15'

Oprócz wyrażeń możemy także bezpośrednio wywoływać funkcje/metody:

>>> def to_uppercase(s):
...    return s.upper()

>>> name = 'Peter Wright'
>>> f'Hello, {to_uppercase(name)}!'
'Hello, PETER WRIGHT!'

>>> f'Hello, {name.lower()}!'
'Hello, peter wright!'

Także możemy używać obiektów utworzonych z klas. Weźmy dla przykładu nastapującą prostą klase:

class Person:
    def __init__(self, first_name, last_name, age):
        self.first_name = first_name
        self.last_name = last_name
        self.age = age

    def __str__(self):
      return f'{self.first_name} {self.last_name} is {self.age}.'

    def __repr__(self):
      return f'{self.first_name}-{self.last_name}-{self.age}'

I na jej podstawie możemy napisać:

>>> person = Peron('Peter', 'Wright', 34)
>>> f'{person}
'Peter Wright is 34.'

Magiczne metody __str__() i __repr__() odpowiadają za reprezentację łańcuchową obiektu, więc przynajmniej jedną powinniśmy umieścić w definicji klasy. Jeśli musiałbyś wybierać którą z tych dwóch metod implementować, to wybierz __repr()__, gdyż może ona być używana w zastępstwi __str__().

Łańcuch znaków zwracany przez metodę __str__() jest nieformalną (informacyjną) reprezentacją znakową obiektu i powinien być czytelny dla nas. Łańcuch znaków zwracany przez metodę __repr__() jest to formalna (oficjalna) reprezentacja napisowa obiektu i powinna być jednoznaczna, a otrzymany łańcuch znaków powinien być poprawnym wyrażeniem w Pythonie. W metodzie interpolacji łańcuchów domyślnie używana jest metoda __str__() (podobnie ma to miejsce w przypadku format()), aby uzywać metody __repr__() przy formatowaniu musimy jawnie wyspecyfikować flagę konwersji !r:

>>> f'{person}
'Peter Wright is 34.'
>>> f'{person!r}
'Peter-Wright-34.'

W tym miejscu, każdy z Was powinien już umieć napisać kod realizujący nasze zadanie za pomocą f-stringów. Dla formalności zoabczmy jak to zapisać:

>>> f'Hello {user_name}, you have {messages_count} unread messages(s)'
'Hello Peter, you have 12 unread message(s)'

Dla osób czujących potrzębę zanurkowania głębiej podaję link do szczegółów specyfikacji.

4. Szablony

W języku Python isntnieje jeszcze jedna metoda na formatowanie łańcuchów znaków - są to szablony. Jest to prostsza i mniej elastyczna metoda, jednak w pewnych przypadkach może być bardzo pomocna. Popatrzmy na prosty fragment kodu:

>>> from string import Template
>>> template = Template('Hello, $name!')
>>> template.substitue(name=user_name)
'Hello, Peter!'

Na początku musimy zaimportować klasę Template z wbudowanego w Pythona modułu string, utworzyć obiekt tej klasy przekazując łańcuch znaków, a następnie dokonać podstawienia wartości. W tej metodzie, w odróżnieniu od pozostałych nie wsytępują specyfikatory formatów, dlatego musimy wcześniej sami zatroszczyć się o odpowiednie konwersje i przekazać przygotowane już wartości w zmiennych do podstawienia.

Rodzi się zatem pytanie, kiedy używać tej metody w programach. W mojej opinii, tam gdzie przetwarzamy łańcuchy znaków otrzymane od użytkowników programu. Z racji ograniczonej funkcjonalności wykorzystywanie szablonów w tym wypadku będzie najbezpieczniejszym wyborem. Jak się okazuje bardziej rozbudowane metody formatowania mogą w takiej sytuacji stanowić luki bezpieczeństwa w naszym programie. Złośliwy użytkownik może tak przygotować łańcuch znaków do formatowania aby wykraść nam jakieś wrażliwe dane. Zobaczmy jakby to mogło wyglądać:

>>> # To jest nasze tajne hasło
>>> SECRET_PASSWORD = 'my-secret-password'

>>> class Message:
>>>    def __init__(self):
>>>        pass

>>> # złosliwy użytkownik np. poprzez pole formularza przekazuje nam nastepujący tekst
>>> user_input = '{message.__init__.__globals__[SECRET_PASSWORD]}'

>>> msg = Message()
>>> user_input.format(message=msg)
'my-secret-password'

Ups.. Potencjalny atakujący może dostać sie do słownika __globals__ poprzez ciąg formatujący. Powtórzmy to teraz dla szablonów:

>>> user_input = '${message.__init__.__globals__[SECRET_PASSWORD]}'
>>> Template(user_input).substitute(messages=msg)
ValueError: Invalid placeholder in string: line 1, col 1

No dobrze, to której metody mam używać?

Poznaliśmy już dostępne metody formatowania łańcuchów i zasady ich działania w Pythonie to teraz przyszła pora na podsumowanie. Wobec mnogości dostępnych metod mogą pojawić się wątpliwości co do tego, której metody używać. O części zasad już wspomniałem wyżej, ale chciałbym jeszcze pokazać to w formie jednej praktycznej porady:

python strings diagram

Jeśli formatowane łańcuchy pochodzą od użytkowników używamy szablonów (#4) ze względów bezpieczeństwa. W przeciwnym wypadku używamy Interpolacji łańcuchów (f-strings) (#3) jeśli piszemy kod dla Pythona 3.6+, albo “nowej” metody (#2) dla starszych wersji języka.

Na zakończenie przyjrzyjmy się jeszcze wydajności prezentowanych metod. Do zbadania i prównania wydajności napiszemy prosty program, który zmierzy czasy formatowania łańcuchów znaków dla każdej z meteod. Skorzystamy tutaj z modułu timeit i przy jego pomocy będziemy uruchamiać metody określoną liczbę razy.

from timeit import repeat
from string import Template

number = 50000


def method_1():
    for i in range(number):
        "Metohd #1 - %s" % i


def method_2():
    for i in range(number):
        "Method #2 - {}".format(i)


def method_3():
    for i in range(number):
        f"Method #3 - {i}"


def method_4():
    template = Template("Method #4 - $value")
    for i in range(number):
        template.substitute(value=i)


time_1 = repeat(stmt=method_1, repeat=5, number=100)
time_2 = repeat(stmt=method_2, repeat=5, number=100)
time_3 = repeat(stmt=method_3, repeat=5, number=100)
time_4 = repeat(stmt=method_4, repeat=5, number=100)

print("Method #1: {} secs".format(min(time_1)))
print("Method #2: {} secs".format(min(time_2)))
print("Method #3: {} secs".format(min(time_3)))
print("Method #4: {} secs".format(min(time_4)))

Wykonaliśmy dla każdej metody 5 serii po 5 milionów podstawień i otrzymaliśmy następujące wyniki:

Method #1: 0.8946281759999692 secs
Method #2: 1.281472788999963 secs
Method #3: 0.7678051210004924 secs
Method #4: 16.648385618999782 secs

Same wartości czasowe dla poszczególnych metod nie są o tyle istotne, co różnice między nimi. Widać wyraźnie, że pod względem wydajnościowym najlepiej wypada metoda #3 interpolacji łańcuchów, a metoda z wykorzystaniem szablonów jest zdecydowanie najmniej wydajna szybkościowo. Metoda .format() jest nieco mniej wydajna w porównaniu do operatora ‘’%’’.

Zostaw komentarz