如何设置接受函数调用者提供的参数的装饰器?

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,则可能需要为其编写一个插件。