Skip to content

Instantly share code, notes, and snippets.

@yassu
Created September 3, 2014 07:37
Show Gist options
  • Save yassu/ecd6153fdf2d5c63e92c to your computer and use it in GitHub Desktop.
Save yassu/ecd6153fdf2d5c63e92c to your computer and use it in GitHub Desktop.
from random import randint
from inspect import isfunction
def get_random():
return randint(0, 10**10)
class Cacher:
pass
DEFAULT_CACHER = Cacher()
def memoize(func, cacher=DEFAULT_CACHER):
"""
cacher: instance that has no __slot__
"""
def _cache(*args, **kw):
cached_varname = '__cache{}'.format(id(func.__call__))
cached_varname += '__'
## several-times computation
if hasattr(cacher, cached_varname) is True:
return getattr(cacher, cached_varname)
## first computation
result = func(*args, **kw)
setattr(cacher, cached_varname, result)
return result
return _cache
def cached_property(func, cacher=DEFAULT_CACHER):
@property
@memoize
def _cache_property(*args, **kw):
return func(*args, **kw)
return _cache_property
from sys import path
path.append('src')
from cache import memoize, cached_property
def only_func_test():
global x
x = 1
@memoize
def multiple():
global x
x *= 2
return x
for i in range(10):
res = multiple()
print('res: ' + str(res))
assert(res == 2)
def class_simple_attr_test():
class TwoTimes():
def __init__(self):
self._value = 1
@memoize
def multiple(self):
self._value *= 2
return self._value
cls = TwoTimes()
for _ in range(10):
assert(cls.multiple() == 2)
def cached_property_simple_class_test():
class TwoTimes():
def __init__(self):
self._value = 1
@cached_property
def multiple(self):
self._value *= 2
return self._value
cls = TwoTimes()
for _ in range(10):
assert(cls.multiple == 2)
def cached_property_same_cacher_and_method_test():
class TwoTimes():
def __init__(self, value):
self._value = value
@cached_property
def multiple(self):
self._value *= 2
return self._value
t1 = TwoTimes(1)
assert(t1.multiple == 2)
# multiple of default casher is equal to 2
t2 = TwoTimes(5)
assert(t2.multiple == 10)
def cached_property_different_class_and_same_method_test():
class TwoTimes():
def __init__(self, value):
self._value = value
@cached_property
def multiple(self):
self._value *= 2
return self._value
class ThreeTimes():
def __init__(self, value):
self._value = value
@cached_property
def multiple(self):
self._value *= 3
return self._value
t1 = TwoTimes(1)
assert(t1.multiple == 2)
# multiple of default casher is equal to 2
t2 = ThreeTimes(5)
assert(t2.multiple == 15)
def cached_property_same_class_and_method_test():
class TwoTimes():
def __init__(self, value):
self._value = value
@cached_property
def multiple(self):
self._value *= 2
return self._value
t1 = TwoTimes(1)
assert(t1.multiple == 2)
del(t1)
t2 = TwoTimes(5)
assert(t2.multiple == 10)
@tk0miya
Copy link

tk0miya commented Sep 3, 2014

テストコードを実行してみたところ、エラーが出ました。

$ git clone https://gist.github.com/yassu/ecd6153fdf2d5c63e92c
Cloning into 'ecd6153fdf2d5c63e92c'...
remote: Counting objects: 4, done.
remote: Compressing objects: 100% (4/4), done.
remote: Total 4 (delta 0), reused 0 (delta 0)
Unpacking objects: 100% (4/4), done.
Checking connectivity... done.
$ cd ecd6153fdf2d5c63e92c
$ nosetests
..FF.F
======================================================================
FAIL: cache_test.cached_property_simple_class_test
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/tkomiya/work/cache-test/lib/python2.7/site-packages/nose/case.py", line 197, in runTest
    self.test(*self.arg)
  File "/Users/tkomiya/work/ecd6153fdf2d5c63e92c/cache_test.py", line 47, in cached_property_simple_class_test
    assert(cls.multiple == 2)
