Skip to content

Latest commit

 

History

History
313 lines (245 loc) · 22.9 KB

7.-input-and-output.md

File metadata and controls

313 lines (245 loc) · 22.9 KB

7. Vào ra dữ liệu

Như chúng ta đã biết, có một vài cách để thể hiện đầu ra của một chương trình: in dữ liệu ra màn hình dưới dạng con người có thể đọc được, hoặc ghi dữ liệu ra file để cho những mục đích sử dụng sau này. Trong chương này, sẽ để cập đến một vài cách xuất dữ liệu đầu ra.

7.1. Định dạng dữ liệu đầu ra thường dùng

Hiện nay, có 2 cách để ghi giá trị: các câu lệnh biểu diễn và hàm print(). (Cách thứ 3 là sử dụng phương thức write() của đối tượng file; chuẩn file đầu ra có thể được tham chiếu tại sys.stdout. Xem trong thư viện để biết thêm thông tin về sys.stdout). Thông thường, các lập trình viên muốn tùy chỉnh hiển thị dữ liệu theo mục đích của mình chứ không đơn giản là in ra giá trị với dấu cách. Vì vậy, Python cung cấp 2 cách để định dạng dữ liệu đầu ra: Cách 1, người lập trình tự xử lý chuỗi dùng các thao tác cắt chuỗi và nối chuỗi. Với kiểu chuỗi(string), python cung cấp phương thức hữu hiệu để căn chỉnh chuỗi dựa theo chiều rộng cột; chúng ta sẽ thảo luận ở phần sau về vấn đề này. Cách 2, sử dụng Định dạng chuỗi ký tự, hoặc hàm str.format().

Mô-đun string bao gồm một lớp mẫu (Template class) thường có cách khác để chuyển đổi các giá trị thành chuỗi.

Một câu hỏi được đặt ra là: làm sao để chuyển từ giá trị thành chuỗi? Câu trả lời là, python đã cung cấp một số cách để chuyển từ bất kỳ giá trị nào về một chuỗi, sử dụng 2 hàm repr() or str().

Hàm str() trả về giá trị con người có thể đọc được, trong khi đó hàm repr() trả về các giá trị đọc bởi trình thông dịch (hoặc sẽ đẩy ra một lỗi (SyntaxError) nếu không có cú pháp tương đương). Với các đối tượng(object), hàm str() sẽ trả về cũng một giá trị như hàm repr(). Với các giá trị, như số đếm hoặc dữ liệu cấu trúc(danh sách hoặc từ điển), giá trị trả về của 2 hàm str()repr() là như nhau. Đặc biệt các chuỗi, có 2 cách thể hiện riêng biệt.

Một vài ví dụ:>>>

>>> s = 'Hello, world.'
>>> str(s)
'Hello, world.'
>>> repr(s)
"'Hello, world.'"
>>> str(1/7)
'0.14285714285714285'
>>> x = 10 * 3.25
>>> y = 200 * 200
>>> s = 'The value of x is ' + repr(x) + ', and y is ' + repr(y) + '...'
>>> print(s)
The value of x is 32.5, and y is 40000...
>>> # The repr() of a string adds string quotes and backslashes:
... hello = 'hello, world\n'
>>> hellos = repr(hello)
>>> print(hellos)
'hello, world\n'
>>> # The argument to repr() may be any Python object:
... repr((x, y, ('spam', 'eggs')))
"(32.5, 40000, ('spam', 'eggs'))"

Có 2 cách để in dữ liệu dạng bảng:>>>

>>> for x in range(1, 11):
...     print(repr(x).rjust(2), repr(x*x).rjust(3), end=' ')
...     # Note use of 'end' on previous line
...     print(repr(x*x*x).rjust(4))
...
 1   1    1
 2   4    8
 3   9   27
 4  16   64
 5  25  125
 6  36  216
 7  49  343
 8  64  512
 9  81  729
10 100 1000

>>> for x in range(1, 11):
...     print('{0:2d} {1:3d} {2:4d}'.format(x, x*x, x*x*x))
...
 1   1    1
 2   4    8
 3   9   27
 4  16   64
 5  25  125
 6  36  216
 7  49  343
 8  64  512
 9  81  729
10 100 1000

