2

So I am working on a Customer class that is supposed to be wrapper for some other classes that retrieve information about a specific customer from a server and from online, as below.

class Customer:
    def __init__(self, name):
        self.name = name

    @property
    @lru_cache()
    def online_info(self):
       print('retrieving customer online info')
       return Online().search_result(for=self)

    @property
    @lru_cache()
    def server_info(self):
      print('retrieving customer server info')
      return Server().search_result(for=self)

The online and server calls have to be @property decorated. The problem I am facing is when trying to cache the online_info and server_info calls. The cache would somehow have to be at a class level so that even if a news customer is instantiated, the lru_cache wold remember previous calls from other instantiations for the same name call. Note my print statements. This is the behavious I am trying to achieve:

>>> cutomer1 = Customer('John')
>>> customer1.online_info
retrieving customer online info
John Smith has the following info bla bla bla ....

>>> cutomer2 = Customer('John')
>>> customer2.online_info # this one will not be calling the function, lru_cache will return the value saved from customer1.online_info
John Smith has the following info bla bla bla ....

Can someone explain how I achieve this behaviour? Is this possible?

Aran-Fey
  • 39,665
  • 11
  • 104
  • 149
callmeGuy
  • 944
  • 2
  • 11
  • 28
  • What if I do `Customer('not John').online_info`? Should that also use the cached value? – Aran-Fey Oct 29 '18 at 13:16
  • @Aran-Fey in that case no, because 'no John' was never called before – callmeGuy Oct 29 '18 at 13:17
  • You can't use `lru_cache` on properties, or at least, there is *no point in using `lru_cache` there* even if you got your decorator order right (which is incorrect as it is now). `lru_cache` works by looking at the *arguments to a function*, and here all you have is... nothing, as properties don't take arguments. – Martijn Pieters Oct 29 '18 at 13:18
  • @Aran-Fey if however you would have another Customer('no John').online info, after initial 'no John' then the values for the second 'no John' will be pulled from the cache and not from the property call – callmeGuy Oct 29 '18 at 13:19
  • @MartijnPieters That's not right. There's the `self` argument. The code works perfectly fine if the decorators are swapped. – Aran-Fey Oct 29 '18 at 13:20
  • @MartijnPieters so lru_cache goes after property. I just fixed that. – callmeGuy Oct 29 '18 at 13:32
  • Use `methodtools.lru_cache` for methods. Use `cached_property` for property. Use `ring.lru` for methods/property with expirations. – youknowone May 05 '19 at 13:32

1 Answers1

2

Instead of caching the property values on the class, I would recommend re-using the same Customer instance for each "John", so that

>>> Customer('John') is Customer('John')
True

This would make Customer a singleton of sorts. Singleton implementations can be found aplenty in this question: Creating a singleton in Python. Borrowing one of those implementations gives us a pseudo-singleton metaclass like this:

class NameSingleton(type):
    def __init__(cls, *args, **kwargs):
        cls._instances = {}

    def __call__(cls, name, *args, **kwargs):
        try:
            return cls._instances[name]
        except KeyError:
            instance = super().__call__(name, *args, **kwargs)
            cls._instances[name] = instance
            return instance

Use this as the metaclass for Customer and you're done:

class Customer(metaclass=NameSingleton):
    def __init__(self, name):
        self.name = name

    ...

Demo:

>>> Customer('John') is Customer('John')
True
>>> Customer('John') is Customer('not John')
False
Aran-Fey
  • 39,665
  • 11
  • 104
  • 149
  • that is an interesing built. However, I am a bit reluctant to use this code because up the chain I am making multiple Customers from a pandas dataframe, then calling the server_info and online_info on multiple threads and I afraid that with a singleton and multithreading, they might stunble on each other – callmeGuy Oct 29 '18 at 14:00
  • @callmeGuy Well, the worst thing that could happen is that two threads create a John at the same time, and end up with two different John instances. Every caching mechanism has that problem though, whether you're caching `Customer` instances or property values. If it's a problem, you can wrap the `Customer` constructor in a [`threading.Lock`](https://docs.python.org/3/library/threading.html#threading.Lock). – Aran-Fey Oct 29 '18 at 14:06
  • so if i am creating 2 Customers in the same time and they happen to be of the same value, then the cache will be useless. Is there such thing as a global cache? – callmeGuy Oct 29 '18 at 14:10
  • @callmeGuy This *is* a global cache, but it's not thread-safe. It just needs a lock. – Aran-Fey Oct 29 '18 at 14:14