Непонятное поведение load_dataset сосновой шишки

В следующей программе у меня есть три функции get_dataset1, get_dataset2 и get_dataset3, которые очень похожи. Они отличаются только тем, когда вызывают len(dataset) и os.path.join = tmp.

Функции get_dataset1 и get_dataset3 ведут себя так, как задумано; они загружают набор данных, и его длина больше 0. Однако в случае get_dataset2 набор данных имеет длину 0. Почему?

import copy
import os
import time

from pinecone_datasets import load_dataset

datasetName = "langchain-python-docs-text-embedding-ada-002"


def get_dataset1():
    os.path.join = lambda *s: "/".join(s)  # pinecone bug workaround
    dataset = load_dataset(datasetName)
    print("Dataset loaded:", len(dataset) != 0)  # dataset has length greater than 0


def get_dataset2():
    os.path.join = lambda *s: "/".join(s)  # pinecone bug workaround
    dataset = load_dataset(datasetName)
    os.path.join = tmp
    print("Dataset loaded:", len(dataset) != 0)  # dataset has length 0


def get_dataset3():
    os.path.join = lambda *s: "/".join(s)  # pinecone bug workaround
    dataset = load_dataset(datasetName)
    print("Dataset loaded:", len(dataset) != 0)  # dataset has length greater than 0
    os.path.join = tmp
    print("Dataset loaded:", len(dataset) != 0)  # dataset has length greater than 0


def main():
    get_dataset1()
    get_dataset2()
    get_dataset3()


if __name__ == "__main__":
    tmp = copy.deepcopy(os.path.join)
    main()

Что-то происходит с вашими побочными эффектами. Поскольку get_dataset3 работает, а get_dataset2 нет, я думаю, что вызов len(dataset) имеет побочный эффект (может быть, это значение кэшируется?)

tbrugere 06.07.2024 13:47
Почему в Python есть оператор "pass"?
Почему в Python есть оператор "pass"?
Оператор pass в Python - это простая концепция, которую могут быстро освоить даже новички без опыта программирования.
Некоторые методы, о которых вы не знали, что они существуют в Python
Некоторые методы, о которых вы не знали, что они существуют в Python
Python - самый известный и самый простой в изучении язык в наши дни. Имея широкий спектр применения в области машинного обучения, Data Science,...
Основы Python Часть I
Основы Python Часть I
Вы когда-нибудь задумывались, почему в программах на Python вы видите приведенный ниже код?
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
LeetCode - 1579. Удаление максимального числа ребер для сохранения полной проходимости графа
Алиса и Боб имеют неориентированный граф из n узлов и трех типов ребер:
Оптимизация кода с помощью тернарного оператора Python
Оптимизация кода с помощью тернарного оператора Python
И последнее, что мы хотели бы показать вам, прежде чем двигаться дальше, это
Советы по эффективной веб-разработке с помощью Python
Советы по эффективной веб-разработке с помощью Python
Как веб-разработчик, Python может стать мощным инструментом для создания эффективных и масштабируемых веб-приложений.
1
1
60
1
Перейти к ответу Данный вопрос помечен как решенный

Ответы 1

Ответ принят как подходящий

Короткий ответ

В итоге мне пришлось просмотреть исходный код pinecone-datasets, но ответ, по сути, сводится к ленивым вычислениям.

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

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

Чтобы добиться ожидаемого поведения, убедитесь, что os.path.join определен правильно, когда вы делаете что-то интересное с Dataset, а не только когда вы его создаете.

Длинный ответ

Когда вы пишете строку:

dataset = load_dataset(datasetName)

В конечном итоге это приводит к вызову конструктора Dataset. Большая часть конструктора не имеет значения, но есть пара строк, которые, похоже, влияют на ваш вариант использования:

self._dataset_path = dataset_path
self._documents = None

Набор данных хранит информацию о том, как он может получить данные, но откладывает доступ к ним на более позднее время. Таким образом, к тому времени, когда вы попытаетесь найти len(dataset), свойство dataset._documents может все еще быть None, а может и не быть. Чтобы обработать оба случая, len(dataset) обращается к свойству documents (а не _documents), используя этот метод:

@property
def documents(self) -> pd.DataFrame:
    if self._documents is None:
        self._documents = self._safe_read_from_path("documents")
    return self._documents

Это часть класса, посвященная ленивым вычислениям. Если _documents уже существует, он просто вернет свое значение. Но если _documents по-прежнему равно None, он вычислит его значение с помощью _safe_read_from_path, затем навсегда сохранит это значение и вернет его.

В свою очередь, _safe_read_from_path содержит следующую строку:

read_path_str = os.path.join(self._dataset_path, data_type, "*.parquet")

Таким образом, функциональность метода будет зависеть от того, как вы в последний раз определили os.path.join.

Если сложить все вместе, это означает, что Dataset не сохраняет многого, пока вы не вызовете len, после чего он использует текущее значение os.path.join, чтобы присвоить полезное значение своему свойству _document и продолжает использовать это значение во всех будущих вызовах.

Учитывая это, мы можем выполнить каждую из ваших функций:

get_dataset1

Функция выполняет следующие шаги:

  1. Определите os.path.join как свою лямбду.
  2. Загрузите новый набор данных со свойством _documents None.
  3. Рассчитайте длину набора данных, используя лямбду, хранящуюся в os.path.join, чтобы наконец присвоить значение _documents.

Это довольно ванильно, что объясняет, почему все работает так, как ожидалось.

get_dataset2

Этот имеет аналогичную структуру, но отличается важным образом:

  1. Как и раньше, определите os.path.join как лямбду.
  2. Как и раньше, загрузите новый набор данных со значением _documents None.
  3. Переопределите os.path.join как temp.
  4. Рассчитайте длину набора данных, используя значение temp, хранящееся в os.path.join, чтобы присвоить значение _documents.

В результате вы получите значение _documents, рассчитанное с использованием неправильной реализации os.path.join, что объясняет, почему вы получаете Dataset нулевой длины.

get_dataset3

Именно здесь действительно вступает в игру ленивая оценка. Функция выполняет следующие шаги:

  1. Выполните все шаги get_dataset1
  2. Переопределяет os.path.join
  3. Попытайтесь получить длину набора данных — и поскольку _documents уже известно из последнего вызова len, он использует это старое значение вместо пересчета на основе нового значения os.path.join.

Таким образом, даже если вы переопределили os.path.join перед вторым len вызовом, Dataset использует то же самое значение _documents, которое вы вычислили в get_dataset1, и ни в малейшей степени не заботится о новом значении os.path.join.

Другие вопросы по теме