(Lưu ý, trong ví dụ đầu tiên, nhận thấy giữa các cột phân cách nhau bởi 1 khoảng trắng, đó là do hàm print(). Vì trong python, khi sử dụng hàm print(), mặc định thêm 1 khoảng trắng giữa các tham số.)

Ở ví dụ 1, hàm str.rjust() căn phải 1 chuỗi theo độ rộng được cho, thêm khoảng trắng ở phía bên trái nếu cần thiết. Các hàm tương tự là: str.ljust() and str.center(). Nhấn mạnh rằng, những hàm ở trên chỉ đơn giản là trả về 1 chuỗi mới với một số tùy chỉnh được cung cấp bởi các hàm đó(ví dụ: căn trái, căn phải hoặc căn giữa). Trong trường hợp chuỗi đầu vào quá dài, các hàm này không cắt chuỗi mà giữ nguyên chuỗi đó; như đã biết, việc giữ nguyên chuỗi quá dài khi in ra sẽ làm xáo trộn bố trí cột nhưng nó thường tốt hơn việc thay đổi chuỗi.(Nếu thực sự muốn cắt chuỗi, có thể dùng các thao tác cắt chuỗi, ví dụ: x.ljust(n)[:n].)

Có 1 cách khác, dùng hàm str.zfill(), đệm một chuỗi số 0 về phía bên trái.

>>> '12'.zfill(5)
'00012'
>>> '-3.14'.zfill(7)
'-003.14'
>>> '3.14159265359'.zfill(5)
'3.14159265359'

Về cơ bản, cách sử dụng của hàm str.format() như sau:>>>

>>> print('We are the {} who say "{}!"'.format('knights', 'Ni'))
We are the knights who say "Ni!"

Dầu ngặc {} và ký tự ở bên trong dấu ngoặc được thay thế bằng tham số được truyền vào trong hàm str.format(). Phần số ở trong dấu {} được sử dụng để chỉ ra vị trí in ra của tham số được truyền vào trong hàm str.format().>>>

>>> print('{0} and {1}'.format('spam', 'eggs'))
spam and eggs
>>> print('{1} and {0}'.format('spam', 'eggs'))
eggs and spam

Nếu tên tham số được sử dụng trong hàm str.format(), giá trị của nó được in ra tại vị trí tương ứng với tên tham số đó.>>>

>>> print('This {food} is {adjective}.'.format(
...       food='spam', adjective='absolutely horrible'))
This spam is absolutely horrible.

Ví trí và tên tham số có thể được kết hợp tùy ý:>>>

>>> print('The story of {0}, {1}, and {other}.'.format('Bill', 'Manfred',
                                                       other='Georg'))
The story of Bill, Manfred, and Georg.

'!a' (áp dụng hàm ascii()), '!s' (áp dụng hàm str()) và '!r' (áp dụng hàm repr()) để chuyển đổi giá trị trước khi in:>>>

>>> contents = 'eels'
>>> print('My hovercraft is full of {}.'.format(contents))
My hovercraft is full of eels.
>>> print('My hovercraft is full of {!r}.'.format(contents))
My hovercraft is full of 'eels'.

Có thể dùng ':' và thông số định dạng(format specifier) ở trong {} để tùy chỉnh định dạng dữ liệu. Ví dụ bên dưới, làm tròn số Pi, lấy 3 chữ số sau dấu phẩy.>>>

>>> import math
>>> print('The value of PI is approximately {0:.3f}.'.format(math.pi))
The value of PI is approximately 3.142.

Nếu thêm số nguyên sau dấu ':' , định ra số ký tự tối thiếu được in ra. việc này rất có ý nghĩa khi in ra bảng(table).>>

>>> table = {'Sjoerd': 4127, 'Jack': 4098, 'Dcab': 7678}
>>> for name, phone in table.items():
...     print('{0:10} ==> {1:10d}'.format(name, phone))
...
Jack       ==>       4098
Dcab       ==>       7678
Sjoerd     ==>       4127

Nếu có 1 chuỗi rất dài, mà không muốn chia chuỗi, có thể in các biến theo tên thay vì vị trí. Để làm điều đó, đưa vào 1 từ điển và sử dụng dấu ngoặc '[]' để truy cập các giá trị thông qua các khóa (key) của từ điển>>>