AssertionError

======================================================================
FAIL: cache_test.cached_property_same_cacher_and_method_test
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/tkomiya/work/cache-test/lib/python2.7/site-packages/nose/case.py", line 197, in runTest
    self.test(*self.arg)
  File "/Users/tkomiya/work/ecd6153fdf2d5c63e92c/cache_test.py", line 64, in cached_property_same_cacher_and_method_test
    assert(t2.multiple == 10)
AssertionError

======================================================================
FAIL: cache_test.cached_property_same_class_and_method_test
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/tkomiya/work/cache-test/lib/python2.7/site-packages/nose/case.py", line 197, in runTest
    self.test(*self.arg)
  File "/Users/tkomiya/work/ecd6153fdf2d5c63e92c/cache_test.py", line 107, in cached_property_same_class_and_method_test
    assert(t2.multiple == 10)
AssertionError

----------------------------------------------------------------------
Ran 6 tests in 0.007s

FAILED (failures=3)

python2.7 で実行しているのが問題かと気になり、python3.3 で実行してみたところ、
エラー件数は減るものの、依然としてエラーが出ます。

..F..F
======================================================================
FAIL: cache_test.cached_property_simple_class_test
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/tkomiya/work/cache-test/lib/python3.3/site-packages/nose/case.py", line 198, in runTest
    self.test(*self.arg)
  File "/Users/tkomiya/work/ecd6153fdf2d5c63e92c/cache_test.py", line 47, in cached_property_simple_class_test
    assert(cls.multiple == 2)
AssertionError

======================================================================
FAIL: cache_test.cached_property_same_class_and_method_test
----------------------------------------------------------------------
Traceback (most recent call last):
  File "/Users/tkomiya/work/cache-test/lib/python3.3/site-packages/nose/case.py", line 198, in runTest
    self.test(*self.arg)
  File "/Users/tkomiya/work/ecd6153fdf2d5c63e92c/cache_test.py", line 103, in cached_property_same_class_and_method_test
    assert(t1.multiple == 2)
AssertionError

----------------------------------------------------------------------
Ran 6 tests in 0.006s

FAILED (failures=2)

確認してみたところ、cached_property()@memoize を呼んでいると、
memoize() 関数の引数は常に _cache_property() が渡ってきているように見えます。
そのため、id(func.__call__) は常に同じ値を返しているようです。

こちらの環境だけで発生する問題でしょうか? 確認してみてください。

@tk0miya
Copy link

tk0miya commented Sep 4, 2014

デバッグしてみましたが、やはり _cache_property()@memoize されているようなので、
@memoize を使うことを諦めて作りなおしてみました。
https://gist.github.com/1bd5b7623f77115923ff

memoize() の変更点

  • 引数が同じときだけキャッシュし、引数が異なる場合は計算するようにした (よく用いられる memoize 手法)
  • キャッシュキーを関数名にした (ぶつかる可能性が大いにあるので、id(func) の方がよかったかも...)
  • キャッシュ先をシンプルな辞書に変更
  • functools.wraps() を使って、関数の属性(docname など) を維持するようにした

cached_property() の変更点

  • キャッシュ先を self の属性に変更 (インスタンスが消滅すると一緒に GC されるので省メモリ)
  • functools.wraps() を使って、関数の属性(docname など) を維持するようにした

@tk0miya
Copy link

tk0miya commented Sep 4, 2014

ちなみに、作ってもらった memoize()cached_property() には cacher という引数が定義されていますが、
普通のデコレータの呼び方 (@memoize ) の場合はここを指定することが難しそうです。

引数を取るデコレータを作る場合は

def decorator(*dargs, **dkwargs):
    def internal_decorator(func):
         def wrapper(*args, **kwargs):
             # some process
         return wrapper
    return internal_decorator

のように「デコレータを返す関数」を用意することになります(やたらとややこしいですね)。

参考まで。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment