<jupyter_start><jupyter_text>Python 预缓存的 Property 修饰器简单实现 > 创建时间：2021-03-19 这份简短笔记，我们会讨论预缓存 Python Property (类属性) 的简单实现。在运行程序时，特别是对于耗时但不耗内存、以后需要经常取用的计算，我们会希望找个内存或硬盘空间储存起来。为了方便起见，我们只讨论借用内存的方法。假设现在的问题是，我们要计算 $c = a+b, d = a^2$。为了调用的便利，$a, b$ 两个变量 (作为常数) 作为 property。 但麻烦之处在于，$a, b$ 的值并不容易求，求完之后对内存的消耗却又不大。(嘛先不要追问为什么求个 1+1 要这么复杂)$$\begin{align}a &= \sum_{n = 1}^{\infty} \frac{1}{2^n} \simeq \sum_{n = 1}^{1000} \frac{1}{2^n} \\b &= \int_0^1 3 x^2 \, \mathrm{d} x \simeq \sum_{n = 0}^{5000} \frac{3 n^2}{5000^3}\end{align}$$同时，我们也不清楚末端用户是否需要求 $c$ (需要同时计算 $a$, $b$)，还是需要求 $d$ (只需要计算 $a$ 即可)。<jupyter_code>def get_a():
    a = 0
    for n in range(1, 1001):
        a += 1 / 2**n
    return a
def get_b():
    b = 0
    for n in range(5001):
        b += 3 * n**2 / 5000**3
    return b<jupyter_output><empty_output><jupyter_text>这篇文档讨论四种做法。第一种做法简单但低效；第二、三种做法代码较复杂；第四种代码简单且不会产生多余的计算。作者倾向使用 [第三种](偷懒的做法：将赋值函数嵌入-getter-函数) 与 [第四种](改进的做法：缩减隐含变量的声明) 做法。  即时调用 property 定义方法 最简单粗暴的方法是需要 $a, b$ 时就现场计算。在第一次调用 $a, b$ 时固然需要耗时的计算，但第二次调用仍然会相当费时。<jupyter_code>class Dummy:
    
    @property
    def a(self):
        return get_a()
    
    @property
    def b(self):
        return get_b()
    
    @property
    def c(self):
        return self.a + self.b
    
    @property
    def d(self):
        return self.a**2
dum = Dummy()
dum.c
%%timeit -n 50
dum.c<jupyter_output>2.41 ms ± 172 µs per loop (mean ± std. dev. of 7 runs, 50 loops each)<jupyter_text>一般的 property 做法 为了避免多余的计算，一般的方法是，需要首先在 `__init__` 中声明两个隐含变量 `_a`, `_b` 以保存结果。在使用 `a`, `b` 两个 property 之前，先要使用 setter 函数作 $a, b$ 的计算并分别保存到 `_a`, `_b` 中；随后再用 getter 函数调用它们。<jupyter_code>class General:
    
    def __init__(self):
        self._a = NotImplemented
        self._b = NotImplemented
    
    @property
    def a(self):
        return self._a
    
    @a.setter
    def a(self, val):
        self._a = val
    
    @property
    def b(self):
        return self._a
    
    @b.setter
    def b(self, val):
        self._b = val
    
    @property
    def c(self):
        return self.a + self.b
    
    @property
    def d(self):
        return self.a**2<jupyter_output><empty_output><jupyter_text>如果没有预先使用 setter 函数，就会碰到下面这种尴尬的情况：<jupyter_code>gen = General()