>>> table = {'Sjoerd': 4127, 'Jack': 4098, 'Dcab': 8637678}
>>> print('Jack: {0[Jack]:d}; Sjoerd: {0[Sjoerd]:d}; '
...       'Dcab: {0[Dcab]:d}'.format(table))
Jack: 4098; Sjoerd: 4127; Dcab: 8637678

Có thể đưa biến table ở trên là một tham số và thêm 2 dấu ‘**’ ở đầu.>>>

>>> table = {'Sjoerd': 4127, 'Jack': 4098, 'Dcab': 8637678}
>>> print('Jack: {Jack:d}; Sjoerd: {Sjoerd:d}; Dcab: {Dcab:d}'.format(**table))
Jack: 4098; Sjoerd: 4127; Dcab: 8637678

Điều này đặc biệt có ích trong việc kết hợp với hàm vars(), hàm này trả về một từ điển bao gồm tất cả các biến địa phương(local variables)

Để có một cái nhìn toàn diện về định dạng chuỗi với str.format(), hãy xem chi tiết tại Format String Syntax.

7.1.1. Định dạng chuỗi truyền thống

Toán tử % cũng được sử dụng trong định dạng chuỗi. Nó diễn giải đối số bên trái giống như hàm sprintf()(hàm sprintf(): là một kiểu định dạng chuỗi được áp dụng cho đối số phải, và trả về chuỗi kết quả từ toán tử định dạng này). Ví dụ:>>>

>>> import math
>>> print('The value of PI is approximately %5.3f.' % math.pi)
The value of PI is approximately 3.142.

Để biết thêm thông tin, hãy xem ở printf-style String Formatting

7.2. Đọc và ghi tệp (file)

Hàm open() trả về một đối tượng file(file object, thông thường hàm này có 2 tham số: open(filename, mode)).>>>

>>> f = open('workfile', 'w')

Tham số đầu tiên của hàm open() là một chuỗi bao gồm tên file. Tham số thứ 2 là một chuỗi bao gồm một vài ký tự để miêu tả cách sử dụng của file đó, gọi là mode. Nếu mode'r', file đó chỉ được đọc. Nếu mode'w', file đó chỉ được ghi (Nếu tồn tại 1 file có cùng tên, file cùng tên đó sẽ bị xóa đi khi sử dụng mode'w'). Nếu mode'a' mở file để ghi thêm vào, trong trường hợp này, bất kỳ dữ liệu nào được thêm vào sẽ được tự động ghi ở cuối file. Nếu mode'r+', mở file để vừa ghi vừa đọc. Tham số mode là tùy chọn, trong trường hợp không khai báo mode, mặc định là 'r'.

Thông thường, tệp được mở ở chế độ văn bản (text mode), có nghĩa là, đọc là ghi chuỗi từ 1 file sang 1 file khác, chuỗi đó được mã hóa bằng một dạng mã hóa cụ thể. Nếu mã hóa không được chỉ định, kiểu mã hóa mặc định cho chuỗi trên phụ thuộc vào nền tảng (xem hàm open()). Sử dụng mode'b' khi mở file binary, trong trường hợp này, dữ liệu được đọc ghi dưới dạng đối tượng byte. Mode này được sử dụng khi tất cả các file không bao gồm ký tự văn bản (text).

Ở chế độ văn bản (text mode), khi đọc file, \n mặc định được chuyển đổi phụ thuộc vào nền tảng cụ thể (\n cho Unix, \r\n cho Windows). Khi ghi file, \n mặc định được chuyển đổi trở lại phụ thuộc vào nền tảng. Việc chuyển đổi ngầm này rất có lợi đối với file văn bản, nhưng nó sẽ làm hỏng dữ liệu nhị phân giống như file JPEG hoặc file EXE. Vì vậy, hãy cẩn thận khi sử dụng chế độ nhị phân (binary mode) khi đọc và ghi file.

Sử dụng từ khóa with với đối tượng file. Ưu điểm là đóng file ngay sau khi kết thúc phiên làm việc, thậm chí các lỗi ngoại lệ (exception) có thể được xuất ra khi sử dụng with. Thêm vào đó, sử dụng with cũng ngắn gọn hơn với cách viết try-finally:>>>

>>> with open('workfile') as f:
...     read_data = f.read()
>>> f.closed
True

Nếu không sử dụng từ khóa with, phải gọi hàm f.close() để đóng file và giải phóng tài nguyên hệ thống đã được sử dụng. Nếu không đóng file, file này vẫn để mở cho tới khi trình thu gom rác (garbage collector) của Python hủy đối tượng và đóng file. Một rủi ro khác là, thời điểm dọn dẹp của trình thu gom rác phụ thuộc vào các phiên bản python khác nhau.

Lưu ý, một khi đối tượng file được đóng, bằng câu lệnh with hoặc đã gọi hàm f.close(), chúng ta sẽ không thể thao tác trên đối tượng file đó nữa.>>>

>>> f.close()
>>> f.read()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
ValueError: I/O operation on closed file

7.2.1. Các phương thức sử dụng cho đối tượng file

Các ví dụ còn lại trong phần này giả định rằng một đối tượng file gọi là f đã được tạo ra.

Để đọc nội dung 1 file, gọi hàm f.read(size), hàm này đọc một lượng dữ liệu chỉ ra ở biến size và trả về 1 chuỗi (ở chế độ text) hoặc đối tượng byte (ở chế độ nhị phân). size là một tham số kiểu số và tùy chọn. Khi size được bỏ qua hoặc là số âm, toàn bộ nội dung file sẽ được đọc và được trả về trong 1 chuỗi; Sẽ là vấn đề nếu file lớn gấp đôi dung lượng bộ nhớ của máy. Trong trường hợp này, hàm f.read(size) đọc lượng dữ liệu tối đa có thể đọc và trả về 1 chuỗi. Khi đọc tới cuối file, hàm f.read() trả về một chuỗi rỗng ('').>>>

>>> f.read()
'This is the entire file.\n'
>>> f.read()
''

Hàm f.readline() đọc từng dòng trong file; Ký tự xuống dòng (\n) được thêm vào cuối chuỗi, và chỉ được bỏ qua khi đọc dòng cuối cùng của file nếu file đó không kết thúc bằng ký tự xuống dòng. Điều này làm cho giá trị trả về không rõ ràng; nếu hàm f.readline() trả về một chuỗi rỗng, mà đọc tới dòng cuối file, trong khi dòng trống thể hiện bằng '\n', thì một chuỗi bao gồm chỉ một ký tự xuống dòng.>>>

>>> f.readline()
'This is the first line of the file.\n'
>>> f.readline()
'Second line of the file\n'
>>> f.readline()
''

Có thể dùng vòng lặp trên đối tượng file. Đây là một cách đơn giản và hiệu quả để đọc từng dòng của file.>>>

>>> for line in f:
...     print(line, end='')
...
This is the first line of the file.
Second line of the file

Nếu muốn đọc tất cả các dòng của file và đưa vào 1 list, có thể sử dụng list(f) hoặc f.readlines(). Hàm f.write(string) ghi nội dung của chuỗi (string) vào file, trả về số lượng ký tự được ghi.>>>

>>> f.write('This is a test\n')
15

Những kiểu đối tượng khác cần phải được chuyển đổi - hoặc là sang 1 chuỗi (string) (trong chế độ văn bản - text mode) hoặc là đối tượng byte (trong chế độ nhị phân - binar) - trước khi ghi ra file:>>>

>>> value = ('the answer', 42)
>>> s = str(value)  # convert the tuple to string
>>> f.write(s)
18

Hàm f.tell() trả về một số nguyên, số nguyên này định vị trí hiện tại của đối tượng file trong file hiển thị số lượng byte từ đầu file khi ở chế độ nhị phân (binary mode) và một số opaque number ở chế độ văn bản (text mode) Để thay đổi vị trí của đối tượng file, sử dụng hàm f.seek(offset, from_what). Vị trí được tính toán dựa trên tham số offset; điểm tham chiếu được lựa chọn dựa vào tham số from_what. Nếu from_what là 0, vị trí tham chiếu từ đầu file, from_what là 1, ví trí tham chiếu là vị trí file hiện tại. from_what là 2, vị trí tham chiếu là vị trí cuối của tệp.>>>

