(Acest articol a fost publicat pentru prima dată pe Analitică deschisăși a contribuit cu drag la R-Bloggers). (Puteți raporta problema despre conținutul de pe această pagină aici)
Doriți să vă împărtășiți conținutul pe R-Bloggers? Faceți clic aici dacă aveți un blog sau aici dacă nu.
Un aspect important al scrierii unui R-script sau a unui pachet R este asigurarea reproductibilității și menținerii codului dezvoltat, nu numai pentru alții, ci și pentru viitoarele noastre. Ecosistemul modern R oferă diverse instrumente și pachete pentru a ajuta la organizarea și validarea codului R scris. Unele pachete utilizate pe scară largă includ roxygen2
(pentru documentația funcțională), renv
(pentru gestionarea dependenței și izolarea mediului) și testthat
, tinytest
şi Runit
pentru testarea unității (1).
Când vine vorba de dezvoltarea pachetelor, este o practică bună să rulați R CMD check
Pentru a efectua o serie de verificări automate care identifică posibile probleme cu Palagerea R. Printre cecurile efectuate de
R CMD check
este o inspecție statică a copacilor de sintaxă internă a codului prin utilizarea
codetools
pachet. Această analiză a codului descoperă funcții și variabile nedefinite fără a executa codul în sine, ceea ce duce la următoarele (poate familiare) notificări:
❯ checking R code for possible problems ... NOTE my_fun: no visible binding for global variable ‘g’
Variabilele globale nedefinite returnate de R CMD check
poate fi fals pozitiv cauzat de funcții care utilizează o evaluare a maskingului de date sau a evaluării non-standard, cum ar fi subset()
, transform()
sau with()
. În aceste cazuri, o soluție comună este de a suprima notificările prin includerea numelor variabile în interiorul unui apel la utils::globalVariables()
.
Cel mai important, dorim să detectăm nume variabile care sunt cu adevărat nedefinite cât mai curând posibil, deoarece acestea ar putea indica o greșeală în cod sau semnalând o funcție lipsă sau un import de pachete.
În acest context, această postare introduce un pachet R minim checkglobals
menită să servească ca o alternativă eficientă la analiza codului static furnizat de codetools
Pentru a verifica pachetele R și R-Scripturi pentru importurile de funcții lipsă și numele variabile la zbor. Procedurile de inspecție a codului sunt implementate folosind API-ul C intern al R pentru eficiență și nu sunt necesare strict necesare dependențe externe de pachete R (sunt sugerate doar CLI și Knitr pentru utilizarea interactivă și, respectiv, verificarea documentelor RMD).
Exemplu de utilizare
checkglobals
-Plackage conține o singură funcție de înveliș checkglobals()
Pentru a inspecta scripturile R, RMD-Documents, folderele, șirurile cu cod R sau pachetele R. Ca exemplu, luați în considerare următorul R-script care conține o aplicație strălucitoare demo (sursa: https://raw.githubusercontent.com/rstudio/shiny-examples/main/004-mpg/app.r).
# scripts/app.R library(shiny) library(datasets) # Data pre-processing ---- mpgData <- mtcars mpgData$am <- factor(mpgData$am, labels = c("Automatic", "Manual")) # Define UI for miles per gallon app ---- ui <- fluidPage( titlePanel("Miles Per Gallon"), sidebarLayout( sidebarPanel( selectInput("variable", "Variable:", c("Cylinders" = "cyl", "Transmission" = "am", "Gears" = "gear")), checkboxInput("outliers", "Show outliers", TRUE) ), mainPanel( h3(textOutput("caption")), plotOutput("mpgPlot") ) ) ) # Define server logic to plot various variables against mpg ---- server <- function(input, output) { formulaText <- reactive({ paste("mpg ~", input$variable) }) output$caption <- renderText({ formulaText() }) output$mpgPlot <- renderPlot({ boxplot(as.formula(formulaText()), data = mpgData, outline = input$outliers, col = "#75AADB", pch = 19) }) } # Create Shiny app ---- shinyApp(ui, server)
Chemare checkglobals()
cu argumentul file
pe R-script salvat ca fișier local se întoarce ca ieșire:
Privind ieșirea tipărită a obiectului returnat de checkglobals()
enumeră următoarele informații:
- nume şi locaţie din toate variabilele globale nerecunoscute;
- nume şi locaţie Dintre toate funcțiile importate detectate grupate de R-Package.
locaţie app.R#36
listează numele de fișier r (app.R
) și numărul liniei (36
) din variabila sau funcția detectată. Dacă CLI este instalat și sunt acceptate CLI-hiperlink-uri, făcând clic pe locaţie Link -urile deschide fișierul sursă care indică numărul de linie dată. Barele și numărarea în spatele numelor de pachete importate evidențiază numărul de apeluri funcționale detectate din fiecare pachet.
Informații mai detaliate pot fi obținute prin apel print()
direct. De exemplu, putem imprima liniile de cod sursă la care se referă la variabilele globale nerecunoscute cu:
Detectarea funcțiilor și pachetelor importate este o motivație importantă pentru
checkglobals
-pachet. În primul rând, acest lucru ne permite să validăm fișierul spațiului de nume al unui pachet de dezvoltare R sau să verificăm R-Scripturi pentru orice pachete suplimentare care necesită instalare înainte de executarea codului. În al doilea rând, aceste informații pot fi utilizate pentru a înțelege mai bine importanța unui pachet importat, de exemplu, pentru a determina cât de mult efort ar depune pentru a -l elimina sau înlocui ca dependență. Acest lucru este diferit de, de exemplu, codetools
Pachet, unde findGlobals()
sau
checkUsage()
Returnați un nume variabil nedefinit dacă un import de funcții nu este recunoscut, dar nu returnați nume variabile care au fost recunoscute sub numele de importuri. Același lucru este valabil și pentru pachetele de comoditate lintr
(cu object_usage_linter()
) sau
globals
care furnizează codetools
Învelișuri producând rezultate similare ca returnate de R CMD check
. Mai asemănător este renv::dependencies()
care scanează pentru toate pachetele încărcate și/sau importate într-un folder de proiect R, analizând fișierele de descriere și spațiu de nume al unui pachet R sau prin detectarea apelurilor la library()
, require()
etc. într-un R-script. Rețineți că renv::dependencies()
Returnează numele pachetelor, dar nu funcțiile apelate din aceste pachete.
Un beneficiu suplimentar al unui pachet minim și eficient de analiză a codului este acela că putem reduce semnificativ rularea necesară pentru a inspecta mari pachete R sau cod-uri care permit să verifice rapid codul în mod interactiv în timpul dezvoltării:
## absolute timings (seconds) for inspecting the shiny package ## (100-fold relative time difference) bench::mark( lint_package = lint_package("~/git/shiny", linters = list(object_usage_linter())), checkglobals = checkglobals(pkg = "~/git/shiny/"), iterations = 10, check = FALSE, time_unit = "s" ) #> # A tibble: 2 × 6 #> expression min median `itr/sec` mem_alloc `gc/sec` #>#> 1 lint_package 18.8 19.5 0.0508 1.33GB 2.42 #> 2 checkglobals 0.157 0.162 5.96 15.69MB 1.19
R Markdown Files
file
Argumentul acceptă și R Markdown (.Rmd
sau .Rmarkdown
) locații de fișiere. Pentru fișierele R Markdown, bucățile de cod R sunt extrase mai întâi într-un script R temporar cu knitr::purl()
care este apoi analizat de checkglobals()
. În loc de un fișier local, file
argument în
checkglobals()
Poate fi, de asemenea, o locație de fișier la distanță (de exemplu, un server sau web), caz în care fișierul de la distanță este descărcat mai întâi ca fișier temporar cu download.file()
. Mai jos, scanează unul dintre
tidyr
Vignetele pachetului (sursa: https://raw.githubusercontent.com/tidyverse/tidyr/main/vignettes/tidy-data.rmd),
R-pachete care sunt importate sau încărcate, dar nu au importuri de funcții detectate
n/a
referinţă. Acest lucru se poate întâmpla când checkglobals()
ignoră în mod fals una sau mai multe funcții importate din pachetul dat sau când pachetul nu este de fapt necesar ca dependență. În ambele cazuri, aceasta este o informație utilă. În exemplul de mai sus, tibble
este încărcat pentru a utiliza
tribble()
dar tribble()
funcția este, de asemenea, exportată de dplyr
deci apare sub
dplyr
în schimb import.
Dosare
Dosarele care conțin scripturi R pot fi scanate cu dir
argument, care inspectează toate scripturile R prezente în dir
(și oricare dintre subdirectoriile sale). Următorul exemplu scanează un folder de aplicații R-shiny care conține un ui.R
şi server.R
Fișier (sursă: https://github.com/rstudio/shiny-examples/tree/main/018-datatatable-options),
Dacă importurile sunt detectate dintr-un pachet R care nu este instalat în sesiunea R actuală, se imprimă o alertă (ca și în cazul DT
pachet de mai sus). Apeluri funcționale care accesează explicit pachetul R lipsă, folosind EG ::
sau :::
poate fi în continuare identificat pe deplin ca nume de funcții importate. Apelurile funcționale fără nicio referire la pachetul R lipsă vor fi listate ca variabile globale nerecunoscute.
R-pachete
Folderele de pachete R pot fi scanate cu pkg
argument. Conceptual, checkglobals()
scanează toate fișierele din /R
Folder al pachetului și contrastează globurile și importurile detectate (nerecunoscute) și importurile enumerate în fișierul de spații de nume a pachetului. R-scripturi prezente în altă parte a pachetului (de exemplu, în /inst
folder) nu sunt analizate, deoarece acestea nu sunt acoperite de fișierul de nume de nume de pachet. Pentru a ilustra, putem rula checkglobals()
pe propriul folder de pachete:
Pachete încorporate
Pe lângă folderele locale de pachete R, pkg
Argumentul acceptă, de asemenea, căi de fișiere pentru pachetele R ROS (tar.gz). Acesta poate fi fie un pachet tar.gz pe sistemul de fișiere local, fie o locație de fișiere la distanță, cum ar fi Web -ul (similar cu file
argument).
Sistemul de fișiere local:
Locația fișierului de la distanță:
Limitări cunoscute
În concluzie, discutăm unele dintre limitările analizei codului static cu codetools
şi
checkglobals
. Când folosiți codetools
(sau R CMD check
) Există mai multe scenarii în care inspecția codului este cunoscută pentru a sări nume nedefinite care ar putea fi detectate. În primul rând, poate fi ratată o variabilă care necesită evaluare înainte de a fi definită codetools
nu urmărește în ce atribuire și evaluare a comenzii se întâmplă în interiorul unui domeniu local. Iată un exemplu minim folosind
codetools::findGlobals()
:
## findGlobals requires a function as input test1 <- function() { print(x) x <- 1 } ## calling this function generates an error test1() #> (1) NA library(codetools) ## x is not recognized as an undefined ## variable at the moment of evaluation findGlobals(test1) #> (1) "{" "<-" "print"
O altă situație destul de comună este utilizarea unui nume de funcție a caracterului în interiorul unui funcțional, de exemplu Reduce()
, Filter()
, Map()
sau apply
-Familia funcțiilor. Aceste nume de funcții sunt vizualizate de codetools
ca șiruri obișnuite de personaje:
test2 <- function() { do.call("foo", 1) } ## foo is not recognized as an undefined ## variable since it is defined as a string findGlobals(test2) #> (1) "{" "do.call"
În cele din urmă, declarațiile de atribuire mai complexe pot să nu fie întotdeauna gestionate așa cum era de așteptat:
test3 <- function() { assign(x = "x1", value = 1) assign(value = 2, x = "x2") c(x1, x2) } ## assignment to x1 is recognized correctly, ## but assignment to x2 is not findGlobals(test3) #> (1) "{" "assign" "c" "x2" x <- NA test4 <- function() { x <<- 1 x } ## x is assigned in a different scope ## but is available when evaluated findGlobals(test4) #> (1) "{" "<<-" "x"
checkglobals
-Plackage încearcă să abordeze unele dintre aceste cazuri de utilizare, dar datorită flexibilității R ca limbă, există o serie de cazuri de utilizare la care ne putem gândi, care sunt fie prea ambigue, fie complexe pentru a fi analizate fără evaluarea codului în sine. Mai jos enumerăm unele dintre aceste cazuri, unde
checkglobals()
Nu recunoaște un nume variabil (fals negativ) sau detectează în mod fals o variabilă globală atunci când nu ar trebui (fals pozitiv).
Variabilă de caractere/nume funcții
## this works (character arguments are recognized as functions) checkglobals(text="do.call(args = list(1), what = "median")") checkglobals(text="Map("g", 1, n = 1)") checkglobals(text="stats::aggregate(x ~ ., data = y, FUN = "g")") ## this doesn't work (evaluation is required) checkglobals(text="g <- "f"; Map(g, 1, n = 1)") checkglobals(text = "eval(substitute(g))") ## same for ~, expression, quote, bquote, Quote, etc. ## this works (calling a function in an exotic way) checkglobals(text=""head"(1:10)") checkglobals(text="`::`("utils", "head")(1:10)") checkglobals(text="list("function" = utils::head)$`function`(1:10)") ## this doesn't work (evaluation is required) checkglobals(text="get("head")(1:10)") checkglobals(text="methods::getMethod("f", signature = "ANY")")
Încărcarea pachetului
## this works (simple evaluation of package names) checkglobals(text="attachNamespace("utils"); head(1:10)") checkglobals(text="pkg <- "utils"; library(pkg, character.only = TRUE); head(1:10)") ## this doesn't work (more complex evaluation is required) checkglobals(text="pkg <- function() "utils"; library(pkg(), character.only = TRUE); head(1:10)") checkglobals(text="loadPkg <- library; loadPkg(utils)") checkglobals(text="box::use(utils(...))")
Simboluri necunoscute
## this works (special functions self, private, super are recognized) checkglobals(text="R6::R6Class("cl", public = list( initialize = function(...) self$f(...), f = function(...) private$p ), private = list( p = list() ))") ## this doesn't work (data masking) checkglobals(text="transform(mtcars, mpg2 = mpg^2)") checkglobals(text="attach(iris); print(Sepal.Width)")
Evaluare leneșă
## this works (basic lazy evaluation) checkglobals(text="{ addy <- function(y) x + y x <- 0 addy(1) }") checkglobals( text="function() { on.exit(rm(x)) x <- 0 }") ## this doesn't work (lazy evaluation in external functions) checkglobals( text="server <- function(input, output) { add1x <- shiny::reactive({ add1(input$x) }) add1 <- function(x) x + 1 }")
- CheckGlobals, pagina web CRAN a
checkglobals
Pachet care include link -uri către documentație suplimentară. codetools::findGlobals()
detectează variabile globale de la R-Scripturi prin analiza codului static. Asta și altul Codetools Funcțiile sunt utilizate în verificările codului sursă rulate deR CMD check
.- Globals, R-pachet de H. Bengtsson care oferă o reimplementare a funcțiilor din Codetools Pentru a identifica variabilele globale folosind diverse strategii de export în calcule paralele.
renv::dependencies()
detectează dependențele de pachete R scanând toate fișierele R într-un proiect pentru funcții sau pachete importate prin analiza codului static.- LINTR, R-PACKAGE de J. Hester și alții pentru a efectua o analiză generală a codului static în proiectele R.
lintr::object_usage_linter()
oferă un înveliș de
codetools::checkUsage()
pentru a detecta variabile globale similare cuR CMD check
.
- Testarea unității cu
R CMD check
nu necesită utilizarea pachetelor externe, dar mulți dezvoltatori de pachete se bazează pe pachete precumtestthat
sautinytest
pentru comoditate și datorită practicii comune.