5

I'm creating a custom S3 object in R, but when I print it the attributes also gets printed. For example:

x <- 1:3
x <- structure(x, class = "myclass")
print(x)
#> [1] 1 2 3
#> attr(,"class")
#> [1] "myclass"

Fair enough, I haven't yet added a print method. Many answers here in SO and other places recommend removing the attributes from the object, and then printing it, like so (also returning the object invisibly to maintain the print original property):

x <- 1:3
x <- structure(x, class = "myclass")

print.myclass <- function(x, ...) {
  print(unclass(x), ...)
  return(invisible(x))
}

print(x)
#> [1] 1 2 3

This approach, however, creates a copy of x, and I don't want that. We can see that with tracemem():

x <- 1:3
x <- structure(x, class = "myclass")
tracemem(x)
#> [1] "<0x558e1a9adcc8>"

print.myclass <- function(x, ...) {
  print(unclass(x), ...)
  return(invisible(x))
}

print(x)
#> tracemem[0x558e1a9adcc8 -> 0x558e1aa63918]: print print.myclass print
#> [1] 1 2 3

The original print doesn't create a copy of x:

x <- 1:3
x <- structure(x, class = "myclass")
tracemem(x)
#> [1] "<0x557799ebeb88>"

print(x)
#> [1] 1 2 3
#> attr(,"class")
#> [1] "myclass"

Hadley suggests, in his Advanced R book, that one could use NextMethod() to prevent creating a copy in a subset method. I have spent some time thinking on how to prevent creating a copy when printing an S3 object with an attribute, but I couldn't figure out how to do so.

In fact, I just tested printing a tibble and a copy is also created:

z <- tibble::tibble(x = 1, y = 2)
tracemem(z)
#> [1] "<0x55e2aa368c48>"

print(z)
#> tracemem[0x55e2aa368c48 -> 0x55e2aa339e48]: lapply tbl_subset_row [.tbl_df [ do.call head.data.frame head as.data.frame tbl_format_setup.tbl tbl_format_setup_ tbl_format_setup format.tbl format writeLines print.tbl
#> # A tibble: 1 x 2
#>       x     y
#>   <dbl> <dbl>
#> 1     1     2

But a data.table doesn't:

z <- data.table::data.table(x = 1, y = 2)
tracemem(z)
#> [1] "<0x55d64b2bd310>"

print(z)
#>    x y
#> 1: 1 2

So I guess that this problem is solvable, but I haven't figured out how...

EDIT:

Ok, I did some reading on print.data.frame and it solves this problem by converting the data.frame to a matrix and then printing this matrix. This is a very clever solution, and I guess data.table uses something similar.

More generally speaking, however, this wouldn't work for some other type of objects (e.g. those that can't be formatted as a matrix, for example). Take this one:

z <- list(a = data.frame(a = 1, b = 2), b = data.frame(c = 3, d = 4))
z <- structure(z, class = "myotherclass")
tracemem(z)
#> [1] "<0x56298a95fd48>"

print(z)
#> $a
#>   a b
#> 1 1 2
#> 
#> $b
#>   c d
#> 1 3 4
#> 
#> attr(,"class")
#> [1] "myotherclass"

print.myotherclass <- function(x, ...) {
  print(unclass(x), ...)
  return(invisible(x))
}

print(z)
#> tracemem[0x56298a95fd48 -> 0x56298aaa58a8]: print print.myotherclass print
#> $a
#>   a b
#> 1 1 2
#> 
#> $b
#>   c d
#> 1 3 4

How'd you do it in this case?

dhersz
  • 525
  • 2
  • 8

1 Answers1

3

What about using something like print(x[seq_along(x)]) in your print method? It avoids the copy it seems and attributes persist.

z <- list(a = data.frame(a = 1, b = 2), b = data.frame(c = 3, d = 4))
z <- structure(z, class = "myotherclass")
tracemem(z)

print.myotherclass <- function(x, ...) {
  print(x[seq_along(x)])
  return(invisible(x))
}

y <- print(z)
#> $`a`
#> a b
#> 1 1 2
#> 
#> $b
#> c d
#> 1 3 4

attributes(y)
#> $`names`
#> [1] "a" "b"
#> 
#> $class
#> [1] "myotherclass"
bobbel
  • 1,983
  • 6
  • 21
  • That does the trick! I had thought about something similar, but your answer is way more elegant. I wonder if it is possible to conditionally hide only a few attributes, instead of all of them, but well, you did anwer the question I originally asked! – dhersz Mar 05 '21 at 21:40
  • I guess you could add a second print statement in the function to show only a subset of the attributes, something like `print(attr(x, 'class'))`. Although that isn't really conditioned on anything. – bobbel Mar 08 '21 at 07:04