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

7 minut(y)

W języku Python istnieją cztery podstawowe metody formatowania łańcuchów znaków (od wersji 3.6 wzwyż). W niniejszym wpisie przedstawię te metody, omówię ich wady i zalety, a w podsumowaniu udzielę przydatnych wskazówek, którą z tych metod (i w jakiej sytuacji) najlepiej stosować.

Zanim przejdę do przedstawienia szczegółów, chciałbym pokazać pewną sytuację. Załóżmy, żę mamy następujące zmienne z przypisanymi wartościami:

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

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

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

Zrealizujemy 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 klasy str. Tworzymy je poprzez umieszczenie napisu w cudzysłowach:

  • 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. Łańcuchy znaków w Pythonie są niezmienne (ang. immutable), co oznacza, że nie możemy ich samych w sobie zmieniać.

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

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

Rezultatem wykonania tego wyrażenia jest łańcuch znaków, w którym specjalna 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 użyjemy %d, a dla zmiennoprzecinkowych %f. Po szczegóły dotyczące dostępnych specyfikatorów i ich sposobów 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ć im 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ńcucha 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 twórcy języka wprowadzili “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ć sformatowanego łańcucha. 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 teraz formatowanie:

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

Tworzymy 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 programu 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 argumentów przekazywanych 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ść wyświetlanych 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 nie mogło zabraknąć też możliwości określania sposobu wyświetlania wartości w sekwencji formatującej. Aby wyświetlić np. liczbę całkowita jako jej reprezentację szesnastkową napiszemy:

>>> '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 zamiast 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 widać na powyższym - nowością jest literka f przed łańcuchem znaków (stąd nazwa f-strings). Jest to bardzo elastyczny sposób formatowania, choć niosący pewne ograniczenia. Przykładem elastyczności jest fakt, że można w łańcuchach znaków osadzać wyrażenia. Możemy dzięki temu 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!'

Korzystając z f-stringów możemy używać też obiektów utworzonych z klas. Weźmy dla przykładu następującą prostą klasę:

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 napiszemy:

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

Magiczne metody __str__() i __repr__() odpowiadają za reprezentację łańcuchową obiektu. Przynajmniej jedną z nich 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ępstwie ___str__()_.

Łańcuch znaków zwracany przez metodę __str__() jest nieformalną (informacyjną) reprezentacją znakową obiektu i powinien być czytelny dla nas programistów. Ł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 używać 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 zobaczmy jak to powinno wyglądać:

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

Prawda, że proste?

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

4. Szablony

W języku Python istnieje jeszcze jedna metoda na formatowanie łańcuchów znaków - są to szablony. Jest to prosta i mniej elastyczna metoda, jednak w pewnych przypadkach może być bardzo pomocna. Popatrzmy na prosty fragment kodu aby przybliżyć sobie zasady jakie panują dla tej metody:

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

Na początku importujemy klasę Template z wbudowanego w język Python modułu string, następnie tworzymy obiekt tej klasy przekazując do inicjalizera łańcuch znaków będący szablonem, a następnie dokonać podstawienia wartości. W tej metodzie, w odróżnieniu od pozostałych, nie wystę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 - wszędzie 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 takiech sytuacjach stanowić luki bezpieczeństwa w aplikacji. 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łośliwy użytkownik np. poprzez pole formularza przekazuje nam następują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 teraz atak 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

Nic z tego, nie udało nam się wykraść tajnych danych tym sposobem.

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. 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 jakimi się kierować przy wyborze metody 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 porównania wydajności napiszemy prosty program, który zmierzy czasy formatowania łańcuchów znaków dla każdej z metod. Skorzystamy tutaj z modułu timeit i przy jego pomocy będziemy uruchamiać metody określoną ilość razy.

from timeit import repeat
from string import Template

number = 50000

def method_1():
for i in range(number):
"Method #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