數聚點

數聚點

Share this post

數聚點
數聚點
約維安計畫:R 語言的流程控制
中級約維安

約維安計畫:R 語言的流程控制

二十八週

Yao-Jen Kuo's avatar
Yao-Jen Kuo
Apr 26, 2022
∙ Paid
1

Share this post

數聚點
數聚點
約維安計畫:R 語言的流程控制
2
Share
來源:giphy.com

什麼是流程控制

多數程式語言都會從程式碼的第一列開始按照列(Row-wise)的順序往下讀取並且執行,但是在某些情況下,我們會希望能在某些程式碼可以依照需求不執行、執行或者執行多次,這時就可以透過流程控制的機制來滿足這些情況。

流程控制指的是在程式執行時,能夠依賴條件來決定程式區塊(Code blocks)中的指令、敘述或者函數呼叫的執行次數,在條件為邏輯向量 FALSE 的時候,程式區塊中的程式碼執行次數為 0(不執行);在條件為邏輯向量 TRUE 的時候,程式區塊中的程式碼執行次數大於等於 1。具體來說,流程控制是一種機制,這個機制能讓一段指令在某些情況下不被執行、在某些情況下被執行一次或者在某些情況下被重複執行,R 語言實現流程控制有三種方式:

  1. 條件判斷。

  2. 迴圈。

  3. 例外處理。

其中,條件判斷能夠使用 if-else if-else 敘述與邏輯向量完成;迴圈則有兩種選擇,一是使用 while 迴圈與邏輯向量,二是使用 for 迴圈與資料結構;例外處理能夠使用 tryCatch() 函數完成。

什麼是區塊

區塊(Code blocks)指的是將一系列相關的敘述集合在一起的程式碼結構,在 R 語言中,區塊使用一對大括號(A curly-brace pair)將敘述包裝在裡面形成區塊。在前述三種類型的流程控制中,都會利用區塊將敘述依附給條件判斷、迴圈與例外處理,如此一來就能夠在某些情況下讓區塊中的程式不被執行、執行僅一次或執行特定次數。

function_name <- function() {
  # code block attached to a function
}
if (CONDITION) {
  # code block attached to an if statement
}
while (CONDITION) {
  # code block attached to a while loop
}
for (element in data_structure) {
  # code block attached to a for loop
}

條件判斷

條件判斷是依邏輯向量為假 FALSE 或真 TRUE 時,來決定是否執行一段位於某區塊內的程式,透過 if-else if-else 敘述可以根據指定條件是否成立,決定後續要執行的程式,也可以組合多個 if-else if-else 敘述進行更複雜的條件判斷。

我們使用保留字 if 搭配一個條件形成邏輯向量,就能夠建立一個 if 敘述,if 敘述能夠讓區塊的執行與否依賴條件的邏輯向量,如果條件為 TRUE 則執行依附大括號內的程式區塊;若條件為 FALSE 則不執行依附大括號內的程式區塊。

if (CONDITION) {
  # code block attached to an if statement
}

CONDITION 可以透過關係運算符或者邏輯運算符生成,例如判斷輸入整數是否為正。僅有在輸入為正數時會執行依附大括號內的程式區塊,因此輸入零或負數時函數的輸出皆為 NULL。

is_positive <- function(x) {
  if (x > 0) {
    return("Positive.")
  }
}
print(is_positive(1))
print(is_positive(0))
print(is_positive(-1))

我們使用保留字 if 與 else if 搭配各自的條件建立一個 if-else if 敘述,如果 if 後面的條件為 TRUE 則執行依附其大括號內的程式區塊;若 if 後面的條件為 FALSE 則跳過該區塊改為檢視 else if 後面的條件,如果是 TRUE 就執行依附其大括號內的程式區塊;若 else if 後面的條件為 False 則跳過該區塊。

if (CONDITION) {
  # code block attached to if statement
} else if (CONDITION) {
  # code block attached to else if statement
}

