R语言如何判断函数是否为S3方法并查找其对应的泛型函数
需求背景
需要实现两个功能函数:
HasGeneric(AMethod):判断传入的函数对象AMethod是否为某个S3泛型的重写方法FindGeneric(AMethod):若目标函数是S3重写方法,直接返回其关联的泛型名称
最初的实现思路是解析函数名:S3方法官方推荐采用泛型名.类名的命名规则,比如print是内置泛型,print.aov是针对S3类aov重写的print方法。但这个思路存在明显缺陷:
- 字符串拆分存在歧义,比如dplyr包中的
all.equal.data.frame,无法通过简单按点拆分确定泛型名的边界 泛型名.类名的命名规则不是强制要求,R支持注册任意命名的函数作为S3方法,示例如下:
a <- 1:5 class(a) <- "SomeTestClassOfMineNotToBeConfused" # 注册任意命名的函数为print的S3重写方法 thisismyaprinter <- function(x) cat("I am A okay") .S3method("print", "SomeTestClassOfMineNotToBeConfused", thisismyaprinter) # 注册为`print`的重写方法 methods(class = "SomeTestClassOfMineNotToBeConfused") # 返回的方法列表中包含`print` #> [1] coerce initialize print show slotsFromS3 #> see '?methods' for accessing help and source code print(a) # 测试调用正常 #> I am A okay
普通1:5向量的默认打印效果作为对照:
print(a <- 1:5) #> [1] 1 2 3 4 5
已尝试方案的缺陷
R内置的S3方法查询工具都要求预先传入泛型名或类名,无法适配泛型、类均未知的判断场景。之前尝试的遍历匹配方案逻辑如下:
- 遍历
.knownS3Generics中记录的所有已知S3泛型 - 对每个泛型调用
methods()获取其关联的所有S3方法列表 - 在返回的方法列表中匹配目标函数
这个方案存在两个明显问题:
- 执行效率极低,需要遍历所有已知泛型全量拉取方法列表
- 无法正确识别非标准命名的注册S3方法:以上述自定义打印方法为例,
methods(print)的返回结果中会出现按命名规则生成的print.SomeTestClassOfMineNotToBeConfused*条目,但环境中实际不存在这个函数对象,直接调用会抛出找不到函数的错误:
print.SomeTestClassOfMineNotToBeConfused(a) #> Error in print.SomeTestClassOfMineNotToBeConfused(a): konnte Funktion "print.SomeTestClassOfMineNotToBeConfused" nicht finden
可行实现方案
核心思路是直接遍历所有已加载命名空间的S3方法注册表,通过对象严格匹配定位目标方法,再反向校验确定泛型边界,完全不依赖方法的命名规则。
# 辅助函数:判断指定名称的函数是否为S3泛型 isS3Generic <- function(fname) { f <- get0(fname, mode = "function") if (is.null(f)) return(FALSE) # 识别标准S3泛型:函数首行调用UseMethod func_body <- body(f) if (is.call(func_body) && identical(func_body[[1]], quote(UseMethod))) { return(TRUE) } # 识别内置已知S3泛型、组泛型 if (fname %in% .knownS3Generics) return(TRUE) return(FALSE) } FindGeneric <- function(fun) { if (!is.function(fun)) stop("入参必须为函数对象") # 遍历所有已加载命名空间的S3方法注册表 for (ns_name in loadedNamespaces()) { ns_env <- asNamespace(ns_name) s3_method_table <- get0(".__S3MethodsTable__.", envir = ns_env, ifnotfound = NULL) if (is.null(s3_method_table)) next # 遍历注册表匹配目标函数 for (entry_name in ls(s3_method_table, all.names = TRUE)) { entry_fun <- get(entry_name, envir = s3_method_table, inherits = FALSE) if (identical(entry_fun, fun)) { # 逐段拆分注册名,反向校验确定泛型边界(解决泛型名带点的歧义问题) name_parts <- strsplit(entry_name, "\\.")[[1]] for (i in seq_along(name_parts)) { candidate_generic <- paste(name_parts[seq_len(i)], collapse = ".") if (isS3Generic(candidate_generic)) { return(candidate_generic) } } } } } # 未匹配到则返回NULL return(NULL) } HasGeneric <- function(fun) { !is.null(FindGeneric(fun)) }
效果测试
# 测试1:非标准命名的自定义S3方法 a <- 1:5 class(a) <- "SomeTestClassOfMineNotToBeConfused" thisismyaprinter <- function(x) cat("I am A okay") .S3method("print", "SomeTestClassOfMineNotToBeConfused", thisismyaprinter) HasGeneric(thisismyaprinter) #> [1] TRUE FindGeneric(thisismyaprinter) #> [1] "print" # 测试2:泛型名带点的标准S3方法 FindGeneric(all.equal.data.frame) #> [1] "all.equal" # 测试3:普通自定义函数(非S3方法) normal_fun <- function(x) x + 1 HasGeneric(normal_fun) #> [1] FALSE FindGeneric(normal_fun) #> NULL
注意:上述实现默认只匹配当前已加载到内存中的命名空间注册的S3方法,如果需要检测未加载包中的方法,可以先加载对应包后再调用,或扩展逻辑遍历所有已安装包的命名空间。
该方案相比之前的遍历方案效率更高,且可以识别所有合法注册的S3方法,不受方法命名规则的限制,也解决了泛型名带点的拆分歧义问题。
内容的提问来源于stack exchange,提问作者Jan
相关产品推荐
相关产品推荐

