如何设置接受函数调用者提供的参数的装饰器?
How to set up decorator that takes in argument provided by the function caller?
我有几个函数可以处理字符串。虽然它们采用不同类型的参数,但它们都采用一个名为 tokenizer_func
(默认为 str.split
)的通用参数,该参数基本上根据提供的函数将输入字符串拆分为标记列表。然后在每个函数中修改返回的字符串列表。由于 tokenizer_func
似乎是一个常见的参数,并且是所有函数中出现的第一行代码,我想知道使用装饰器来装饰字符串修改函数是否会更容易。基本上,装饰器会采用 tokenizer_func
,将其应用于传入的字符串并调用适当的字符串修改函数。
Edit-2
我找到了解决方案(也许是 hacky?):
def tokenize(f):
def _split(text, tokenizer=SingleSpaceTokenizer()):
return tokenizer.decode(f(tokenizer.encode(text)))
return _split
@tokenize
def change_first_letter(token_list, *_):
return [random.choice(string.ascii_letters) + token[1:] for token in token_list]
这样我就可以调用 change_first_letter(text)
来使用默认分词器,调用 change_first_letter(text, new_tokenizer)
来使用 new_tokenizer
。如果有更好的方法,请告诉我。
编辑-1:
在查看了对这个问题的第一个回复后,我想我可以概括这个问题,我可以更好地处理更多涉及的分词器。具体来说,我现在有这个:
class Tokenizer(ABC):
"""
Base class for Tokenizer which provides the encode and decode methods
"""
def __init__(self, tokenizer: Any) -> None:
self.tokenizer = tokenizer
@abstractmethod
def encode(self, text: str) -> List[str]:
"""
Tokenize a string into list of strings
:param datum: Text to be tokenized
:return: List of tokens
"""
@abstractmethod
def decode(self, token_list : List[str]) -> str:
"""
Creates a string from a tokens list using the tokenizer
:param data: List of tokens
:return: Reconstructed string from token list
"""
def encode_many(self, texts: List[str]) -> List[List[str]]:
"""
Encode multiple strings
:param data: List of strings to be tokenized
:return: List of tokenized strings
"""
return [self.encode(text) for text in texts]
def decode_many(self, token_lists: List[List[str]]) -> List[str]:
"""
Decode multiple strings
:param data: List of tokenized strings
:return: List of reconstructed strings
"""
return [self.decode(token_list) for token_list in token_lists]
class SingleSpaceTokenizer(Tokenizer):
"""
Simple tokenizer that just splits a string on a single space using str.split
"""
def __init__(self, tokenizer=None) -> None:
super(SingleSpaceTokenizer, self).__init__(tokenizer)
def encode(self, text: str) -> List[str]:
return text.split()
def decode(self, token_list: List[str]) -> str:
return ' '.join(token_list)
我写了一个基于回复和搜索的装饰函数:
def tokenize(tokenizer):
def _tokenize(f):
def _split(text):
response = tokenizer.decode(f(tokenizer.encode(text)))
return response
return _split
return _tokenize
现在我可以做到了:
@tokenize(SingleSpaceTokenizer())
def change_first_letter(token_list):
return [random.choice(string.ascii_letters) + token[1:] for token in token_list]
这没有任何问题。如何让我作为用户想要使用另一个分词器:
class AtTokenizer(Tokenizer):
def __init__(self, tokenizer=None):
super(AtTokenizer, self).__init__(tokenizer)
def encode(self, text):
return text.split('@')
def decode(self, token_list):
return '@'.join(token_list)
new_tokenizer = AtTokenizer()
如何通过传递此 new_tokenzer
来调用我的文本函数?
我发现我可以这样称呼 new_tokenizer
:
tokenize(new_tokenizer)(change_first_letter)(text)
如果我不要修饰change_first_letter
函数。这看起来很乏味吗?有没有办法更简洁地做到这一点?
原文:
下面是两个这样的函数的例子(第一个是虚拟函数):
def change_first_letter(text: str, tokenizer_func: Callable[[str], List[str]]=str.split) -> str:
words = tokenizer_func(text)
return ' '.join([random.choice(string.ascii_letters) + word[1:] for word in words])
def spellcheck(text: str, tokenizer_func: Callable[[str], List[str]]=str.split) -> str:
words = tokenizer_func(text)
return ' '.join([SpellChecker().correction(word) for word in words])
对于这两个函数,第一行是应用分词器函数。如果 tokenizer 函数总是 str.split
,我可以创建一个装饰器来为我做这个:
def tokenize(func):
def _split(text):
return func(text.split())
return _split
然后我可以用 @tokenize
修饰其他函数,它就可以工作了。在这种情况下,函数将直接采用 List[str]
。但是,tokenizer_func
是由函数调用者提供的。我如何将其传递给装饰器?这能做到吗?
装饰器的 @
语法简单地将行的其余部分计算为一个函数,在紧接着定义的函数上调用该函数,并替换它。通过使 'decorator with arguments' (tokenize()
) return 成为常规装饰器,该装饰器将包含原始功能。
def tokenize(method):
def decorator(function):
def wrapper(text):
return function(method(text))
return wrapper
return decorator
@tokenize(method=str.split)
def strfunc(text):
print(text)
strfunc('The quick brown fox jumped over the lazy dog')
# ['The', 'quick', 'brown', 'fox', 'jumped', 'over', 'the', 'lazy', 'dog']
这个问题是,如果你要分配一个默认参数(例如 def tokenize(method=str.split):
),你仍然需要在应用装饰器时将其作为函数调用:
@tokenize()
def strfunc(text):
...
所以最好不要提供默认参数,或者找到解决此问题的创造性方法。一种可能的解决方案是根据装饰器是使用函数调用(在这种情况下装饰该函数)还是字符串(在这种情况下调用 str.split()
)来更改装饰器的行为:
def tokenize(method):
def decorator(arg):
# if argument is a function, then apply another decorator
# otherwise, assume str.split()
if type(arg) == type(tokenize):
def wrapper(text):
return arg(method(text))
return wrapper
else:
return method(str.split(arg))
return decorator
这应该允许以下两项:
@tokenize # default to str.split
def strfunc(text):
...
@tokenize(str.split) # or another function of your choice
def strfunc(text):
...
这样做的缺点是它有点老套(玩 type()
总是这样,这里特别省钱的地方是所有函数都是函数;你可以看看你是否可以做一个检查因为“是可调用的”,如果你希望它也适用于 类,也许),并且很难弄清楚哪些参数在 tokenize()
中做了什么 - 因为它们根据如何改变目的该方法被调用。
def tokenize(tokenizer):
def _tokenize(f):
def _split(text, tokenizer=tokenizer):
response = tokenizer.decode(f(tokenizer.encode(text)))
return response
return _split
return _tokenize
这样您就可以通过两种方式调用您的 change_first_letter
:
change_first_letter(text)
使用默认分词器
change_first_letter(text, new_tokenizer)
使用 new_tokenizer
MyPy 不喜欢装饰器更改函数接受的参数,因此如果您使用的是 MyPy,则可能需要为其编写一个插件。
我有几个函数可以处理字符串。虽然它们采用不同类型的参数,但它们都采用一个名为 tokenizer_func
(默认为 str.split
)的通用参数,该参数基本上根据提供的函数将输入字符串拆分为标记列表。然后在每个函数中修改返回的字符串列表。由于 tokenizer_func
似乎是一个常见的参数,并且是所有函数中出现的第一行代码,我想知道使用装饰器来装饰字符串修改函数是否会更容易。基本上,装饰器会采用 tokenizer_func
,将其应用于传入的字符串并调用适当的字符串修改函数。
Edit-2
我找到了解决方案(也许是 hacky?):
def tokenize(f):
def _split(text, tokenizer=SingleSpaceTokenizer()):
return tokenizer.decode(f(tokenizer.encode(text)))
return _split
@tokenize
def change_first_letter(token_list, *_):
return [random.choice(string.ascii_letters) + token[1:] for token in token_list]
这样我就可以调用 change_first_letter(text)
来使用默认分词器,调用 change_first_letter(text, new_tokenizer)
来使用 new_tokenizer
。如果有更好的方法,请告诉我。
编辑-1:
在查看了对这个问题的第一个回复后,我想我可以概括这个问题,我可以更好地处理更多涉及的分词器。具体来说,我现在有这个:
class Tokenizer(ABC):
"""
Base class for Tokenizer which provides the encode and decode methods
"""
def __init__(self, tokenizer: Any) -> None:
self.tokenizer = tokenizer
@abstractmethod
def encode(self, text: str) -> List[str]:
"""
Tokenize a string into list of strings
:param datum: Text to be tokenized
:return: List of tokens
"""
@abstractmethod
def decode(self, token_list : List[str]) -> str:
"""
Creates a string from a tokens list using the tokenizer
:param data: List of tokens
:return: Reconstructed string from token list
"""
def encode_many(self, texts: List[str]) -> List[List[str]]:
"""
Encode multiple strings
:param data: List of strings to be tokenized
:return: List of tokenized strings
"""
return [self.encode(text) for text in texts]
def decode_many(self, token_lists: List[List[str]]) -> List[str]:
"""
Decode multiple strings
:param data: List of tokenized strings
:return: List of reconstructed strings
"""
return [self.decode(token_list) for token_list in token_lists]
class SingleSpaceTokenizer(Tokenizer):
"""
Simple tokenizer that just splits a string on a single space using str.split
"""
def __init__(self, tokenizer=None) -> None:
super(SingleSpaceTokenizer, self).__init__(tokenizer)
def encode(self, text: str) -> List[str]:
return text.split()
def decode(self, token_list: List[str]) -> str:
return ' '.join(token_list)
我写了一个基于回复和搜索的装饰函数:
def tokenize(tokenizer):
def _tokenize(f):
def _split(text):
response = tokenizer.decode(f(tokenizer.encode(text)))
return response
return _split
return _tokenize
现在我可以做到了:
@tokenize(SingleSpaceTokenizer())
def change_first_letter(token_list):
return [random.choice(string.ascii_letters) + token[1:] for token in token_list]
这没有任何问题。如何让我作为用户想要使用另一个分词器:
class AtTokenizer(Tokenizer):
def __init__(self, tokenizer=None):
super(AtTokenizer, self).__init__(tokenizer)
def encode(self, text):
return text.split('@')
def decode(self, token_list):
return '@'.join(token_list)
new_tokenizer = AtTokenizer()
如何通过传递此 new_tokenzer
来调用我的文本函数?
我发现我可以这样称呼 new_tokenizer
:
tokenize(new_tokenizer)(change_first_letter)(text)
如果我不要修饰change_first_letter
函数。这看起来很乏味吗?有没有办法更简洁地做到这一点?
原文:
下面是两个这样的函数的例子(第一个是虚拟函数):
def change_first_letter(text: str, tokenizer_func: Callable[[str], List[str]]=str.split) -> str:
words = tokenizer_func(text)
return ' '.join([random.choice(string.ascii_letters) + word[1:] for word in words])
def spellcheck(text: str, tokenizer_func: Callable[[str], List[str]]=str.split) -> str:
words = tokenizer_func(text)
return ' '.join([SpellChecker().correction(word) for word in words])
对于这两个函数,第一行是应用分词器函数。如果 tokenizer 函数总是 str.split
,我可以创建一个装饰器来为我做这个:
def tokenize(func):
def _split(text):
return func(text.split())
return _split
然后我可以用 @tokenize
修饰其他函数,它就可以工作了。在这种情况下,函数将直接采用 List[str]
。但是,tokenizer_func
是由函数调用者提供的。我如何将其传递给装饰器?这能做到吗?
装饰器的 @
语法简单地将行的其余部分计算为一个函数,在紧接着定义的函数上调用该函数,并替换它。通过使 'decorator with arguments' (tokenize()
) return 成为常规装饰器,该装饰器将包含原始功能。
def tokenize(method):
def decorator(function):
def wrapper(text):
return function(method(text))
return wrapper
return decorator
@tokenize(method=str.split)
def strfunc(text):
print(text)
strfunc('The quick brown fox jumped over the lazy dog')
# ['The', 'quick', 'brown', 'fox', 'jumped', 'over', 'the', 'lazy', 'dog']
这个问题是,如果你要分配一个默认参数(例如 def tokenize(method=str.split):
),你仍然需要在应用装饰器时将其作为函数调用:
@tokenize()
def strfunc(text):
...
所以最好不要提供默认参数,或者找到解决此问题的创造性方法。一种可能的解决方案是根据装饰器是使用函数调用(在这种情况下装饰该函数)还是字符串(在这种情况下调用 str.split()
)来更改装饰器的行为:
def tokenize(method):
def decorator(arg):
# if argument is a function, then apply another decorator
# otherwise, assume str.split()
if type(arg) == type(tokenize):
def wrapper(text):
return arg(method(text))
return wrapper
else:
return method(str.split(arg))
return decorator
这应该允许以下两项:
@tokenize # default to str.split
def strfunc(text):
...
@tokenize(str.split) # or another function of your choice
def strfunc(text):
...
这样做的缺点是它有点老套(玩 type()
总是这样,这里特别省钱的地方是所有函数都是函数;你可以看看你是否可以做一个检查因为“是可调用的”,如果你希望它也适用于 类,也许),并且很难弄清楚哪些参数在 tokenize()
中做了什么 - 因为它们根据如何改变目的该方法被调用。
def tokenize(tokenizer):
def _tokenize(f):
def _split(text, tokenizer=tokenizer):
response = tokenizer.decode(f(tokenizer.encode(text)))
return response
return _split
return _tokenize
这样您就可以通过两种方式调用您的 change_first_letter
:
change_first_letter(text)
使用默认分词器change_first_letter(text, new_tokenizer)
使用new_tokenizer
MyPy 不喜欢装饰器更改函数接受的参数,因此如果您使用的是 MyPy,则可能需要为其编写一个插件。