CONDITION 可以透過關係運算符或者邏輯運算符生成,例如判斷輸入整數是否為正。這時輸入零或負數時函數的輸出皆為 “Nonpositive“。

is_positive <- function(x) {
  if (x > 0) {
    return("Positive.")
  } else {
    return("Nonpositive.")
  }
}
print(is_positive(1))
print(is_positive(0))
print(is_positive(-1))

我們使用保留字 if 、else 搭配一個條件建立一個 if-else 敘述,如果 if 後面的條件為 TRUE 則執行依附其大括號內的程式區塊;若 if 後面的條件為 FALSE 則跳過該區塊改為執行依附 else 大括號內的程式區塊。

if (CONDITION) {
  # code block attached to if statement
} else {
  # code block attached to else statement
}

CONDITION 可以透過關係運算符或者邏輯運算符生成,例如判斷輸入整數是否為正。

is_positive <- function(x) {
  if (x > 0) {
    return("Positive.")
  } else {
    return("Nonpositive.")
  }
}
print(is_positive(1))
print(is_positive(0))
print(is_positive(-1))

在前面的例子中,我們可以看到同樣一個「判斷輸入整數是否為正」的函數,可以用不同的條件判斷敘述來實作,那麼在應用情境上,該如何區別與採用?簡單來說,我們可以藉由判斷 CONDITION 是否互斥(Mutually exclusive)來決定。這個說法是依據 if 敘述、if-else if 與 if-else 敘述的主要差別,由於 if-else if 與 if-else 敘述的條件判斷是有先後順序的,當前面 if 敘述得到了邏輯 TRUE,意味著後續的 else if 或 else 敘述就無關緊要了。簡單來說,假如條件彼此之間是互斥的,條件判斷怎麼寫大概都沒有問題;但反過來說,假如條件彼此之間是「非互斥」的話,判斷的順序以及 if 敘述、if-else if 與 if-else 敘述就會需要謹慎地選擇。舉例來說,我們依據 https://en.wikipedia.org/wiki/Body_mass_index 寫作一個函數 get_bmi_desc() 可以根據輸入的 BMI 數值,給予過輕、正常、過重或肥胖的文字輸出,將四個條件寫成互斥的格式:

  1. 過輕:BMI < 18.5。

  2. 正常:BMI >= 18.5 and BMI < 25。

  3. 過重:BMI >= 25 and BMI < 30。

  4. 肥胖:BMI >= 30。

在條件互斥的狀況下,怎麼寫大概都沒有問題。

get_bmi_desc <- function(bmi) {
  if (bmi < 18.5){
    bmi_desc <- "過輕"
  } else if (bmi >= 30) {
    bmi_desc <- "肥胖"
  } else if (bmi >= 25 & bmi < 30) {
    bmi_desc <- "過重"
  } else if (bmi >= 18.5 & bmi < 25) {
    bmi_desc <- "正常"
  } 
  return(bmi_desc)
}   
print(get_bmi_desc(17))
print(get_bmi_desc(23))
print(get_bmi_desc(29))
print(get_bmi_desc(31))

但這不表示條件判斷就必須將條件都寫成互斥才可以,在適當順序與敘述安排下,即使非互斥也能達到正確判斷結果。

get_bmi_desc <- function(bmi) {
  if (bmi >= 30){
    bmi_desc <- "肥胖"
  } else if (bmi >= 25) {
    bmi_desc <- "過重"
  } else if (bmi >= 18.5) {
    bmi_desc <- "正常"
  } else {
    bmi_desc <- "過輕"
  } 
  return(bmi_desc)
}   
print(get_bmi_desc(17))
print(get_bmi_desc(23))
print(get_bmi_desc(29))
print(get_bmi_desc(31))

迴圈

迴圈是常見的流程控制之一,這是一種在區塊中只出現一次、但卻可能被連續執行多次的程式碼結構,區塊中的程式碼會執行特定的次數、執行到特定條件成立時結束或者走訪資料結構中的所有內容,迴圈能夠用開始、結束與間隔三個要素描述。

  • start:迴圈的開始。

  • stop:迴圈的結束。

  • step:迴圈從起始前往終止的方法。

