วิธี“ แก้ไข” เพื่อระบุอาร์กิวเมนต์ที่เลือกระบุได้ในฟังก์ชัน R


165

ฉันสนใจในสิ่งที่เป็น "วิธีที่ถูกต้อง" ในการเขียนฟังก์ชันด้วยอาร์กิวเมนต์ที่เป็นตัวเลือกใน R เมื่อเวลาผ่านไปฉันสะดุดโค้ดสองสามชิ้นที่ใช้เส้นทางอื่นที่นี่และฉันไม่สามารถหาตำแหน่งที่เหมาะสม (เป็นทางการ) ในหัวข้อนี้

จนถึงตอนนี้ฉันได้เขียนข้อโต้แย้งเพิ่มเติมดังนี้:

fooBar <- function(x,y=NULL){
  if(!is.null(y)) x <- x+y
  return(x)
}
fooBar(3) # 3
fooBar(3,1.5) # 4.5

ฟังก์ชั่นเพียงแค่ส่งกลับข้อโต้แย้งของมันถ้าxมีให้เท่านั้น มันใช้ค่าเริ่มต้นNULLสำหรับอาร์กิวเมนต์ที่สองและหากอาร์กิวเมนต์นั้นไม่เป็นNULLเช่นนั้นฟังก์ชันจะเพิ่มตัวเลขสองตัว

อีกทางเลือกหนึ่งสามารถเขียนฟังก์ชั่นเช่นนี้ (โดยที่อาร์กิวเมนต์ที่สองจำเป็นต้องระบุด้วยชื่อ แต่สามารถunlist(z)ระบุหรือกำหนดz <- sum(...)แทนได้):

fooBar <- function(x,...){
  z <- list(...)
  if(!is.null(z$y)) x <- x+z$y
  return(x)
}
fooBar(3) # 3
fooBar(3,y=1.5) # 4.5

ส่วนตัวแล้วฉันชอบเวอร์ชั่นแรกมากกว่า อย่างไรก็ตามฉันเห็นทั้งดีและไม่ดีได้ทั้งคู่ เวอร์ชันแรกมีแนวโน้มที่จะเกิดข้อผิดพลาดน้อยกว่าเล็กน้อย แต่เวอร์ชันที่สองสามารถใช้เพื่อรวมจำนวนตัวเลือกโดยพลการ

มีวิธี "ถูกต้อง" เพื่อระบุอาร์กิวเมนต์ตัวเลือกใน R? จนถึงตอนนี้ฉันได้ตัดสินในแนวทางแรก แต่ทั้งคู่อาจรู้สึก "แฮ็ค" เล็กน้อย


ลองดูซอร์สโค้ดxy.coordsเพื่อดูวิธีการที่ใช้กันทั่วไป
Carl Witthoft

5
ซอร์สโค้ดสำหรับCarl Witthoft l ที่xy.coordsกล่าวถึงสามารถพบได้ที่xy.coords
RubenLaguna

คำตอบ:


129

คุณสามารถใช้missing()เพื่อทดสอบว่ามีการระบุอาร์กิวเมนต์y:

fooBar <- function(x,y){
    if(missing(y)) {
        x
    } else {
        x + y
    }
}

fooBar(3,1.5)
# [1] 4.5
fooBar(3)
# [1] 3

5
ฉันชอบที่หายไปดีกว่า โดยเฉพาะอย่างยิ่งถ้าคุณมีค่าเริ่มต้นเป็น NULL จำนวนมากคุณจะไม่มี x = NULL, y = NULL, z = NULL ในเอกสารประกอบแพ็กเกจของคุณ
rawr

5
@rawr missing()มีความหมายมากกว่าในแง่ที่ว่า "พูดว่ามันหมายถึงอะไร" นอกจากนี้ยังช่วยให้ผู้ใช้ผ่านค่า NULL ในสถานที่ที่เหมาะสม!
Josh O'Brien

31
สำหรับฉันมีข้อเสียอย่างใหญ่หลวงในการใช้วิธีที่ขาดหายไป: เมื่อ skimming อาร์กิวเมนต์ของฟังก์ชันคุณไม่สามารถดูได้ว่าต้องการอาร์กิวเมนต์ใดและตัวเลือกใดเป็นตัวเลือกอีกต่อไป
hadley

