len() Fonksiyonu ve ascii’nin Laneti¶
Bildiğiniz gibi, içinde Türkçe karakterler geçen bir program yazdığımızda betiğimizin en başına şu satırı eklememiz gerekir:
#-*-coding:utf8-*-
Not: Windows kullananlar “utf8” yerine “cp1254” kodlamasını kullanabilir...
Diyelim ki şöyle bir şey var elimizde:
#!/usr/bin/env python
print "Türkçeye özgü karakterler: şçöğüı"
Burada olduğu gibi, “#--coding:utf8--” satırını eklemezsek şuna benzer bir hata alırız:
File "deneme.py", line 3
SyntaxError: Non-ASCII character '\xc3' in file deneme.py on line 3,
but no encoding declared; see http://www.python.org/peps/pep-0263.html for details
Burada Python bize ASCII olmayan bir karakter kullandığımızı, üstelik uygun bir kodlama düzeni de belirtmediğimizi söylüyor. Bu hatayı almamak için betiğimizi şu hale getirmemiz gerekir:
#!/usr/bin/env python
#-*-coding:utf8-*-
print "Türkçeye özgü karakterler: şçöğüı"
Burada eklediğimiz “#--coding:utf8--” satırı yardımıyla betiğimizin kodlama düzenini “utf8” olarak belirledik. Eğer betiğimiz içinde kendimiz bir kodlama düzeni belirtmezsek, Python otomatik olarak “ascii” kodlamasını kullanacaktır. Şu komutun çıktısına baktığımızda gerçekten de Python’un varsayılan kod çözücüsünün “ascii” olduğunu görüyoruz:
>>> import sys
>>> sys.getdefaultencoding()
'ascii'
Peki bu “ascii” denen şey de ne oluyor?
“ascii” kelimesi İngilizce “American Standard Code for Information Interchange” (Bilgi Alışverişi için Amerikan Standart Kodu) ifadesinin kısaltması... “ascii” denen karakter kodlama düzeni İngiliz alfabesi temel alınarak 1960’lı yıllarda hazırlanmış. Amacı da o dönemde kullanımda olan yazı makinelerinin standartlaşmasını ve böylece bu makinelerin birbirleriyle uyumlu çalışmasını sağlamak. “ascii” kelimesini “askii” şeklinde telaffuz ediyoruz... “ascii” kodlamasında, İngiliz alfabesindeki her harf bir sayıya karşılık geliyor. Mesela “65” sayısının karşılığı “A” harfidir... Şu kod yardımıyla bunu doğrulayabiliriz:
>>> print chr(65)
A
Örneğin “100” sayısının hangi karaktere karşılık geldiğine bakalım:
>>> print chr(100)
d
Demek ki “100” sayısı “d” harfine karşılık geliyormuş. Standart “ascii” kodlamasında toplam 128 karakter bulunur. Bunların ilk 33 tanesi ekranda görünmeyen karakterlerdir. Bu karakterlere, yazılan bir metnin akışını kontrol etme görevi gördükleri için, “kontrol karakterleri” adı veriliyor. Geri kalan 95 karakter ise ekranda bilfiil gördüğümüz karakterlerdir. ascii kodlamasında hangi sayının hangi karaktere denk geldiğini bulmak için http://www.asciitable.com/ adresindeki tablodan faydalanabilirsiniz. Ayrıca isterseniz şu komutu kullanarak kendiniz de benzer bir ascii tablosu üretebilirsiniz:
>>> for i in range(128):
... print chr(i)
Bu tabloda hangi sayının hangi karaktere karşılık geldiğini açık açık görebilmek için ise şöyle bir şey yazabilirsiniz:
>>> for i in range(128):
... print "%s => %c"%(i,i)
Burada “%s”, 128 aralığındaki sayıların “bildiğimiz” halini; “%c” ise bu sayıların “char” yani “karakter” karşılıklarını gösteriyor... Peki burada neden 128’e kadar olan sayıları kullandık? Çünkü yukarıda da söylediğimiz gibi ascii tablosunda toplam 128 karakter bulunur. Yani bu tabloda sadece 128 adet karakter tanımlanmıştır. Bu durumun bir yansımasını, daha önce bir yerlerde muhtemelen karşınıza çıkmış olan şu hata mesajından da hatırlıyor olabilirsiniz:
UnicodeDecodeError: 'ascii' codec can't decode byte 0xc5 in position 0:
ordinal not in range(128)
Buradan anladığımıza göre, Python’un varsayılan kod çözücüsü olan “ascii”, konumu belirtilen karakteri çözümleyemiyormuş. Yani söz konusu karakter, yukarıda bahsettiğimiz 128 karakterlik tablonun dışında kalıyormuş...
ascii tablosuna baktığımız zaman, Türkçe harflerin (şçöğı) bu tabloda bulunmadığını görüyoruz. İşte biz betiğimizde “utf8” gibi bir kodlama düzeni belirtmediğimiz zaman, Python’un varsayılan kodlama düzeni olan “ascii”, tabloda bulunmayan bu karakterleri çözümleyemiyor. Betiğimizde “utf8” kodlaması kullanarak Python’un “ascii” yerine “utf8”i kullanmasını sağlıyoruz. “utf8” kodlaması, “ascii”nin aksine Türkçe karakterleri de çözümleyebiliyor.
Şimdi yukarıda anlattığımız meseleye biraz daha yakından bakalım. Hemen Python komut satırında şunu yazıyoruz:
>>> "a"
Buradan elde edeceğimiz çıktı tabii ki şöyle olacaktır:
'a'
Gördüğünüz gibi Python “a” harfini doğru bir şekilde algıladı. Şimdi bir de şuna bakalım:
>>> "ş"
Burada Türkçe’ye özgü bir harf olan “ş”yi kullandık. Bakalım Python ne yapacak?
'\xc5\x9f'
Python, bu harfi “a” harfinden farklı bir şekilde yorumladı. Bunun nedeni “a” harfinin “ascii” tablosunda bulunuyorken, “ş” harfinin bu tabloda bulunmuyor olması...
Şimdi bu konuya ufak bir ara verelim. Aslında ara vermekten ziyade aynı konuya başka bir pencereden bakmaya devam edeceğiz...
Hepimizin bildiği gibi, Python’da bir nesnenin boyutunu öğrenmek için len() fonksiyonundan yararlanıyoruz. En basit şekliyle bu fonksiyonu şu şekilde kullanabiliriz:
>>> a = "python"
>>> len(a)
6
Demek ki “python” karakter dizisi içinde 6 karakter varmış. Bir de şuna bakalım:
>>> b = "çilek"
>>> len(b)
6
Burada len() fonksiyonu “6” sonucunu verdi. Ama gördüğünüz gibi aslında “çilek” karakter dizisi 6 değil, 5 karakterden oluşuyor!.. Şimdi bu tuhaflığın nereden kaynaklandığını bulmaya çalışalım:
>>> len("s")
1
>>> len("a")
1
>>> len("ç")
2
Gördüğünüz gibi, “a”, “s” gibi harfler normal olarak 1 karakter uzunluğunda, ama “ç” harfi öyle değil... Dolayısıyla Python “çilek” karakter dizisi içinde geçen “ç” harfini tek başına 2 saydığı için toplam 6 karakter buluyor... Burada “a” ve “s” harflerinin ortak özelliği, bunların “ascii” kod tablosunda yer alıyor olması. Ya da kabaca şöyle diyebiliriz: “a” ve “s” harflerinin ortak özelliği her ikisinin de İngiliz alfabesinde bulunuyor olması... Dolayısıyla bunlar “ascii” ile kodlandığında tek karakter şeklinde temsil edilebiliyor. “ç” harfinin suçu ise Türkçe’ye özgü karakterlerden biri olması... Aynı durumu, Türkçe’ye özgü öteki karakterlerde de görüyoruz:
>>> len("ı")
2
>>> len("ş")
2
>>> len("ö")
2
>>> len("ğ")
2
Yukarıda Python’un “ş” harfini nasıl gösterdiğini hatırlıyoruz:
>>> "ş"
'\xc5\x9f'
Aynı şekilde öteki Türkçe harfler de İngilizce harflerden farklı görünecektir:
>>> "ç","ö","ğ","ş","ı"
('\xc3\xa7', '\xc3\xb6', '\xc4\x9f', '\xc5\x9f', '\xc4\xb1')
Aslında sorunun temelinde şu yatıyor: len() fonksiyonu, çoğu kişinin zannettiği gibi, karakter sayısını saymaz. Bu fonksiyonun yaptığı iş, bir karakter dizisindeki bayt sayısını saymaktır. Yani aslında len() fonksiyonu bir karakter dizisinin bir veya birden fazla karakter (veya harf) içerip içermemesiyle ilgilenmez. Onun ilgilendiği, yalnızca bayt sayısıdır. Çünkü bilgisayarlar sadece sayılardan anlar. Karakterler veya harfler özünde bilgisayara hiç bir şey ifade etmez. Python’un varsayılan kod çözücüsü olan ascii yalnızca 1 baytlık verileri doğru olarak gösterebilir. Türkçe harfler ascii kodlamasına göre 1 baytla gösterilemediği için de yukarıdaki gibi bir durum ortaya çıkar. Esasında yukarıdaki çıktıları incelediğimiz zaman bu durumu net olarak görüyoruz. Mesela “ş” harfinin çıktısına bakalım tekrar:
>>> "ş"
'\xc5\x9f'
Buradaki çıktıda “xcf” + “x9f” şeklinde gösterilen toplam 2 bayt var. İşte len() fonksiyonu da karakter içindeki bu bayt sayısına bakıyor. Bayt sayısı 1’den fazla olduğunda ise çuvallıyor!.. Bu problem tabii ki yalnızca Türkçe’ye özgü karakterler için geçerli değil. Mesela “avro” işareti de ascii’nin lanetinden payını alıyor:
>>> "€"
'\xe2\x82\xac'
>>> len("€")
3
Demek ki, “€” işareti tek başına üç bayt içeriyormuş... (“xe2”, “x82” ve “xac”).
Bütün bu anlattıklarımızdan yola çıkarak, özellikle Türkçe yazılmış bir programda, eğer kullanıcıdan birtakım veriler alıyorsak ve program içinde bu verilerin uzunluğunu ölçüyorsak, yukarıdaki duruma dikkat etmemiz gerekir. Çünkü len() fonksiyonunu kullanırken aslında bir karakterin boyunu posunu değil, bayt sayısını ölçüyoruz. Bu duruma bir örnek verelim. Diyelim ki elimizde şöyle bir betik var:
#!/usr/bin/env python
#-*-coding:utf8-*-
liste = [ ]
while True:
soru = raw_input("lütfen bir karakter giriniz: ")
if len(soru) == 1:
liste.append(soru)
print liste
else:
print "lütfen birden fazla karakter girmeyiniz"
Burada, kullanıcıdan tek bir karakter girmesini istiyoruz. Amacımız kullanıcının yanlışlıkla veya bilerek birden fazla karakter girmesini engellemek. Böyle bir şeye, örneğin bir adamasmaca oyunu yazıyorsanız ihtiyacınız olabilir... Yukarıda yazdığımız kodlara göre, eğer kullanıcı sadece İngilizce’de bulunan harfleri (ya da daha doğru bir ifadeyle ascii tablosunda bulunan karakterleri) girerse sorun olmayacaktır. Ama kullanıcı “şçöğüı” gibi harfler girmeye kalkışırsa, bunlar da görünüşte tek karakter olmasına rağmen, programımız if bloğu yerine else bloğunu işletecektir. Böyle bir durumda kullanıcıdan küfür yemeye hazırlıklı olun!...
Şu ana kadar bahsettiğimiz, “ascii’nin lanetinden” kurtulmanın tek yolu, karakter dizilerimizi Python’un varsayılan kod çözücüsünün ellerine ve insafına bırakmamaktır. Bunun yerine, karakter dizilerimizi mutlaka “unicode” olarak kodlamamız gerekir (Bu “unicode” konusu başka bir yazıda ayrıntılı olarak ele alınacak). Türkçe için en uygun unicode kodlaması “utf8” olacaktır. (unicode ve utf8 ile ilgili güzel bir belge için bkz: http://www.cl.cam.ac.uk/~mgk25/unicode.html#unicode)
Hemen şöyle bir örnek yapalım:
>>> a = "ışık"
>>> len(a)
7
Burada Python “ı”, “ş” ve “ı” harflerinin her birini çift saydığı için toplam 7 karakter buldu. Yanlış! Ama biz şimdi Python’a doğru yolu göstereceğiz:
>>> a = "ışık"
>>> a = unicode(a,"utf8")
>>> len(a)
4
Gördüğünüz gibi burada, “a = unicode(a,”utf8”)” satırı yardımıyla a değişkenini “utf8” olarak kodladık. Yani karakter dizimizi ascii’nin elinden kurtardık. Bir de bize daha önce sorun çıkaran “ş”, “ç”, “ğ” gibi harflerin durumuna bakalım:
>>> a = "ş"
>>> a = unicode(a,"utf8")
>>> len(a)
1
>>> b = "ğ"
>>> b = unicode(b,"utf8")
>>> len(b)
1
Yukarıda ascii’nin yanlış olarak “2 karakter uzunluğunda” gösterdiği bu harflerin “utf8” kodlamasıyla doğru olarak gösterildiğini görüyoruz... Bir de şu zavallı “avro” işaretine bakalım:
>>> avro = "€"
>>> len(avro)
3
>>> avro = unicode(avro,"utf8")
>>> len(avro)
1
Gördüğünüz gibi, avromuz artık mutlu!!
Bütün bu söylediklerimizden çıkan sonuca göre, bizim yukarıda verdiğimiz, kullanıcıdan karakter girmesini isteyen betiğimizin içinde bir değişiklik yaparak, betiğimizin Python’un varsayılan çözücüsü yerine, mesela “utf8”i kullanmasını sağlamamız gerekiyor. Yani kodlarımızı şöyle yazmalıyız:
#!/usr/bin/env python
#-*-coding:utf8-*-
liste = [ ]
while True:
soru = raw_input("lütfen bir karakter giriniz: ")
if len(unicode(soru,"utf8")) == 1:
liste.append(soru)
for i in liste:
print i
else:
print "Lütfen sadece tek bir karakter giriniz!"
Dikkat ederseniz, “if len(unicode(soru,”utf8”)) == 1:” gibi bir satır eklemenin yanısıra, listeyi ekrana yazdırırken bir “for” döngüsü kurarak listedeki Türkçe karakter içeren öğelerin ekrana düzgün yazdırılmasını da sağladık.
Böylece “ascii” ve kodlama meselesine ayrıntılı sayılabilecek bir bakış sunmuş olduk. Daha sonra, bu makalede bahsedilen ve bu konuyla yakından ilişkili olan “unicode” konusuna da değineceğiz...