Đầu tiên, chức năng dành cho những người chỉ muốn sao chép và dán mã:
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]])
Điều này hợp lệ trong Python 2.7 và 3.1+. Đối với các phiên bản cũ hơn, không thể có được hiệu ứng "làm tròn thông minh" tương tự (ít nhất, không phải là không có nhiều mã phức tạp), nhưng việc làm tròn đến 12 chữ số thập phân trước khi cắt ngắn sẽ hiệu quả trong phần lớn thời gian:
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]])
Giải trình
Cốt lõi của phương pháp cơ bản là chuyển đổi giá trị thành một chuỗi với độ chính xác đầy đủ và sau đó chỉ cần cắt mọi thứ vượt quá số ký tự mong muốn. Bước sau rất dễ dàng; nó có thể được thực hiện với thao tác chuỗi
i, p, d = s.partition('.')
'.'.join([i, (d+'0'*n)[:n]])
hoặc decimal
mô-đun
str(Decimal(s).quantize(Decimal((0, (1,), -n)), rounding=ROUND_DOWN))
Bước đầu tiên, chuyển đổi thành một chuỗi, khá khó khăn vì có một số cặp ký tự dấu phẩy động (tức là những gì bạn viết trong mã nguồn), cả hai đều tạo ra cùng một biểu diễn nhị phân nhưng nên được cắt ngắn khác nhau. Ví dụ: hãy xem xét 0,3 và 0,29999999999999998. Nếu bạn viết 0.3
bằng chương trình Python, trình biên dịch sẽ mã hóa nó bằng cách sử dụng định dạng dấu phẩy động IEEE thành chuỗi các bit (giả sử là một số float 64 bit)
0011111111010011001100110011001100110011001100110011001100110011
Đây là giá trị gần nhất với 0,3 có thể được biểu diễn chính xác dưới dạng một IEEE float. Nhưng nếu bạn viết 0.29999999999999998
bằng chương trình Python, trình biên dịch sẽ dịch nó thành chính xác cùng một giá trị . Trong một trường hợp, bạn có nghĩa là nó được cắt ngắn (thành một chữ số) 0.3
, trong khi trong trường hợp khác, bạn muốn nó được cắt ngắn thành 0.2
, nhưng Python chỉ có thể đưa ra một câu trả lời. Đây là một hạn chế cơ bản của Python, hoặc thực sự là bất kỳ ngôn ngữ lập trình nào mà không đánh giá lười biếng. Hàm cắt ngắn chỉ có quyền truy cập vào giá trị nhị phân được lưu trữ trong bộ nhớ của máy tính, không phải chuỗi bạn thực sự nhập vào mã nguồn. 1
Nếu bạn giải mã chuỗi bit trở lại thành số thập phân, một lần nữa bằng cách sử dụng định dạng dấu phẩy động 64 bit IEEE, bạn sẽ nhận được
0.2999999999999999888977697537484345957637...
vì vậy một triển khai ngây thơ sẽ xuất hiện 0.2
mặc dù đó có thể không phải là điều bạn muốn. Để biết thêm về lỗi biểu diễn dấu phẩy động, hãy xem hướng dẫn Python .
Rất hiếm khi làm việc với một giá trị dấu phẩy động gần với một số tròn nhưng lại cố tình không bằng số tròn đó. Vì vậy, khi cắt ngắn, có lẽ hợp lý khi chọn biểu diễn thập phân "đẹp nhất" trong số tất cả những gì có thể tương ứng với giá trị trong bộ nhớ. Python 2.7 trở lên (nhưng không phải 3.0) bao gồm một thuật toán phức tạp để thực hiện điều đó , mà chúng ta có thể truy cập thông qua thao tác định dạng chuỗi mặc định.
'{}'.format(f)
Lưu ý duy nhất là điều này hoạt động giống như một g
đặc tả định dạng, theo nghĩa là nó sử dụng ký hiệu hàm mũ ( 1.23e+4
) nếu số lượng đủ lớn hoặc nhỏ. Vì vậy, phương pháp phải bắt trường hợp này và xử lý nó khác. Có một vài trường hợp mà việc sử dụng f
đặc tả định dạng gây ra sự cố, chẳng hạn như cố gắng cắt bớt 3e-10
độ chính xác còn 28 chữ số (nó tạo ra 0.0000000002999999999999999980
) và tôi chưa chắc cách tốt nhất để xử lý chúng.
Nếu bạn thực sự đang làm việc với float
các s rất gần với số làm tròn nhưng cố ý không bằng chúng (như 0,29999999999999998 hoặc 99,959999999999994), điều này sẽ tạo ra một số dương tính giả, tức là nó sẽ làm tròn số mà bạn không muốn làm tròn. Trong trường hợp đó, giải pháp là chỉ định độ chính xác cố định.
'{0:.{1}f}'.format(f, sys.float_info.dig + n + 2)
Số lượng chữ số chính xác để sử dụng ở đây không thực sự quan trọng, nó chỉ cần đủ lớn để đảm bảo rằng bất kỳ phép làm tròn nào được thực hiện trong chuyển đổi chuỗi không làm "tăng" giá trị lên biểu diễn thập phân đẹp mắt của nó. Tôi nghĩ sys.float_info.dig + n + 2
có thể là đủ trong mọi trường hợp, nhưng nếu không thì 2
có thể phải tăng lên, và làm như vậy cũng không hại gì.
Trong các phiên bản trước của Python (lên đến 2.6 hoặc 3.0), định dạng số dấu phẩy động thô hơn rất nhiều và sẽ thường xuyên tạo ra những thứ như
>>> 1.1
1.1000000000000001
Nếu đây là tình hình của bạn, nếu bạn làm muốn sử dụng "thoải mái" cơ quan đại diện thập phân cho cắt ngắn, tất cả các bạn có thể làm (như xa như tôi biết) là chọn một số số chữ số, thấp hơn biểu diễn chính xác đầy đủ bởi một float
và vòng quanh đánh số đến nhiều chữ số đó trước khi cắt bớt. Một lựa chọn điển hình là 12,
'%.12f' % f
nhưng bạn có thể điều chỉnh điều này cho phù hợp với các số bạn đang sử dụng.
1 Chà ... tôi đã nói dối. Về mặt kỹ thuật, bạn có thể hướng dẫn Python phân tích lại mã nguồn của chính nó và trích xuất phần tương ứng với đối số đầu tiên mà bạn chuyển cho hàm cắt ngắn. Nếu đối số đó là một ký tự dấu phẩy động, bạn chỉ có thể cắt nó đi một số vị trí nhất định sau dấu thập phân và trả về. Tuy nhiên, chiến lược này không hoạt động nếu đối số là một biến, điều này khiến nó khá vô dụng. Những điều sau đây chỉ được trình bày cho giá trị giải trí:
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
Tổng quát hóa điều này để xử lý trường hợp bạn truyền vào một biến có vẻ như là một nguyên nhân bị mất, vì bạn phải truy ngược lại quá trình thực thi của chương trình cho đến khi bạn tìm thấy ký tự dấu phẩy động đã cho biến giá trị của nó. Nếu thậm chí có một. Hầu hết các biến sẽ được khởi tạo từ đầu vào của người dùng hoặc các biểu thức toán học, trong trường hợp đó, biểu diễn nhị phân là tất cả.