【问题标题】:Is it okay for decorators to access private members of a class?装饰器可以访问类的私有成员吗?
【发布时间】:2013-09-12 14:54:02
【问题描述】:

我正在编写一个解析 HTML 的类,以便为网页上的个人资料提供接口。它看起来像这样:

class Profile(BeautifulSoup):
    def __init__(self, page_source):
        super().__init__(page_source)

    def username(self):
        return self.title.split(':')[0]

除了更复杂和耗时。因为我知道底层配置文件在Profile 对象的生命周期内不会发生变化,所以我认为这是缓存结果的好地方,以避免重新计算已知值。我用装饰器实现了这个,结果如下:

def cached_resource(method_to_cache):
    def decorator(self, *args, **kwargs):
        method_name = method_to_cache.__name__

        try:
            return self._cache[method_name]
        except KeyError:
            self._cache[method_name] = method_to_cache(self, *args, **kwargs)
            return self._cache[method_name]

    return decorator


class Profile(BeautifulSoup):
    def __init__(self, page_source):
        super().__init__(page_source)
        self._cache = {}

    @cached_resource
    def username(self):
        return self.title.split(':')[0]

当我将此代码提供给 pylint 时,它抱怨 cached_resource 可以访问客户端类的受保护变量。

我意识到公共和私有之间的区别在 Python 中并不是什么大问题,但我仍然很好奇——我在这里做了什么坏事吗?让装饰器依赖与其关联的类的实现细节是不是很糟糕?

编辑:我不清楚 Duncan 的答案中的闭包是如何工作的,所以这可能有点杂乱无章,但这会是一个更简单的解决方案吗?

def cached_resource(method_to_cache):
    def decorator(self, *args, **kwargs):
    method_name = method_to_cache.__name__

    try:
        return self._cache[method_name]
    except KeyError:
        self._cache[method_name] = method_to_cache(self, *args, **kwargs)
    except AttributeError:
        self._cache = {}
        self._cache[method_name] = method_to_cache(self, *args, **kwargs)
    finally:
        return self._cache[method_name]

return decorator

【问题讨论】:

  • 虽然您没有将 username 建模为私有函数...它在您的实现中是公开的
  • 是的——它抱怨cached_resource 可以访问私有变量self._cache。一方面,这对我来说是有意义的,因为除非我通读它,否则我无法知道 cached_resource 依赖于它的客户端类具有名为 _cache 的属性。另一方面,说像用作缓存的字典一样低级和丑陋的东西应该是公共的,这似乎很奇怪。

标签: python oop styles decorator information-hiding


【解决方案1】:

这有点代码味道,我想我会同意 pylint 的观点,尽管它很主观。

您的装饰器看起来像是一个通用的装饰器,但它与类的内部实现细节相关联。如果您尝试从另一个类中使用它,则如果没有在 __init__ 中初始化 _cache,它将无法工作。我不喜欢的链接是类和装饰器之间共享一个名为“_cache”的属性的知识。

您可以将_cache 的初始化移出__init__ 并移到装饰器中。我不知道这是否有助于安抚 pylint,它仍然需要班级了解并避免使用该属性。这里(我认为)一个更简洁的解决方案是将缓存属性的名称传递给装饰器。那应该彻底打破链接:

def cached_resource(cache_attribute):
  def decorator_factory(method_to_cache):
    def decorator(self, *args, **kwargs):
        method_name = method_to_cache.__name__
        cache = getattr(self, cache_attribute)
        try:
            return cache[method_name]
        except KeyError:
            result = cache[method_name] = method_to_cache(self, *args, **kwargs)
            return result

    return decorator
  return decorator_factory


class Profile(BeautifulSoup):
    def __init__(self, page_source):
        super().__init__(page_source)
        self._cache = {}

    @cached_resource('_cache')
    def username(self):
        return self.title.split(':')[0]

如果你不喜欢大量重复属性名称的装饰器调用,那么:

class Profile(BeautifulSoup):
    def __init__(self, page_source):
        super().__init__(page_source)
        self._cache = {}

    with_cache = cached_resource('_cache')

    @with_cache
    def username(self):
        return self.title.split(':')[0]

编辑: Martineau 认为这可能是矫枉过正。如果您实际上不需要单独访问类中的 _cache 属性(例如,具有缓存重置方法),则可能是这样。在这种情况下,您可以完全在装饰器中管理缓存,但如果您要这样做,则根本不需要实例上的缓存字典,因为您可以将缓存存储在装饰器中并在 @987654328 上存储键@实例:

from weakref import WeakKeyDictionary

def cached_resource(method_to_cache):
    cache = WeakKeyDictionary()
    def decorator(self, *args, **kwargs):
        try:
            return cache[self]
        except KeyError:
            result = cache[self] = method_to_cache(self, *args, **kwargs)
        return result
    return decorator

class Profile(BeautifulSoup):
    def __init__(self, page_source):
        super().__init__(page_source)
        self._cache = {}

    @cached_resource
    def username(self):
        return self.title.split(':')[0]

【讨论】:

  • 虽然我同意你的观点,最好避免类的耦合,但我认为你的装饰器应该只将它自己选择的缓存属性添加到装饰函数中。将其作为变量并拥有工厂的复杂性是不值得的,而且可能是不必要的,IMO。
  • @martineau,我可能会同意你的看法,具体取决于其他情况。 Profile 是否需要清除/重置缓存的方法?如果是这样,类和装饰器都需要访问相同的属性。另一方面,如果从不重置缓存(除非通过创建新的Profile() 实例)使​​用任何属性都可能是矫枉过正,因为装饰器中的局部变量就足够了。
  • 它可能不需要清除/重置缓存的方法,Profile 对象的寿命很短。不过,我不明白如何完全在装饰器中对其进行管理。我总是对嵌套函数中变量的生命周期有点困惑,但是第二个代码 sn-p 中的缓存不会总是为空吗?或者,如果我错了,它不会尝试跨配置文件对象使用相同的缓存吗?如何将cached_resource 中的局部变量绑定到特定的Profile 对象?
  • @PatrickCollins 非常正确,我并没有直接思考。我已经更正了我的代码,因此它为每个实例缓存一个值。
【解决方案2】:

在我看来,你所做的一切都很好。该错误可能是因为 pylint 无法确定 cached_resource 只是通过其内部函数“访问”self._cache,最终 类的方法(由装饰器分配)。

可能值得为此在pylint tracker 上提出问题。静态分析可能很难处理,但当前的行为似乎不对。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2021-06-21
    • 2014-05-30
    • 2013-07-15
    • 2020-01-19
    • 2011-08-04
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多