3
@param x numeric; something something; @param y numeric; **optional** something something; @param z logical; **optional** something something
rawr

4
missing()จะแย่มากเมื่อคุณต้องการที่จะผ่านการขัดแย้งจากฟังก์ชั่นหนึ่งไปยังอีกฟังก์ชั่น
John Smith

55

พูดตามตรงฉันชอบวิธีแรกของ OP ในการเริ่มต้นด้วยNULLค่าจากนั้นตรวจสอบด้วยis.null(ส่วนใหญ่เป็นเพราะมันง่ายและเข้าใจง่าย) มันอาจขึ้นอยู่กับวิธีที่ผู้คนคุ้นเคยกับการเขียนโค้ด แต่ Hadley ดูเหมือนจะให้การสนับสนุนis.nullเช่นกัน:

จากหนังสือของ Hadley "Advanced-R" บทที่ 6, ฟังก์ชั่น, หน้า 82 (สำหรับรุ่นออนไลน์ตรวจสอบที่นี่ ):

คุณสามารถกำหนดได้ว่ามีการระบุอาร์กิวเมนต์หรือไม่พร้อมกับฟังก์ชัน () ที่หายไป

i <- function(a, b) {
  c(missing(a), missing(b))
}
i()
#> [1] TRUE TRUE
i(a = 1)
#> [1] FALSE  TRUE
i(b = 2)
#> [1]  TRUE FALSE
i(1, 2)
#> [1] FALSE FALSE

บางครั้งคุณต้องการเพิ่มค่าเริ่มต้นที่ไม่สำคัญซึ่งอาจต้องใช้รหัสหลายบรรทัดในการคำนวณ แทนที่จะใส่รหัสนั้นในคำจำกัดความของฟังก์ชันคุณสามารถใช้ส่วนหายไป () เพื่อคำนวณมันแบบมีเงื่อนไขหากจำเป็น อย่างไรก็ตามสิ่งนี้ทำให้ยากที่จะทราบว่าข้อโต้แย้งใดที่จำเป็นและเป็นทางเลือกโดยไม่อ่านเอกสารอย่างละเอียด แต่ฉันมักจะตั้งค่าเริ่มต้นเป็น NULL และใช้ is.null () เพื่อตรวจสอบว่ามีการโต้แย้ง


2
น่าสนใจ ฟังดูสมเหตุสมผล แต่คุณเคยคิดว่าตัวเองสับสนหรือไม่ว่าการโต้เถียงในฟังก์ชั่นใดที่จำเป็นและเป็นทางเลือก ผมไม่แน่ใจว่าที่ผมได้เคยมีประสบการณ์จริงว่า ...
จอชโอไบรอัน

2
@ JoshO'Brien ฉันคิดว่าฉันไม่ได้มีปัญหากับรูปแบบการเข้ารหัสที่จะซื่อสัตย์อย่างน้อยก็ไม่เคยเป็นปัญหาที่สำคัญอาจเป็นเพราะเอกสารหรืออ่านรหัสต้นฉบับ และนั่นคือเหตุผลที่ฉันพูดเป็นหลักว่าจริงๆมันเป็นเรื่องของรูปแบบการเข้ารหัสที่คุณคุ้นเคย ฉันใช้NULLวิธีนี้มาระยะหนึ่งแล้วและนั่นอาจเป็นเหตุผลว่าทำไมฉันจึงคุ้นเคยกับมันมากขึ้นเมื่อฉันเห็นซอร์สโค้ด ดูเหมือนเป็นธรรมชาติมากขึ้นสำหรับฉัน ที่กล่าวว่าในขณะที่คุณพูดว่า base R ใช้ทั้งสองแนวทางดังนั้นมันจึงขึ้นอยู่กับความชอบส่วนบุคคล
LyzandeR

2
ถึงตอนนี้ฉันหวังว่าฉันจะทำเครื่องหมายสองคำตอบให้ถูกต้องเพราะสิ่งที่ฉันได้มาถึงการใช้ทั้งสองis.nullและmissingขึ้นอยู่กับบริบทและสิ่งที่ใช้ในการโต้แย้ง
SimonG

