Pertama, fungsinya, bagi mereka yang hanya menginginkan kode salin dan tempel:
def truncate(f, n):
'''Truncates/pads a float f to n decimal places without rounding'''
s = '{}'.format(f)
if 'e' in s or 'E' in s:
return '{0:.{1}f}'.format(f, n)
i, p, d = s.partition('.')
return '.'.join([i, (d+'0'*n)[:n]])
Ini berlaku di Python 2.7 dan 3.1+. Untuk versi yang lebih lama, tidak mungkin untuk mendapatkan efek "pembulatan cerdas" yang sama (setidaknya, bukan tanpa banyak kode yang rumit), tetapi pembulatan ke 12 tempat desimal sebelum pemotongan akan sering dilakukan:
def truncate(f, n):
'''Truncates/pads a float f to n decimal places without rounding'''
s = '%.12f' % f
i, p, d = s.partition('.')
return '.'.join([i, (d+'0'*n)[:n]])
Penjelasan
Inti dari metode yang mendasari adalah mengubah nilai menjadi string dengan presisi penuh dan kemudian memotong semuanya di luar jumlah karakter yang diinginkan. Langkah terakhir ini mudah; itu bisa dilakukan baik dengan manipulasi string
i, p, d = s.partition('.')
'.'.join([i, (d+'0'*n)[:n]])
atau decimal
modul
str(Decimal(s).quantize(Decimal((0, (1,), -n)), rounding=ROUND_DOWN))
Langkah pertama, mengubah ke string, cukup sulit karena ada beberapa pasangan literal floating point (yaitu apa yang Anda tulis di kode sumber) yang keduanya menghasilkan representasi biner yang sama namun harus dipotong secara berbeda. Misalnya, pertimbangkan 0,3 dan 0,29999999999999998. Jika Anda menulis 0.3
dalam program Python, kompilator mengkodekannya menggunakan format titik-mengambang IEEE ke dalam urutan bit (dengan asumsi float 64-bit)
0011111111010011001100110011001100110011001100110011001100110011
Ini adalah nilai terdekat dengan 0,3 yang secara akurat dapat direpresentasikan sebagai float IEEE. Tetapi jika Anda menulis 0.29999999999999998
dalam program Python, kompilator menerjemahkannya menjadi nilai yang persis sama . Dalam satu kasus, Anda bermaksud memangkasnya (menjadi satu digit) sebagai 0.3
, sedangkan dalam kasus lain Anda bermaksud memangkasnya 0.2
, tetapi Python hanya dapat memberikan satu jawaban. Ini adalah batasan mendasar Python, atau memang bahasa pemrograman apa pun tanpa evaluasi malas. Fungsi pemotongan hanya memiliki akses ke nilai biner yang disimpan di memori komputer, bukan string yang sebenarnya Anda ketikkan ke dalam kode sumber. 1
Jika Anda mendekode urutan bit kembali menjadi angka desimal, sekali lagi menggunakan format titik mengambang IEEE 64-bit, Anda mendapatkan
0.2999999999999999888977697537484345957637...
jadi implementasi yang naif akan muncul 0.2
meskipun itu mungkin bukan yang Anda inginkan. Untuk lebih lanjut tentang kesalahan representasi floating-point, lihat tutorial Python .
Sangat jarang bekerja dengan nilai floating-point yang sangat dekat dengan bilangan bulat tetapi sengaja tidak sama dengan bilangan bulat itu. Jadi, saat memotong, mungkin masuk akal untuk memilih representasi desimal "terbaik" dari semua yang dapat sesuai dengan nilai dalam memori. Python 2.7 dan yang lebih baru (tetapi bukan 3.0) menyertakan algoritme canggih untuk melakukan hal itu , yang dapat kita akses melalui operasi pemformatan string default.
'{}'.format(f)
Satu-satunya peringatan adalah bahwa ini bertindak seperti g
spesifikasi format, dalam arti bahwa ia menggunakan notasi eksponensial ( 1.23e+4
) jika angkanya cukup besar atau kecil. Jadi metode ini harus menangkap kasus ini dan menanganinya secara berbeda. Ada beberapa kasus di mana menggunakan f
spesifikasi format malah menyebabkan masalah, seperti mencoba memotong 3e-10
ke presisi 28 digit (menghasilkan 0.0000000002999999999999999980
), dan saya belum yakin cara terbaik untuk menanganinya.
Jika Anda benar - benar bekerja dengan float
s yang sangat dekat dengan bilangan bulat tetapi sengaja tidak sama dengan bilangan tersebut (seperti 0.29999999999999998 atau 99.959999999999994), ini akan menghasilkan beberapa positif palsu, yaitu bilangan bulat yang tidak ingin dibulatkan. Dalam hal ini, solusinya adalah dengan menentukan presisi tetap.
'{0:.{1}f}'.format(f, sys.float_info.dig + n + 2)
Jumlah digit ketepatan yang digunakan di sini tidak terlalu penting, hanya perlu cukup besar untuk memastikan bahwa pembulatan apa pun yang dilakukan dalam konversi string tidak "menaikkan" nilai ke representasi desimalnya yang bagus. Saya pikir sys.float_info.dig + n + 2
mungkin cukup dalam semua kasus, tetapi jika tidak 2
mungkin harus ditingkatkan, dan tidak ada salahnya untuk melakukannya.
Pada versi Python sebelumnya (hingga 2.6, atau 3.0), pemformatan angka floating point jauh lebih kasar, dan secara teratur akan menghasilkan hal-hal seperti
>>> 1.1
1.1000000000000001
Jika ini adalah situasi Anda, jika Anda tidak ingin menggunakan "bagus" representasi desimal untuk pemotongan, semua dapat Anda lakukan (sejauh yang saya tahu) adalah memilih beberapa jumlah digit, kurang dari representable presisi penuh oleh float
, dan putaran nomor sebanyak itu sebelum memotongnya. Pilihan tipikal adalah 12,
'%.12f' % f
tetapi Anda dapat menyesuaikan ini agar sesuai dengan angka yang Anda gunakan.
1 Yah ... aku berbohong. Secara teknis, Anda dapat menginstruksikan Python untuk mengurai ulang kode sumbernya sendiri dan mengekstrak bagian yang sesuai dengan argumen pertama yang Anda berikan ke fungsi pemotongan. Jika argumen tersebut adalah literal floating-point, Anda bisa memotongnya di sejumlah tempat setelah koma desimal dan mengembalikannya. Namun strategi ini tidak berhasil jika argumennya adalah variabel, yang membuatnya tidak berguna. Hal berikut disajikan hanya untuk hiburan:
def trunc_introspect(f, n):
'''Truncates/pads the float f to n decimal places by looking at the caller's source code'''
current_frame = None
caller_frame = None
s = inspect.stack()
try:
current_frame = s[0]
caller_frame = s[1]
gen = tokenize.tokenize(io.BytesIO(caller_frame[4][caller_frame[5]].encode('utf-8')).readline)
for token_type, token_string, _, _, _ in gen:
if token_type == tokenize.NAME and token_string == current_frame[3]:
next(gen) # left parenthesis
token_type, token_string, _, _, _ = next(gen) # float literal
if token_type == tokenize.NUMBER:
try:
cut_point = token_string.index('.') + n + 1
except ValueError: # no decimal in string
return token_string + '.' + '0' * n
else:
if len(token_string) < cut_point:
token_string += '0' * (cut_point - len(token_string))
return token_string[:cut_point]
else:
raise ValueError('Unable to find floating-point literal (this probably means you called {} with a variable)'.format(current_frame[3]))
break
finally:
del s, current_frame, caller_frame
Menggeneralisasi ini untuk menangani kasus di mana Anda meneruskan variabel tampaknya seperti penyebab yang hilang, karena Anda harus menelusuri mundur melalui eksekusi program sampai Anda menemukan literal floating-point yang memberi variabel nilainya. Bahkan jika ada. Sebagian besar variabel akan diinisialisasi dari input pengguna atau ekspresi matematika, dalam hal ini representasi biner adalah segalanya.