🚀 go-pugleaf

RetroBBS NetNews Server

Inspired by RockSolid Light RIP Retro Guy

Article View: pl.comp.lang.python
Article #27526

Re: String, unicode, dziwny problem i 5 pytań - długie

#27526
From: Jan Kaliszewski
Date: Fri, 18 Jun 2010 18:36
283 lines
13015 bytes
Piotr Pastuszka <piotr.pastuszka@wp.pl> napisał (2010-06-16):
[...]
> PYTANIE  1.
> Co powoduje, że powyższy kod uruchamiany na tym samym komputerze, ale u
> różnych użytkowników działa dobrze lub źle.

Ustawienia kodowania znaków terminala/wiersza poleceń -- czy mówiąc
ogólniej -- środowiska, w którym jest uruchomiany Python. Podany przez
Ciebie kod wysyła na zewnątrz tekst zakodowany kodowaniem utf-8, więc
prawidłowe znaki wyświetlną się tylko, jeżeli środowisko (terminal/wiersz
poleceń/IDE, w którym osadzony jest python etc.) też ma ustawione utf-8
(no i jeżeli font, którego owe środowisko używa, zawiera znaki, których
użyłeś.

> PYTANIE 2.
> Co powoduje, że w niektórych edytora powyższy kod działa "ładnie", a na
> niektórych "nieładnie"

Patrz punkt 1.


> "
> Łańcuch znaków przechowuje znaki w zakodowanej postaci np. w systemie  
> UTF-8,
> ISO 8859-1, czy ISO 8859-4; może być wieloznaczny. Natomiast unikod jest
> jednoznaczny, więc nie jest zakodowany w tego typu systemach kodowania.
> Ponieważ łańcuch znaków jest zakodowany, musimy odkodować (za pomocš
> decode), aby otrzymać niezakodowany unikod. Z kolei unikod, ponieważ jest
> niezakodowany, musimy zakodować (za pomocą encode), aby otrzymać  
> zakodowany łańcuch znaków.
> "

Święta prawda. W Pytonie 2.x są dwa typu napisowe: str i unicode. Obiekt
str to sekwencja elementów, z których każdy reprezentowany jest przez
1 bajt czyli liczbę całkowitą od 0 do 255. A więc, by zinterpretować
obiekt str jako tekst zawierający np. polskie litery, musimy określić
jakiego *kodowania* (czyli jakie wartości lub ciągi wartości oznaczają
jakie znaki) użyjemy do jego odczytu; tak samo przy zapisie: musimy
określić *kodowanie*, czyli -- jakiej literze (znakowi) odpowiada jaka
wartość liczbowa. Przy czym są kodowania, jak np. iso-8859-2 czy cp-1250,
które wszystkim objętym przez siebie znakom przyporządkowują 1-bajtowe
wartości; a są też kodowania, takie jak utf-8 czy utf-16, w przypadku
których 1 znak może być zakodowany za pomocą kilku bajtów (np.
w przypadku utf-8 litera 'a' kodowana jest, tak jak w ASCII, za pomocą
wartości 97, ale już np. litera 'ń' -- za pomocą ciągu: 197, 132
[w zapisie szestanstkowym: 0xc5, 0x84]).

Obiekt unicode to sekwencja elementów, z których każdy reprezentowany
jest przez tzw. code point czyli liczbę całkowitą od 0 do wielu wielu
tysięcy (ilu dokładnie, to zależy od wersji standaru Unicode i innych
detali, którymi w tym momencie się nie zajmuję). A więc dany kod (code
point) odpowiada zawsze temu samemu znakowi -- określa to standard
Unicode, np. literze 'ń' odpowiada liczba 324 czuli 0x144 w zapisie
szesnastkowym (patrz: http://www.decodeunicode.org/de/u+0144). Dzięki
dużej liczbie możliwych kodów (code point'ów), standard Unicode obejmuje
znaki z bardzo wielu (jeżeli nawet nie wszystkich, to prawie wszystkich)
języków świata.

Kilka wniosków i spostrzeżeń:

* gdy przerabiamy obiekt unicode na obiekt str -- kodujemy go, np.:
   napis_str = napis_unicode.encode('cp-1250')

* gdy przerabiamy obiekt str na obiekt unicode -- dekodujemy go, np.:
   napis_unicode = napis_str.decode('cp-1250')
   lub
   napis_unicode = unicode(napis_str, 'cp-1250')

* unicode i utf-8 to dwie różne rzeczy! (to pierwsze to pythonowy typ
   obiektów napisowych zawierających znaki określone standardem Unicode;
   to drugie to sposób kodowania takich znaków, jeżeli są przechowywane
   w postaci obiektu str).

> Czyli moje oznaczanie plików UTF-8 NIC NIE ZNACZY! Ze dopóki nie wskaże
> Pythonowi, że chcę na wyjściu mieć kod unicode to muszę albo mieć na  
> wejściu unicod, lub ODCZYTAC/ODSZYFROWAĆ (decode) zapisany teks do
> kodu unicod.

> PYTANIE 4
> Czemu musze robić np ("Gotówka  wysłane").decode('utf-8') skoro jest  
> nagówek
> #-*- coding:utf-8 -*-
> Przecież ten nagłówek jawnie wskazuje, że mam 'utf-8' więc po co muszę to
> wskazywać w kodzie? czemu sam python tego nie zauważa ?.
> Uogólniajšc co robi kontrukcja #-*- coding:utf-8 -*- ?
> Gdybym miał strzelać powiedziałby, że dla Pythona nie znaczy NIC, że to
> informacja dla EDYTORA.

Oznaczenie na początku pliku typu # -*- coding: utf-8 -*- informuje Pythona
jakiego kodowania ma użyć, by odczytać Twój kod źródłowy (który wszak jest
plikiem, a więc w sensie logicznym -- tylko ciągiem bajtów). Deklaracja ta
musi być zgodna z faktycznym kodowaniem, jakiego dostarcza edytor tekstu,
za pomocą którego piszesz kod (inaczej Python albo od razu wywali błąd,
albo zinterpretuje Twój kod niezgodnie z Twoją intencją). Raz jeszcze
podkreślę: ta deklaracja na początku pliku dotyczy *wyłącznie* kodowania
*kodu źródłowego* Twojego programu, i niczego innego w tym programie.

A więc, jeżeli Twój edytor jest ustawiony na utf-8 i jeżeli napiszesz:

     # -*- coding: utf-8 -*-
     print "żółć"

...twój program prześle na wyjście ciąg bajtów, będący napisem "żółć"
zakodowanym kodowaniem utf-8 (0xc5, 0xbc, 0xc3, 0xb3, 0xc5, 0x82, 0xc4,  
0x87)

Natpmiast, jeżeli Twój edytor jest ustawiony na iso-8859-2 i napiszesz:

     # -*- coding: iso-8859-2 -*-
     print "żółć"

...twój program prześle na wyjście ciąg bajtów, będący napisem "żółć"
zakodowanym kodowaniem iso-8859-2 (0xbf, 0xf3, 0xb3, 0xe6).

Oczywiście, tak jak napisałem wyżej, terminal/wiersz poleceń/środowisko
programistyczne, pod którym uruchamiasz program wypisze właściwe znaki
tylko wtedy jeżeli skonfigurowane jest pod takie a nie inne kodowanie.

Czyżby stworzenie programu uruchamialnego na różnych maszynach jest
skomplikowane i wymaga czegoś jak poniżej?

     # -*- coding: iso-8859-2 -*-
     kodowanie = raw_inpit('Podaj kodowanie Twojego srodowiska:')
     print "żółć".decode('iso-8859-2').encode(kodowanie)

Na szczęście Python sam stara się dowiedzieć, jakiego kodowania
używa środowisko (terminal, wiersz poleceń bądź IDE czyli środowisko
programistyczne takie jak SPE...) w którym został uruchomiony --
informację tę można pobrać z sys.stdout.encoding, np.

     # -*- coding: iso-8859-2 -*-
     import sys
     print "żółć".decode('iso-8859-2').encode(sys.stdout.encoding)

Ale nie dość na tym. print jest na tyle mądre, że jeżeli podamy mu
obiekt unicode, a nie str, zakoduje ten obiekt kodowaniem takim,
jakie jest w sys.stdout.encoding -- a więc możemy napisać:

     # -*- coding: iso-8859-2 -*-
     print "żółć".decode('iso-8859-2')  # przekazujemy obiekt unicode

Przy czym -- to osobna sprawa, ale warto ją zauważyć i stosować gdzie
się da -- zamiast tworzyć obiekt str (z str-owego literału "żółć")
i następnie go dekodować do postaci unicode za pomocą kodowania jakim
kodowane są źródłą naszego programu -- możemy od razu utworzyć obiekt
unicode (z unicodowego literału u"żółć"):

Mówiąc bardziej ogólnie:

     # -*- coding: JAKIES_KODOWANIE_ZRODEL -*-
     .......
     ....... "żółć".decode(JAKIES_KODOWANIE_ZRODEL) .......
     .......

jest równoznaczne z:

     # -*- coding: JAKIES_KODOWANIE_ZRODEL -*-
     .......
     ....... u"żółć" .......
     .......

A wracając do naszego przykładu:

     # -*- coding: iso-8859-2 -*-
     print u"żółć"

Przekaże na wyjśćie ciąg bajtów będącym napisem "żółć" zakodowanym
takim kodowaniem, jakie Python wykrył i zapisał w sys.stdout.encoding.

Oczywiście może się zdarzyć, że w sys.stdout.encoding znajdzie się
kodowanie nie obsługujące podanych znaków -- np. gdy standardowe wyjście
przekierowano do pliku (sys.stdout.encoding zawiera wówczas kodowanie
ascii); powyższy kod zwróci wówczas błąd:

     UnicodeEncodeError: 'ascii' codec can't encode characters in position  
0-3: ordinal not in range(128)

Więc w programach, które nie są prostymi skryptami czy programikami
do użytku własnego i znajomych, można dać np.:

     # -*- coding: iso-8859-2 -*-
     try:
         print u"żółć"
     except UnicodeEncodeError:
         print u"żółć".encode(errors=ignore)
         # ewentualnie: print u"żółć".encode('unicode_escape')

lub nawet:

     # -*- coding: iso-8859-2 -*-
     import locale
     KODOWANIE_Z_LOCALE = locale.getpreferredencoding()

     try:
         print u"żółć"
     except UnicodeEncodeError:
         print u"żółć".encode(KODOWANIE_Z_LOCALE, errors=ignore)

> ========================
> print unicode("Gotówka wysłane",'utf-8') odszyfrowanie do unicode stringu
> zaszyfrowanego UTF-8
> ^
> SyntaxError: invalid syntax
> Script terminated.
> ========================
[w związku z pytaniem 3]

Przypuszczam, że:

* albo zapomniałeś, by przed komentarzem "odszyfrowanie do unicode
stringu..." dać znacznik komentarza #

* albo uruchomiłeś Pythona 3.x, który nie ma słowa kluczowego print, ma
natomiast funkcję print() (pierwsza rzucająca się w oczy różnica, jest
taka, że zawsze trzeba używać nawiasów)

* albo zadeklarowane kodowanie źródeł programu jest niezgodnie z
kodowaniem faktycznie używanym przez edytor, pod którym pisałeś kod
(nie sądzę, żeby to było to, ale teoretycznie w pewnych sytuacjach też
może to spowodować błąd składniowy).

> PYTANIE 5
> W którymś poście na grupie pojawia zapis:
> sys.setdefaultencoding('utf8')
> czyli coś o czym wcześniej nie czytałem. i pytanie co robi ten kod?

W 99% przypadków nie należy tego ruszać (zresztą celowo jest to
utrudnione -- poprzez usuwanie tej funkcji z modułu sys zaraz po
pierwszym jej użyciu w module site...).

Funkcja ta ustawia tzw. domyślne kodowanie Pythona -- dla sytuacji,
w których programista nie określił, jakie kodowanie ma być użyte do
zakodowania/rozkodowania... Np. jeżeli użyjemy encode()/decode()
bez podawania kodowania, lub jeżeli (jest wiele takich sytuacji)
Python dostał unicode, a spodziewał się str, lub jeżeli dostał str, a
spodziewał się unicode -- sam niejawnie rozkodowuje/zakodowuje dane
by uzyskać właściwy typ; używa wtedy owego domyślnego kodowania.

Domyślne kodowanie jest standardowo ustawione na ascii (obejmje
tylko najbardziej standardowe 128 znaków: alfabet łaciński,
podstawowe znaki matematyczne i przestankowe, podstawowe kody
kontrolne, np. znak końca wiersza...), co oznacza, że np. kod:

     u"żółć".encode()

...spowoduje wystąpienie wyjątku UnicodeEncodeError.

----

Na koniec kilka morałów:

* w odniesieniu do tekstu -- wszędzie gdzie się da, należy używać
   obiektów unicode a nie str, a jedynie na wejściu/wyjściu
   odkodowywać/zakodowywać z/do postaci ciągu bajtów (str),

* na dłuższą metę lepiej podawać kodowanie jawnie ("explicit is
   better than implicit" -- Zen of Python) i raczej unikać sytuacji,
   w których Python będzie dokonywać automatycznego-niejawnego
   kodowania/dekodowania (bo gdy wyskoczy błąd związany z kodowaniem,
   często trudno potem dojść, gdzie tak naprawdę nastąpiło
   zniekształcenie danych...).


Pozdrawiam,

Jan Kaliszewski (zuo)


PS. UWAGA: cała moja odpowiedź dotyczy Pythona 2.x. W Pythonie 3.x
rzeczy wyglądają trochę inaczej: dawny typ unicode nazywa się str,
a dawnego str nie ma. Jest za to typ bytes (i pokrewny mu bytearray...),
do pewnego stopnia podobny do dawnego str, ale nieprzeznaczony do
operowania danymi tekstowymi, a jedynie binarnymi (ale to inna historia).

W Pythonie 3.x nie ma też niejawnego automatycznego kodowania/dekodowania
-- doświadczenia Pythona 2.x pokazują, że sprawiało ono wiele kłopotów.
W Pythonie 3.x programista, jeżeli poda dane złego typu otrzyma jasny
komunikat błędu, a nie nieprawidłowe wyniki zmuszające do bawienia się
w Sherlocka Holmesa ("w którym momencie Python dokonał tego niejawnego
kodowania/dekodowania?").

-- 
Jan Kaliszewski (zuo)
Technical Details
Message-ID: <op.veidzb06fvx12t@jerzozwiesz.localdomain>
Path: mbox-mo1.pugleaf.net!news-archive.icm.edu.pl!news.icm.edu.pl!plix.pl!newsfeed1.plix.pl!nntp.aster.pl!newsread1.aster.pl!not-for-mail
References: <hvb91g$rpp$1@node2.news.atman.pl>

Show Full Headers
Complete Article Headers
Path: mbox-mo1.pugleaf.net!news-archive.icm.edu.pl!news.icm.edu.pl!plix.pl!newsfeed1.plix.pl!nntp.aster.pl!newsread1.aster.pl!not-for-mail
From: "Jan Kaliszewski" <zuo@chopin.edu.pl>
Newsgroups: pl.comp.lang.python
Subject: Re: String, unicode, dziwny problem i 5 pytań -
 długie
Date: Fri, 18 Jun 2010 20:36:37 +0200
Organization: Aster Sp. z o.o.
Lines: 283
Message-ID: <op.veidzb06fvx12t@jerzozwiesz.localdomain>
References: <hvb91g$rpp$1@node2.news.atman.pl>
NNTP-Posting-Host: 62.121.72.3
Mime-Version: 1.0
X-Trace: newsread1.aster.pl 1276886060 19735 62.121.72.3 (18 Jun 2010 18:34:20 GMT)
X-Complaints-To: abuse@aster.pl
NNTP-Posting-Date: Fri, 18 Jun 2010 18:34:20 +0000 (UTC)
User-Agent: Opera Mail/10.10 (Linux)
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit