Как лениво загрузить структуру данных (python) - PullRequest
7 голосов
/ 30 декабря 2010

У меня есть какой-то способ построения структуры данных (скажем, из некоторого содержимого файла):

def loadfile(FILE):
    return # some data structure created from the contents of FILE

Так что я могу делать такие вещи, как

puppies = loadfile("puppies.csv") # wait for loadfile to work
kitties = loadfile("kitties.csv") # wait some more
print len(puppies)
print puppies[32]

В приведенном выше примере я потратил кучу времени на чтение kitties.csv и создание структуры данных, которую я никогда не использовал. Я хотел бы избежать этих потерь без постоянной проверки if not kitties всякий раз, когда я хочу что-то сделать. Я хотел бы иметь возможность сделать

puppies = lazyload("puppies.csv") # instant
kitties = lazyload("kitties.csv") # instant
print len(puppies)                # wait for loadfile
print puppies[32]

Так что, если я никогда не попытаюсь что-либо сделать с kitties, loadfile("kitties.csv") никогда не вызовут.

Есть ли какой-нибудь стандартный способ сделать это?

Поработав с ним немного, я нашел следующее решение, которое, кажется, работает правильно и довольно кратко. Есть ли альтернативы? Есть ли недостатки в использовании этого подхода, о которых я должен помнить?

class lazyload:
    def __init__(self,FILE):
        self.FILE = FILE
        self.F = None
    def __getattr__(self,name):
        if not self.F: 
            print "loading %s" % self.FILE
            self.F = loadfile(self.FILE)
        return object.__getattribute__(self.F, name)

Что может быть еще лучше, если что-то подобное сработает:

class lazyload:
    def __init__(self,FILE):
        self.FILE = FILE
    def __getattr__(self,name):
        self = loadfile(self.FILE) # this never gets called again
                                   # since self is no longer a
                                   # lazyload instance
        return object.__getattribute__(self, name)

Но это не работает, потому что self локально. На самом деле он звонит loadfile каждый раз, когда ты что-то делаешь.

Ответы [ 6 ]

4 голосов
/ 30 декабря 2010

Модуль csv в стандартной библиотеке Python не будет загружать данные до тех пор, пока вы не начнете выполнять итерации по ним, поэтому он на самом деле ленив.

Редактирование: если вам нужно прочитать весь файл для построения структуры данныхИмея сложный объект Lazy load, который проксирует вещи, это излишне.Просто сделайте это:

class Lazywrapper(object):
    def __init__(self, filename):
        self.filename = filename
        self._data = None

    def get_data(self):
        if self._data = None:
            self._build_data()
        return self._data

    def _build_data(self):
        # Now open and iterate over the file to build a datastructure, and
        # put that datastructure as self._data

С помощью вышеуказанного класса вы можете сделать это:

puppies = Lazywrapper("puppies.csv") # Instant
kitties = Lazywrapper("kitties.csv") # Instant

print len(puppies.getdata()) # Wait
print puppies.getdata()[32] # instant

Также

allkitties = kitties.get_data() # wait
print len(allkitties)
print kitties[32]

Если у вас есть лот данных, и вам не нужно загружать все данные, вы также можете реализовать что-то вроде класса, который будет читать файл, пока не найдет собачку по имени «Froufrou», а затем остановится, но в этот момент, вероятно, лучшераз и навсегда зафиксируйте данные в базе данных и получите к ним доступ.

2 голосов
/ 30 декабря 2010

Если вы действительно беспокоитесь об операторе if, у вас есть объект Stateful .

from collections import MutableMapping
class LazyLoad( MutableMapping ):
   def __init__( self, source ):
       self.source= source
       self.process= LoadMe( self )
       self.data= None
   def __getitem__( self, key ):
       self.process= self.process.load()
       return self.data[key]
   def __setitem__( self, key, value ):
       self.process= self.process.load()
       self.data[key]= value
   def __contains__( self, key ):
       self.process= self.process.load()
       return key in self.data

Этот класс делегирует работу объекту process, который являетсяLoad или DoneLoading объект.Объект Load будет фактически загружен.DoneLoading не будет загружаться.

Обратите внимание, что операторов if нет.

class LoadMe( object ):
   def __init__( self, parent ):
       self.parent= parent
   def load( self ):
       ## Actually load, setting self.parent.data
       return DoneLoading( self.parent )

class DoneLoading( object ):
   def __init__( self, parent ):
       self.parent= parent
   def load( self ):
       return self
1 голос
/ 02 февраля 2013

Вот решение, которое использует декоратор класса для отсрочки инициализации до первого использования объекта:

def lazyload(cls):
    original_init = cls.__init__
    original_getattribute = cls.__getattribute__

    def newinit(self, *args, **kwargs):
        # Just cache the arguments for the eventual initialization.
        self._init_args = args
        self._init_kwargs = kwargs
        self.initialized = False
    newinit.__doc__ = original_init.__doc__

    def performinit(self):
        # We call object's __getattribute__ rather than super(...).__getattribute__
        # or original_getattribute so that no custom __getattribute__ implementations
        # can interfere with what we are doing.
        original_init(self,
                      *object.__getattribute__(self, "_init_args"),
                      **object.__getattribute__(self, "_init_kwargs"))
        del self._init_args
        del self._init_kwargs
        self.initialized = True

    def newgetattribute(self, name):
        if not object.__getattribute__(self, "initialized"):
            performinit(self)
        return original_getattribute(self, name)

    if hasattr(cls, "__getitem__"):
        original_getitem = cls.__getitem__
        def newgetitem(self, key):
            if not object.__getattribute__(self, "initialized"):
                performinit(self)
            return original_getitem(self, key)
        newgetitem.__doc__ = original_getitem.__doc__
        cls.__getitem__ = newgetitem

    if hasattr(cls, "__len__"):
        original_len = cls.__len__
        def newlen(self):
            if not object.__getattribute__(self, "initialized"):
                performinit(self)
            return original_len(self)
        newlen.__doc__ = original_len.__doc__
        cls.__len__ = newlen

    cls.__init__ = newinit
    cls.__getattribute__ = newgetattribute
    return cls

@lazyload
class FileLoader(dict):
    def __init__(self, filename):
        self.filename = filename
        print "Performing expensive load operation"
        self[32] = "Felix"
        self[33] = "Eeek"

kittens = FileLoader("kitties.csv")
print "kittens is instance of FileLoader: %s" % isinstance(kittens, FileLoader) # Well obviously
print len(kittens) # Wait
print kittens[32] # No wait
print kittens[33] # No wait
print kittens.filename # Still no wait
print kittens.filename

Вывод:

kittens is instance of FileLoader: True
Performing expensive load operation
2
Felix
Eeek
kitties.csv
kitties.csv

Я пытался восстановитьоригинальные магические методы после инициализации, но это не сработало.Возможно, потребуется прокси-сервер дополнительных магических методов, я не исследовал каждый сценарий.

Обратите внимание, что kittens.initialized всегда будет возвращать True, потому что запускает инициализацию, если она еще не была выполнена.Очевидно, что было бы возможно добавить исключение для этого атрибута, чтобы он возвращал False, если с объектом не было выполнено никакой другой операции, или проверка может быть заменена на эквивалент вызова hasattr, а инициализированный атрибут может быть удален послеинициализация.

1 голос
/ 30 декабря 2010

Не приведет ли if not self.F к другому вызову __getattr__, что приведет вас в бесконечный цикл?Я думаю, что ваш подход имеет смысл, но чтобы быть в безопасности, я бы сделал следующую строку:

if name == "F" and not self.F:

Кроме того, вы можете сделать loadfile методом для класса в зависимости от того, чтоделает.

0 голосов
/ 30 декабря 2010

Если вам нужно использовать puppies[32], вам также нужно определить метод __getitem__, потому что __getattr__ не улавливает такое поведение.

Я реализую ленивую загрузку для своих нужд, есть неадаптированный код:

class lazy_mask(object):
  '''Fake object, which is substituted in
  place of masked object'''

  def __init__(self, master, id):
    self.master=master
    self.id=id
    self._result=None
    self.master.add(self)

  def _res(self):
    '''Run lazy job'''
    if not self._result:
      self._result=self.master.get(self.id)
    return self._result

  def __getattribute__(self, name):
    '''proxy all queries to masked object'''
    name=name.replace('_lazy_mask', '')
    #print 'attr', name
    if name in ['_result', '_res', 'master', 'id']:#don't proxy requests for own properties
      return super(lazy_mask, self).__getattribute__(name)
    else:#but proxy requests for masked object
      return self._res().__getattribute__(name)

  def __getitem__(self, key):
    '''provide object["key"] access. Else can raise 
    TypeError: 'lazy_mask' object is unsubscriptable'''
    return self._res().__getitem__(key)

(master - это объект реестра, который загружает данные при запуске, это метод get ())

Эта реализация работает нормально для isinstance (), str () и json.dumps ()с этим

0 голосов
/ 30 декабря 2010

Вот хак, который заставляет работать «еще лучшее» решение, но я думаю, что это достаточно раздражает, так что, вероятно, лучше просто использовать первое решение.Идея состоит в том, чтобы выполнить шаг self = loadfile(self.FILE), передав имя переменной в качестве атрибута:

class lazyload:
    def __init__(self,FILE,var):
        self.FILE = FILE
        self.var  = var
    def __getattr__(self,name):
        x = loadfile(self.FILE)
        globals()[self.var]=x
        return object.__getattribute__(x, name)

Затем вы можете выполнить

kitties = lazyload("kitties.csv","kitties")
   ^                                 ^
    \                               /
     These two better match exactly

После вызова любого метода на kitties (кроме kitties.FILE или kitties.var), он станет полностью неотличимым от того, что вы получили бы с kitties = loadfile("kitties.csv").В частности, он больше не будет экземпляром lazyload, kitties.FILE и kitties.var больше не будут существовать.

...