>>> f = open('workfile', 'rb+')
>>> f.write(b'0123456789abcdef')
16
>>> f.seek(5)      # Go to the 6th byte in the file
5
>>> f.read(1)
b'5'
>>> f.seek(-3, 2)  # Go to the 3rd byte before the end
13
>>> f.read(1)
b'd'

In text files (those opened without a b in the mode string), only seeks relative to the beginning of the file are allowed (the exception being seeking to the very file end with seek(0, 2)) and the only valid offset values are those returned from the f.tell(), or zero. Any other offset value produces undefined behaviour.

Đối tượng file (file object) có một thêm một số phương thức như là hàm isatty()truncate(), tuy nhiên chúng ít khi được sử dụng, bạn có thể tham khảo trong thư viện để xem hướng dẫn đầy đủ về đối tượng file.

7.2.2. Lưu trữ dữ liệu có cấu trúc với json

Các chuỗi có thể dễ dàng được đọc hoặc ghi vào 1 file. Đối với dữ liệu số, cần phải mất công thêm một chút, bởi vì hàm read() chỉ trả về chuỗi, vì vậy, cần phải sử dụng hàm int() để chuyển đổi 1 chuỗi như '123' thành 1 số với giá trị bằng 123. Tuy nhiên, khi phải lưu trữ các kiểu dữ liệu phức tạp như danh sách lồng nhau (nested lists) và từ điển (dictionaries), việc phân tích cú pháp và tuần tự hóa bằng tay rất phức tạp.

Thay vì yêu cầu người dùng viết code và debugging để ghi những kiểu dữ liệu phức tạp vào file, Python cho phép sử dụng định dạng trao đổi dữ liệu phổ biến hiện nay gọi là JSON (JavaScript Object Notation). Định dạng [json] có thể lưu trữ dữ liệu phân cấp (data hierarchies) của python, và chuyển các dữ liệu về dạng chuỗi; quá trình này được gọi là tuần tự hóa (serializing). Quá trình ngược lại, tái tạo dữ liệu từ biểu diễn chuỗi được gọi là giải tuần tự hóa ( deserializing). Giữa tuần tự hóa và giải tuần tự hóa, chuỗi đại diện cho đối tượng được lưu trữ trong 1 file hoặc một dữ liệu, hoặc được truyền qua kết nối mạng tới một số máy ở xa.

Lưu ý:

Định dạng JSON thường được các ứng dụng hiện đại sử dụng cho mục đích trao đổi dữ liệu. Nhiều lập trình viên đã quen thuộc với nó, nên JSON là một lựa chọn tốt cho việc tương tác.

Nếu có 1 đối tượng x, có thể xem biểu biểu diễn chuỗi JSON của đối tượng x chỉ với 1 dòng code đơn giản như sau:>>>

>>> import json
>>> json.dumps([1, 'simple', 'list'])
'[1, "simple", "list"]'

Một biến thể khác của hàm dumps(), là dump(), hàm này tuần tự hóa đối tượng thành một file văn bản (text file). Vì vậy, nếu f là một đối tượng file văn bản (text file object), được mở để ghi dữ liệu, ta có thể làm như sau:

json.dump(x, f)

Để giải mã đối tượng một lần nữa, nếu f là một đối tượng file văn bản (text file object), được mở ra để đọc, ta có thể làm như sau:

x = json.load(f)

Kỹ thuật tuần tự hóa đơn giản này có thể xử lý cho các kiểu dữ liệu danh sách (list) và từ điển (dict), nhưng tuần tự hóa đối tượng của 1 lớp cần thêm một số xử lý khác. Bạn có thể tham khảo trong json về vấn đề này.

Xem thêm: pickle - the pickle module

Không như JSON, pickle là một giao thức cho phép tuần tự hóa những đối tượng phức tạp. Vì vậy, nó là đặc trưng của python và không thể giao tiếp với các ứng dụng được viết bằng các ngôn ngữ lập trình khác. Mặc định, nó cũng không an toàn: giải tuần tự hóa (deserializing) dữ liệu từ 1 nguồn không đáng tin có thể thực hiện mã tùy ý nếu dữ liệu được tạo bởi kẻ tấn công.