Funciones de escrituras(procedimientos) para objetos data.table
(2)
En el libro Software for Data Analysis: Programming with R , John Chambers enfatiza que las funciones generalmente no deberían escribirse por su efecto secundario; más bien, que una función debe devolver un valor sin modificar ninguna variable en su entorno de llamada. Por el contrario, escribir una buena secuencia de comandos utilizando objetos data.table debería evitar específicamente el uso de la asignación de objetos con <-
, que normalmente se utiliza para almacenar el resultado de una función.
Primero, es una pregunta técnica. Imagine una función R llamada proc1
que acepta un objeto x
data.table
como su argumento (además de, tal vez, otros parámetros). proc1
devuelve NULL pero modifica x
usando :=
. Por lo que entiendo, proc1
calling proc1(x=x1)
hace una copia de x1
solo por la forma en que las promesas funcionan. Sin embargo, como se demuestra a continuación, el objeto original x1
sigue siendo modificado por proc1
. ¿Por qué / cómo es esto?
> require(data.table)
> x1 <- CJ(1:2, 2:3)
> x1
V1 V2
1: 1 2
2: 1 3
3: 2 2
4: 2 3
> proc1 <- function(x){
+ x[,y:= V1*V2]
+ NULL
+ }
> proc1(x1)
NULL
> x1
V1 V2 y
1: 1 2 2
2: 1 3 3
3: 2 2 4
4: 2 3 6
>
Además, parece que el uso de proc1(x=x1)
no es más lento que hacer el procedimiento directamente en x, lo que indica que mi comprensión vaga de las promesas es incorrecta y que funcionan de una forma de paso por referencia:
> x1 <- CJ(1:2000, 1:500)
> x1[, paste0("V",3:300) := rnorm(1:nrow(x1))]
> proc1 <- function(x){
+ x[,y:= V1*V2]
+ NULL
+ }
> system.time(proc1(x1))
user system elapsed
0.00 0.02 0.02
> x1 <- CJ(1:2000, 1:500)
> system.time(x1[,y:= V1*V2])
user system elapsed
0.03 0.00 0.03
Entonces, dado que pasar un argumento data.table a una función no agrega tiempo, eso hace posible escribir procedimientos para objetos data.table, incorporando tanto la velocidad de data.table como la capacidad de generalización de una función. Sin embargo, dado lo que dijo John Chambers, que las funciones no deberían tener efectos secundarios, ¿está realmente "bien" escribir este tipo de programación de procedimientos en R? ¿Por qué estaba argumentando que los efectos secundarios son "malos"? Si voy a ignorar su consejo, ¿qué tipo de riesgos debo tener en cuenta? ¿Qué puedo hacer para escribir "buenos" procedimientos de data.table?
La documentación podría mejorarse (sugerencias muy bienvenidas), pero aquí está lo que hay en este momento. Tal vez debería decir "incluso dentro de las funciones"?
En ?":="
:
data.tables no se copian-en-cambio por: =, setkey o cualquiera de las otras funciones set *. Ver copia.
DT se modifica por referencia y se devuelve el nuevo valor. Si necesita una copia, primero tome una copia (usando DT2 = copy (DT)). Recuerde que este paquete es para datos grandes (de tipos de columnas mixtas, con claves de varias columnas) donde las actualizaciones por referencia pueden ser de muchos órdenes de magnitud más rápidas que copiar toda la tabla.
y en ?copy
(pero me doy cuenta de que esto está confuso con setkey):
La entrada se modifica por referencia y se devuelve (de forma invisible) para que pueda usarse en declaraciones compuestas; por ejemplo, setkey (DT, a) [J ("foo")]. Si necesita una copia, primero tome una copia (usando DT2 = copy (DT)). copy () también puede ser útil algunas veces antes de: = se usa para asignar una columna por referencia. Ver? Copiar. Tenga en cuenta que setattr también está en el bit de paquete. Ambos paquetes simplemente exponen la función setAttrib interna de R a nivel C, pero difieren en el valor de retorno. bit :: setattr devuelve NULL (de forma invisible) para recordarle que la función se utiliza por su efecto secundario. data.table :: setattr devuelve el objeto modificado (de forma invisible) para su uso en sentencias compuestas.
donde las últimas dos oraciones sobre bit::setattr
relacionan con el punto 2 de flodel, curiosamente.
También vea estas preguntas relacionadas:
Comprender exactamente cuando un data.table es una referencia a (frente a una copia de) otro data.table
Pase por referencia: El operador: = en el paquete data.table
data.table 1.8.1 .: "DT1 = DT2" no es lo mismo que DT1 = copy (DT2)?
Me gusta mucho esta parte de tu pregunta:
que hace posible escribir procedimientos para objetos data.table, incorporando tanto la velocidad de data.table como la capacidad de generalización de una función.
Sí, esta es definitivamente una de las intenciones. Considere cómo funciona una base de datos: muchos usuarios / programas diferentes cambian por referencia (insertar / actualizar / eliminar) una o más tablas (grandes) en la base de datos. Eso funciona bien en la base de datos, y se parece más a la forma en que piensa data.table. De ahí el video svSocket en la página de inicio, y el deseo de insert
y delete
(por referencia, solo verbo, funciones de efectos secundarios).
Sí, la adición, modificación, eliminación de columnas en data.table
s se realiza por reference
. En cierto sentido, es una buena cosa porque una data.table
de datos generalmente contiene una gran cantidad de datos, y sería una gran data.table
memoria y tiempo reasignarlo todo cada vez que se realiza un cambio. Por otro lado, es algo malo porque va en contra del enfoque de programación funcional sin no-side-effect
que R intenta promover utilizando el pass-by-value
por defecto. Con la programación sin efectos secundarios, hay poco de qué preocuparse cuando llama a una función: puede estar seguro de que sus entradas o su entorno no se verán afectados, y puede centrarse solo en la salida de la función. Es simple, por lo tanto, cómodo.
Por supuesto, está bien ignorar el consejo de John Chambers si sabes lo que estás haciendo. Acerca de escribir "buenos" procedimientos de data.tables, aquí hay un par de reglas que consideraría si fuera usted, como una manera de limitar la complejidad y la cantidad de efectos secundarios:
- una función no debe modificar más de una tabla, es decir, la modificación de esa tabla debe ser el único efecto secundario,
- si una función modifica una tabla, entonces haga que esa tabla sea la salida de la función. Por supuesto, no querrá volver a asignarlo: simplemente ejecute
do.something.to(table)
y notable <- do.something.to(table)
. Si, en cambio, la función tenía otra salida ("real"), entonces al llamar alresult <- do.something.to(table)
, es fácil imaginar cómo puede enfocar su atención en la salida y olvidar que la función de invocación tenía un efecto secundario en su mesa.
Mientras que las funciones "una salida / efecto secundario" son la norma en R, las reglas anteriores permiten "una salida o efecto secundario". Si acepta que un efecto colateral es de algún modo una forma de salida, entonces aceptará que no doblaré demasiado las reglas si me apego al estilo de programación funcional de una salida de R. Permitir que las funciones tengan múltiples efectos secundarios sería un poco más exagerado; no es que no puedas hacerlo, pero trataría de evitarlo si es posible.