5
ไม่เป็นไร @SimonG และขอบคุณ :) ฉันยอมรับว่าทั้งสองคำตอบนั้นดีมากและบางครั้งก็ขึ้นอยู่กับบริบท นี่เป็นคำถามที่ดีมากและฉันเชื่อว่าคำตอบจะให้ข้อมูลและความรู้ที่ดีซึ่งเป็นเป้าหมายหลักที่นี่
LyzandeR

24

นี่เป็นกฎง่ายๆของฉัน:

หากสามารถคำนวณค่าเริ่มต้นจากพารามิเตอร์อื่น ๆ ให้ใช้นิพจน์เริ่มต้นเช่นเดียวกับใน:

fun <- function(x,levels=levels(x)){
    blah blah blah
}

ถ้าใช้อย่างอื่นหายไป

fun <- function(x,levels){
    if(missing(levels)){
        [calculate levels here]
    }
    blah blah blah
}

ในกรณีที่ไม่ค่อยเกิดขึ้นกับคุณสิ่งที่ผู้ใช้อาจต้องการระบุค่าเริ่มต้นที่คงอยู่ทั้งเซสชัน R ใช้getOption

fun <- function(x,y=getOption('fun.y','initialDefault')){# or getOption('pkg.fun.y',defaultValue)
    blah blah blah
}

หากมีการใช้พารามิเตอร์บางตัวขึ้นอยู่กับคลาสของอาร์กิวเมนต์แรกให้ใช้ S3 ทั่วไป:

fun <- function(...)
    UseMethod(...)


fun.character <- function(x,y,z){# y and z only apply when x is character
   blah blah blah 
}

fun.numeric <- function(x,a,b){# a and b only apply when x is numeric
   blah blah blah 
}

fun.default <- function(x,m,n){# otherwise arguments m and n apply
   blah blah blah 
}

ใช้...เฉพาะเมื่อคุณส่งพารามิเตอร์เพิ่มเติมไปยังฟังก์ชันอื่น

cat0 <- function(...)
    cat(...,sep = '')

สุดท้ายหากคุณเลือกที่ใช้งาน...โดยไม่ต้องผ่านจุดเข้าสู่ฟังก์ชั่นอื่นเตือนผู้ใช้ว่าการทำงานของคุณจะไม่สนใจพารามิเตอร์ที่ไม่ได้ใช้เพราะมันสามารถเป็นอย่างมากทำให้เกิดความสับสนเป็นอย่างอื่น

fun <- (x,...){
    params <- list(...)
    optionalParamNames <- letters
    unusedParams <- setdiff(names(params),optionalParamNames)
    if(length(unusedParams))
        stop('unused parameters',paste(unusedParams,collapse = ', '))
   blah blah blah 
}

ตัวเลือกวิธี s3 เป็นหนึ่งในสิ่งแรกที่นึกถึงฉันเช่นกัน
rawr

2
ในการหวนกลับฉันได้กลายเป็นรักวิธี OP ของการกำหนดNULLในลายเซ็นของฟังก์ชั่นในขณะที่มันสะดวกมากขึ้นสำหรับการทำหน้าที่ที่ห่วงโซ่อย่าง
Jthorpe

10

มีหลายตัวเลือกและไม่มีวิธีที่ถูกต้องอย่างเป็นทางการและไม่มีตัวเลือกใดที่ไม่ถูกต้องจริงๆถึงแม้ว่าพวกเขาจะสามารถนำเสนอข้อมูลที่แตกต่างกันไปยังคอมพิวเตอร์

สำหรับตัวอย่างที่กำหนดฉันคิดว่าตัวเลือกที่ชัดเจนที่สุดคือการให้ค่าเริ่มต้นตัวตนในกรณีนี้ทำสิ่งที่ชอบ:

fooBar <- function(x, y=0) {
  x + y
}

นี่คือตัวเลือกที่สั้นที่สุดที่แสดงจนถึงขณะนี้และความสั้นสามารถช่วยให้สามารถอ่านได้ (และบางครั้งแม้แต่ความเร็วในการดำเนินการ) เป็นที่ชัดเจนว่าสิ่งที่ส่งคืนคือผลรวมของ x และ y และคุณสามารถเห็นได้ว่า y ไม่ได้รับค่าที่จะเป็น 0 ซึ่งเมื่อเพิ่มเข้าไปใน x จะส่งผลให้ x เห็นได้ชัดว่าหากมีสิ่งที่ซับซ้อนกว่าการเติมนอกจากนี้จะต้องใช้ค่าตัวตนที่แตกต่างกัน (ถ้ามี)