gen.c<jupyter_output><empty_output><jupyter_text>因此，正确的调用方式是<jupyter_code>gen = General()
gen.a, gen.b = get_a(), get_b()
gen.c, gen.d<jupyter_output><empty_output><jupyter_text>上面的步骤是耗时的，但随后当要调用 `a`, `b` 变量时，就会快捷很多：<jupyter_code>%%timeit -n 50
gen.c<jupyter_output>265 ns ± 28 ns per loop (mean ± std. dev. of 7 runs, 50 loops each)<jupyter_text>但这里有一个问题：$a, b$ 的值实际上可以看作常数；如果末端用户真正希望得到的是 $d = a^2$ 而非 $c = a + b$，那么实际上用户不需要 $b$，自然也就不需要对其花时间赋值了。决定是否要对 $b$ 赋值的任务由此交给末端用户，这会造成一些困扰。  偷懒的做法：将赋值函数嵌入 getter 函数 如果这个任务交给程序编写者，那么一种最简单的实现方式是把赋值函数嵌入到 getter 函数中：- 如果 $a$ 的值已经被计算过，那么就从缓存空间 `_a` 取出该值；- 如果 $a$ 被调用前没有被计算过，那么就计算该值并放入缓存 `_a`。<jupyter_code>class Improved:
    
    def __init__(self):
        self._a = NotImplemented
        self._b = NotImplemented
    
    @property
    def a(self):
        if self._a is NotImplemented:
            self._a = get_a()
        return self._a
    
    @property
    def b(self):
        if self._b is NotImplemented:
            self._b = get_b()
        return self._b
    
    @property
    def c(self):
        return self.a + self.b
    
    @property
    def d(self):
        return self.a**2<jupyter_output><empty_output><jupyter_text>如果末端用户只需要求 $d = a^2$，那么耗费时间的关键步就只有 $a$ 的计算；缓存空间 `_b` 就是空的。<jupyter_code>imp = Improved()
print(imp.d)
print(imp._b)<jupyter_output>1.0
NotImplemented<jupyter_text>同时，以后再需要调用 $d$ 时，$a$ 的值也不会再被计算第二次。当然，这种做法的弊端是，用户原则上无权限更改 $a, b$ 的值 (通过更改隐含变量 `_a`, `_b` 是可能的，但这违背了 PEP8 的程序规范)。  改进的做法：缩减隐含变量的声明 但上面的定义仍然有很多冗余。对于每个 property，我们总要声明隐含变量、调用时判断是否缓存空间存在。这两步可以通过改编在 property 修饰器内部增加一段代码方便地实现。这个修饰器我们命名为 `cached_property`：<jupyter_code>def cached_property(f):
    def wrap(*args, **kwargs):
        self = args[0]                                                    # self
        _f = "_" + f.__name__                                             # _a
        if not hasattr(self, _f) or getattr(self, _f) is NotImplemented:  # if self._a is NotImplemented:
            setattr(self, _f, f(*args))                                   # self._a = get_a()
        return getattr(self, _f)                                          # return self._a
    return property(wrap)                                                 # make this wrap a property<jupyter_output><empty_output><jupyter_text>这样之后，不仅代码量减少很多 (调用方式与最简单的 `Dummy` 完全一致)，同时也避免多余重复的计算。<jupyter_code>class Advanced:
    
    @cached_property
    def a(self):
        return get_a()
    
    @cached_property
    def b(self):
        return get_b()
    
    @property
    def c(self):
        return self.a + self.b
    
    @property
    def d(self):
        return self.a**2
adv = Advanced()
print(adv.d)
print(hasattr(adv, "_b"))<jupyter_output>1.0
False
{'fraction_non_alphanumeric': 0.13146595788182375, 'fraction_numerical': 0.01840114496013085, 'mean_word_length': 2.7515337423312882, 'pattern_counts': {'":': 0, '<': 37, '<?xml version=': 0, '>': 38, 'https://': 0, 'lorem ipsum': 0, 'www.': 0, 'xml': 0}, 'pii_count': 0, 'substrings_counts': 0, 'word_list_counts': {'cursed_substrings.json': 9, 'profanity_word_list.json': 0, 'sexual_word_list.json': 0, 'zh_pornsignals.json': 0}}
{'chain_length': '10', 'dir': 'jupyter-structured-clean-dedup', 'id': '267134', 'license': 'MIT', 'n_tokens_mistral': 2584, 'n_tokens_neox': 2717, 'n_words': 397, 'path': 'source/Simple_Notes/cached_property.ipynb', 'repo_name': 'ajz34/ajz34.readthedocs.io', 'size': '4891'}