我們可以使用保留字 while 與條件建立一個 while 迴圈區塊,當條件為 TRUE 重複執行依附其大括號內的程式區塊。

i = 0 # start
while (CONDITION) { # stop
  # code block attached to while statement
  i <- i + step
} 

CONDITION 可以透過關係運算符或者邏輯運算符生成,例如印出前 n 個奇數。

print_first_n_odds <- function(n) {
  i <- 1
  odds <- c() # empty vector
  while (length(odds) < n) {
    print(i)
    odds <- c(odds, i)
    i <- i + 2
  }
}
print_first_n_odds(5)

我們可以使用保留字 for 與資料結構建立一個 for 迴圈區塊,依附其大括號內的程式區塊會重複執行,直到一維資料結構的所有元素都被走訪完畢。

for (element in data_structure) { # start/stop/step
  # code block attached to for statement
}

一維資料結構被宣告完成的當下,迴圈的三要素就已經描述完畢。

  • start 一維資料結構的第一項元素。

  • stop 一維資料結構的最後一項元素。

  • step 一維資料結構的逗號區隔。

例如印出前 n 個奇數。

print_first_n_odds <- function(n) {
  vector_to_be_iterated <- seq(from = 1, by = 2, length.out = n)
  for (i in vector_to_be_iterated) {
    print(i)
  }
}
print_first_n_odds(5)

在前面的例子中,我們可以看到同樣一個「印出前 n 個奇數」的函數,可以用不同的迴圈類型來實作,那麼在應用情境上,該如何區別與採用?簡單來說,我們可以藉由判斷區塊程式「重複執行的次數是否已知」來決定,已知重複執行次數的情境可以用任意迴圈,未知重複執行次數的情境僅能採用 while 迴圈。這個說法是依據 while 迴圈與 for 迴圈的主要差別,while 在重複執行區塊程式的時候,會在 CONDITION 為 FALSE 的時候終止;而 for 在重複執行區塊程式的時候,會在一維資料結構中的元素用罄時候終止。這表示 while 迴圈根據一個為邏輯向量的物件決定重複執行程式區塊是否終止;for 迴圈則根據一個已知元素數量的資料結構決定重複執行程式區塊是否終止。

舉例來說,寫作一段「猜猜猜數字」程式就是一個「未知重複執行次數」的情況,沒有辦法在一開始就得知需要猜多少次才能結束。

random_integer <- sample.int(100, size = 1)
users_guess <- readline()
number_of_guess <- 1
while (as.integer(users_guess) != random_integer) {
  if (as.integer(users_guess) < random_integer) {
    print("Too small, guess larger!")
  } else if (as.integer(users_guess) > random_integer) {
    print("Too large, guess smaller!")
  }
  users_guess <- readline()
  number_of_guess <- number_of_guess + 1
}
sprintf("Correct! You guessed %s times for the answer %s!", number_of_guess, random_integer)

在 R 語言中所謂的一維資料結構是任意長度的向量(vector)、清單(list),這些一維資料結構都能夠直接放置在 for 迴圈中使用。

print_all_elements <- function(x) {
  for (element in x) {
    print(element)
  }
}
primes_vector <- c(2, 3, 5, 7, 11)
primes_list <- list(2, 3, 5, 7, 11)
print_all_elements(primes_vector)
print_all_elements(primes_list)

但矩陣(matrix)與資料框(data.frame)是具有列(rows)與欄(columns)的資料結構,要特別留意是希望走訪列或者欄。

primes_matrix <- matrix(c(1:5, c(2, 3, 5, 7, 11)), ncol = 2)
primes_dataframe = data.frame(primes_matrix)
# Iterating over a matrix's rows
for (i in 1:nrow(primes_matrix)) {
  print(primes_matrix[i, ])
}
# Iterating over a matrix's columns
for (j in 1:ncol(primes_matrix)) {
  print(primes_matrix[, j])
}
# Iterating over a data.frame's rows
for (i in 1:nrow(primes_dataframe)) {
  print(primes_dataframe[i, ])
}
# Iterating over a data.frame's columns
for (j in 1:ncol(primes_dataframe)) {
  print(primes_dataframe[, j])
}

將流程控制的兩個技巧條件判斷與迴圈合併使用會讓我們的程式撰寫更有彈性,保留字 break 能夠讓我們於走訪的過程中在條件為 TRUE 的時候終止迴圈,舉例來說當走訪到非數值的時候終止迴圈。

primes <- list(2, 3, "5", 7, 11)
for (p in primes) {
  if (!is.numeric(p)) {
      break
  }
  print(p)
}

保留字 next 能夠讓我們於走訪的過程中在條件為 TRUE 的時候略過該元素但是繼續完成迴圈,舉例來說當走訪到非整數的時候略過。

primes <- list(2, 3, "5", 7, 11)
for (p in primes) {
  if (!is.numeric(p)) {
      next
  }
  print(p)
}

例外處理

例外處理指的是針對程式區塊執行時所出現的例外(Exceptions)進行對應的處置,在執行程式區塊時遭遇的例外往往是由於錯誤而引發,包含無效的物件名稱、檔案不存在或型別不正確等,例外處理就是在程式區塊遭遇例外時能夠移交控制權的一種機制。簡單來說,例外處理就像是另一種類型的條件判斷,只不過條件判斷倚賴條件的布林值決定哪個程式區塊要被執行,而例外處理則是依賴程式區塊是否產生了錯誤來決定哪個程式區塊要被執行。我們可以使用函數 tryCatch() 處理例外。

tryCatch(
  {#the "try" part },
  error = function(CONDITION) {
    message(CONDITION) # The original error message
    # Choose a return value in case of error
    return(NA)
  },
  warning = function(CONDITION) {
    message(CONDITION) # The original warning message
    # Choose a return value in case of warning
    return(NULL)
  },
  finally = function(CONDITION) {
    # Everything that should be executed at the end,
    # regardless of success, error, or warning.
  }
)

例如將輸入的兩數相除,在輸入兩數其中一者為文字時候會產生錯誤。

divide <- function(a, b) {
  return(a/b)
}

print(divide(55, '11'))

加入例外處理 tryCatch() 函數以及 error 參數處理輸入文字時會產生的錯誤。

divide <- function(a, b) {
  out <- tryCatch(
    {
     a/b
    },
    error = function(CONDITION) {
      message(CONDITION) # The original error message
      # Choose a return value in case of error
      return(NA)
    }
  )
}
print(divide(55, '11'))

例如將輸入轉換為數值,在輸入文字時候會產生警告、輸出 NA。

print(as.numeric("Hello, world!"))

加入例外處理 tryCatch() 函數以及 warning 參數處理輸入文字時會產生的警告、改為輸出 NULL。

as_numeric_try_catch <- function(x) {
  out <- tryCatch(
    {
      as.numeric(x)
    },
    warning = function(CONDITION) {
      message(CONDITION) # The original error message
      # Choose a return value in case of error
      return(NULL)
    }
  )
}
print(as_numeric_try_catch("Hello, world!"))

在暸解 R 語言條件判斷、迴圈與例外處理這三種流程控制的機制之後,本篇文章來到尾聲,希望您也和我一樣期待下一篇文章。

延伸閱讀

  1. https://adv-r.hadley.nz/control-flow.html

  2. https://adv-r.hadley.nz/conditions.html

對於這篇文章有什麼想法呢?喜歡😻、留言🙋‍♂️或者分享🙌

Leave a comment

約維安計畫學員專區

This post is for paid subscribers

Already a paid subscriber? Sign in
© 2025 DATAINPOINT, INC.
Privacy ∙ Terms ∙ Collection notice
Start writingGet the app
Substack is the home for great culture

Share