สิ่งหนึ่งที่ฉันชอบเกี่ยวกับวิธีนี้คือมันชัดเจนว่าค่าเริ่มต้นคืออะไรเมื่อใช้argsฟังก์ชั่นหรือแม้แต่ดูไฟล์ช่วยเหลือ (คุณไม่จำเป็นต้องเลื่อนลงไปที่รายละเอียดมันอยู่ที่นั่นในการใช้งาน )

ข้อเสียเปรียบสำหรับวิธีนี้คือเมื่อค่าเริ่มต้นซับซ้อน (ต้องใช้รหัสหลายบรรทัด) จากนั้นก็อาจลดความสามารถในการอ่านเพื่อพยายามใส่ค่าทั้งหมดลงในค่าเริ่มต้นและmissingหรือNULLวิธีการจะมีความเหมาะสมมากขึ้น

ความแตกต่างอื่น ๆ ระหว่างวิธีจะปรากฏขึ้นเมื่อพารามิเตอร์ถูกส่งผ่านไปยังฟังก์ชันอื่นหรือเมื่อใช้match.callหรือsys.callฟังก์ชั่น

ดังนั้นฉันจึงเดาว่า "ถูกต้อง" วิธีขึ้นอยู่กับสิ่งที่คุณวางแผนที่จะทำกับข้อโต้แย้งนั้นและข้อมูลที่คุณต้องการถ่ายทอดให้ผู้อ่านรหัสของคุณ


7

ฉันมักจะชอบใช้ NULL เพื่อความชัดเจนของสิ่งที่จำเป็นและสิ่งที่เป็นตัวเลือก หนึ่งคำเตือนเกี่ยวกับการใช้ค่าเริ่มต้นที่ขึ้นอยู่กับข้อโต้แย้งอื่น ๆ ตามที่ Jthorpe แนะนำ ค่าไม่ได้ถูกตั้งค่าเมื่อเรียกใช้ฟังก์ชัน แต่เมื่อมีการอ้างอิงอาร์กิวเมนต์ครั้งแรก! ตัวอย่างเช่น

foo <- function(x,y=length(x)){
    x <- x[1:10]
    print(y)
}
foo(1:20) 
#[1] 10

ในทางตรงกันข้ามถ้าคุณอ้างอิง y ก่อนที่จะเปลี่ยน x:

foo <- function(x,y=length(x)){
    print(y)
    x <- x[1:10]
}
foo(1:20) 
#[1] 20

นี่เป็นอันตรายเล็กน้อยเพราะทำให้ยากที่จะติดตามว่า "y" ถูกกำหนดค่าเริ่มต้นราวกับว่ามันไม่ได้ถูกเรียกใช้ก่อนหน้าในฟังก์ชั่น


7

เพิ่งต้องการชี้ให้เห็นว่าsinkฟังก์ชั่นในตัวมีตัวอย่างที่ดีของวิธีที่แตกต่างในการตั้งค่าอาร์กิวเมนต์ในฟังก์ชั่น:

> sink
function (file = NULL, append = FALSE, type = c("output", "message"),
    split = FALSE)
{
    type <- match.arg(type)
    if (type == "message") {
        if (is.null(file))
            file <- stderr()
        else if (!inherits(file, "connection") || !isOpen(file))
            stop("'file' must be NULL or an already open connection")
        if (split)
            stop("cannot split the message connection")
        .Internal(sink(file, FALSE, TRUE, FALSE))
    }
    else {
        closeOnExit <- FALSE
        if (is.null(file))
            file <- -1L
        else if (is.character(file)) {
            file <- file(file, ifelse(append, "a", "w"))
            closeOnExit <- TRUE
        }
        else if (!inherits(file, "connection"))
            stop("'file' must be NULL, a connection or a character string")
        .Internal(sink(file, closeOnExit, FALSE, split))
    }
}

1

แล้วเรื่องนี้ล่ะ

fun <- function(x, ...){
  y=NULL
  parms=list(...)
  for (name in names(parms) ) {
    assign(name, parms[[name]])
  }
  print(is.null(y))
}

จากนั้นลอง:

> fun(1,y=4)
[1] FALSE
> fun(1)
[1] TRUE
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.