Đọc thông báo lỗi của R
Một lỗi trong R có hai phần, và cả hai đều hữu ích:
Error in "10" + 5 : non-numeric argument to binary operator
Sau Error in là lời gọi (call), đoạn code chính xác bị lỗi ("10" + 5). Sau dấu hai chấm là điều kiện (condition), điều gì đã sai (non-numeric argument to binary operator). Hãy đọc lời gọi trước: nó cho biết ở đâu, và rất thường bạn đã thấy ngay vấn đề trong đoạn code được trích. Sau đó đọc điều kiện để biết vì sao.
Hai thói quen phân biệt người gỡ lỗi nhanh với người chật vật. Thứ nhất, hãy thực sự đọc thông báo: thông báo của R thường chính xác, chỉ là viết cụt lủn. Thứ hai, gỡ lỗi đầu tiên, không phải lỗi cuối cùng: một thất bại sớm trong script sẽ kéo theo cả đống lỗi "object not found" phía sau, và tất cả đều biến mất khi bạn sửa lỗi gốc. Phần còn lại của trang này giải mã những thông báo bạn hay gặp nhất, rồi đến các công cụ cho lúc đọc thôi là chưa đủ.
Lỗi về tên: not found
Error: object 'total' not found: R đã tìm trong mọi môi trường nó biết và không có biến nào mang tên đó. Ba nguyên nhân sau bao gần như mọi trường hợp:
- Gõ sai, kể cả chữ hoa chữ thường. R phân biệt chữ hoa chữ thường:
Total,totalvàTOTALlà ba tên khác nhau, và R không đoán xem bạn muốn tên nào. - Dòng định nghĩa chưa được chạy. Bạn đã viết
total <- sum(x)trong script nhưng chưa bao giờ thực thi nó trong phiên này. Chuyện này hay xảy ra sau khi khởi động lại R: tệp script vẫn hiện dòng đó nhưng phiên mới chưa từng thấy nó. Hãy chạy script từ đầu. - Sai môi trường. Biến tạo bên trong một hàm chỉ sống trong lần gọi đó. Dùng nó bên ngoài hàm là đòi một thứ không còn tồn tại; hãy trả về giá trị thay vào đó.
Error: could not find function "read_excel": cùng ý tưởng, nhưng với tên hàm. Chín trên mười lần, hàm đó nằm trong một package bạn đã cài nhưng chưa tải trong phiên này:
library(readxl) # the fix: loading is per-session, installing is per-machine
df <- read_excel("data.xlsx")
Nếu chính library(readxl) báo lỗi thì package chưa được cài: chạy install.packages("readxl") trước. Còn nếu hàm thuộc base R thì bạn đã gõ sai (lenght() là lỗi ai cũng từng mắc).
Lỗi cú pháp: unexpected symbol
Error: unexpected symbol in "..." (cùng các họ hàng unexpected ')', unexpected string constant) nghĩa là R thậm chí không phân tích được code. Thông báo chỉ vào chỗ R nhận ra vấn đề, thường nằm sau chỗ sai thật sự. Các nghi phạm quen thuộc:
mean(x na.rm = TRUE) # missing comma - should be mean(x, na.rm = TRUE)
name <- "Ada # unclosed quote - swallows the following lines
total <- sum(c(1, 2, 3) # unclosed paren - the error fires lines later
Khi dòng bị đánh dấu trông vô tội, lỗi gần như luôn nằm phía trên nó: một dấu nháy, dấu ngoặc tròn hoặc ngoặc nhọn chưa đóng ở đâu đó trước đó trong tệp. Một trình soạn thảo code tô sáng các cặp ngoặc khớp nhau sẽ tìm ra chúng trong vài giây.
Giải mã lỗi về kiểu và chỉ số
non-numeric argument to binary operator: bạn làm phép toán trên một thứ không phải số, thường là một con số đến dưới dạng văn bản (nhập dữ liệu là nguồn kinh điển: một cột chỉ cần một từ lạc loài là bị đọc thành character, như đã nói trong kiểu dữ liệu). Phiên bản bị lỗi:
x <- "10"
x + 5
# Error in x + 5 : non-numeric argument to binary operator
Và cách sửa: chuyển đổi, rồi mới tính:
subscript out of bounds: bạn lấy vị trí n bằng [[ ]] trong một thứ có ít hơn n phần tử:
scores <- list(ada = 92, grace = 88)
scores[[3]]
# Error in scores[[3]] : subscript out of bounds
Kiểm tra length() trước khi truy cập theo chỉ số, hoặc tốt hơn, lấy theo tên (scores[["grace"]]) để việc đổi thứ tự không làm hỏng code. Lưu ý sự bất đối xứng: ngoặc vuông đơn dễ dãi hơn, [ ] vượt phạm vi trên một vector lặng lẽ trả về NA thay vì báo lỗi, tức đổi một bug ồn ào lấy một bug âm thầm.
$ operator is invalid for atomic vectors: $ dành cho list và data frame. Với một vector có tên, hãy dùng ngoặc vuông:
([[ ]] cho giá trị trần; [ ] giữ nguyên tên đi kèm.) Lỗi này thường có nghĩa là một bước phía trước đã trả về vector trong khi bạn tưởng là data frame; hãy kiểm tra lại giả định đó thay vì chỉ đổi toán tử.
argument is of length zero: một câu lệnh if () nhận một điều kiện rỗng, gần như luôn là một NULL lọt vào từ một phần tử list bị thiếu hoặc một hàm không trả về gì:
threshold <- NULL
if (threshold > 5) print("big")
# Error in if (threshold > 5) print("big") : argument is of length zero
Hãy chặn trước phép kiểm tra, và lưu ý rằng && dừng đánh giá ngay khi đã biết kết quả, nên phép so sánh không bao giờ chạy trên NULL:
(Một lỗi liên quan, missing value where TRUE/FALSE needed, là cùng kiểu thất bại nhưng với NA thay vì NULL; cách chặn ở đó là is.na(), được nói trong giá trị thiếu.)
replacement has length zero: phiên bản phép gán của cùng căn bệnh: x[2] <- numeric(0) cố điền một ô bằng không giá trị nào. Thứ tạo ra vế phải đã trả về rỗng; hãy gỡ lỗi chỗ đó, không phải phép gán.
Cảnh báo không phải lỗi, và đó mới là chỗ nguy hiểm
Lỗi làm dừng chương trình; cảnh báo (warning) thì không. R hoàn tất phép tính, đưa bạn kết quả, rồi mới nói ra những điều nó còn e ngại. Kết quả đó đôi khi ổn, đôi khi sai một cách lặng lẽ:
Cả hai dòng đều chạy xong. Dòng đầu lặp lại vector ngắn hơn và cảnh báo longer object length is not a multiple of shorter object length, mà lặp một vector độ dài 2 theo một vector độ dài 3 thì gần như chẳng bao giờ là điều ai đó muốn. Dòng thứ hai cảnh báo NAs introduced by coercion và trả về một vector có lỗ hổng, khiến mọi mean() phía sau đều trả về NA. Hãy xem cả hai cảnh báo là bug cần điều tra, không phải nhiễu để lướt qua. Trong script, bạn có thể áp đặt quan điểm đó bằng options(warn = 2), biến mọi cảnh báo thành lỗi để không gì lọt qua được.
Xử lý thất bại với tryCatch()
Đôi khi lỗi là điều đã lường trước: một tệp hỏng trong thư mục hàng trăm tệp, một hàng dữ liệu xấu, và bạn muốn xử lý rồi đi tiếp thay vì dừng hẳn. tryCatch() bọc một biểu thức có rủi ro bằng các trình xử lý:
Cơ chế: nếu khối chính thành công, giá trị của nó là kết quả. Nếu nó lỗi, trình xử lý error = chạy thay vào đó và giá trị trả về của nó (ở đây là NA) trở thành kết quả, còn script vẫn tiếp tục. conditionMessage(e) lấy lại thông báo gốc để ghi log. finally = chạy trong mọi trường hợp, thành công hay thất bại, nên đó là nơi đặt việc dọn dẹp như đóng kết nối; bạn có thể thấy nó in ra trước mỗi kết quả ở trên.
Cũng có trình xử lý warning =: tryCatch(as.numeric(x), warning = function(w) NA) bắt cảnh báo ép kiểu ở phần trước thay vì để nó trôi qua. Một lời nhắc: trình xử lý trả về giá trị dự phòng mà không ghi log gì cả là cách che giấu thất bại, không phải xử lý nó. Luôn ghi lại conditionMessage(); bạn của tương lai sẽ cần đến nó.
Tìm chỗ thất bại: traceback(), browser() và in ra thật thà
Khi lỗi đến từ sâu bên trong các lời gọi hàm lồng nhau, chỉ thông báo thôi không cho biết chuỗi lời gọi nào dẫn bạn tới đó. Chạy traceback() ngay sau khi lỗi xảy ra:
f <- function(x) g(x)
g <- function(x) stop("boom")
f(1)
# Error in g(x) : boom
traceback()
# 2: g(x)
# 1: f(1)
Nó in ra ngăn xếp lời gọi (call stack) tại thời điểm thất bại: lời gọi của bạn ở một đầu, lời gọi bị lỗi ở đầu kia. Nó phải là lệnh tiếp theo bạn chạy; ngăn xếp bị bỏ đi khi một lỗi khác xảy ra.
Để xem trực tiếp, browser() tạm dừng chương trình ở bất cứ đâu bạn đặt nó và đưa bạn vào một dấu nhắc tương tác bên trong hàm: xem các biến, chạy từng bước với n, tiếp tục với c, thoát với Q. debug(f) làm điều tương tự mà không cần sửa code: nó đánh dấu f để lần gọi tiếp theo mở trong browser (hủy bằng undebug(f)).
Và còn kỹ thuật mà chẳng ai đưa lên slide hội thảo nhưng ai cũng dùng: in ra. Rải print() hoặc cat() ở các điểm kiểm tra, chạy, và xem thực tế bắt đầu khác kỳ vọng từ chỗ nào. Cách này chính đáng, nhanh, và trong script thường là công cụ thực tế nhất. Người bạn thân nhất của nó là str(), thứ trả lời câu hỏi đằng sau có lẽ một nửa số lỗi trong R: "đối tượng này thực sự là gì?":
Một bản tóm tắt gọn: đây là một list, hai phần tử, một là vector integer, một là data frame với các cột và kiểu này. Khi $ thất bại hay phép toán chạy sai, hãy str() đối tượng trước khi đưa ra giả thuyết; câu trả lời thường nằm ngay đó ("...à, nó là một list độ dài 1 chứa data frame của mình").
Những điều cần nhớ
- Đọc thông báo: phần sau
Error incho biết ở đâu, phần sau dấu hai chấm cho biết vì sao. Sửa lỗi đầu tiên, không phải lỗi ồn ào nhất. object not found= gõ sai, code chưa chạy, hoặc biến chỉ tồn tại bên trong một hàm.could not find function= thiếu lời gọilibrary(), gần như luôn là vậy.unexpected symbolnghĩa là ngữ pháp không phân tích được, và lỗi thật thường là một dấu nháy hoặc dấu ngoặc chưa đóng ở trước chỗ bị đánh dấu.- Các lỗi kinh điển về kiểu và chỉ số,
non-numeric argument,subscript out of bounds,$ on atomic vectors,argument is of length zero, đều chỉ ra một giả định sai về bản chất của đối tượng;str()kiểm tra giả định đó trong một lần gọi. - Cảnh báo không làm dừng chương trình, và đó chính là lý do chúng đáng được chú ý: kết quả có thể sai một cách lặng lẽ.
tryCatch(error =, warning =, finally =)xử lý các thất bại đã lường trước mà không dừng chương trình (luôn ghi logconditionMessage());traceback()ngay sau lỗi cho thấy chuỗi lời gọi;browser()/debug()tạm dừng bên trong; gỡ lỗi bằngprint()là cách làm chính đáng.
Tiếp theo: nhiều "lỗi" hoàn toàn không phải lỗi mà đều bắt nguồn từ một giá trị chỉ vài ký tự, NA, và cách giá trị thiếu đi qua mọi thứ R tính toán.
Câu hỏi thường gặp
"object 'x' not found" trong R nghĩa là gì?
R đã tìm một biến tên x và không có tên nào như vậy trong mọi môi trường (environment) mà nó tìm. Nguyên nhân, xếp theo khả năng: gõ sai tên (R phân biệt chữ hoa chữ thường, Total không phải total), dòng tạo ra x chưa được chạy trong phiên này, hoặc x được tạo bên trong một hàm còn bạn lại dùng nó bên ngoài.
"could not find function" trong R nghĩa là gì?
Hàm đó nằm trong một package mà bạn chưa tải trong phiên này. Cài package chỉ cần một lần trên mỗi máy; library() cần một lần trong mỗi phiên, và quên gọi library() là nguyên nhân thường gặp. Nếu chính library() báo lỗi thì package chưa được cài. Gõ sai tên hàm cũng gây ra lỗi này.
Làm sao để xử lý lỗi trong R bằng tryCatch?
Bọc biểu thức có rủi ro: tryCatch(expr, error = function(e) fallback, warning = function(w) fallback, finally = cleanup). Nếu expr thất bại, trình xử lý tương ứng sẽ chạy thay vì script bị dừng, và giá trị trình xử lý trả về trở thành kết quả. conditionMessage(e) bên trong trình xử lý cho bạn thông báo gốc để ghi log.
traceback() trong R dùng để làm gì?
Chạy ngay sau một lỗi, traceback() in ra chuỗi lời gọi hàm đang hoạt động lúc lỗi xảy ra: lời gọi của bạn ở một đầu, lời gọi bị lỗi ở đầu kia. Nó không sửa được gì; nó cho bạn biết nên nhìn vào đâu, và đó là phần lớn công việc khi lỗi đến từ sâu bên trong các hàm lồng nhau.