Различные способы представления записей

PythonPythonIntermediate
Практиковаться сейчас

This tutorial is from open-source community. Access the source code

💡 Этот учебник переведен с английского с помощью ИИ. Чтобы просмотреть оригинал, вы можете перейти на английский оригинал

Введение

В этом лабораторном занятии (LabEx) вы научитесь искать экономичные по памяти способы хранения больших наборов данных в Python. Вы также узнаете о различных способах представления записей, таких как кортежи (tuples), словари (dictionaries), классы (classes) и именованные кортежи (named tuples).

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


Skills Graph

%%%%{init: {'theme':'neutral'}}%%%% flowchart RL python(("Python")) -.-> python/DataStructuresGroup(["Data Structures"]) python(("Python")) -.-> python/FileHandlingGroup(["File Handling"]) python(("Python")) -.-> python/PythonStandardLibraryGroup(["Python Standard Library"]) python/DataStructuresGroup -.-> python/tuples("Tuples") python/DataStructuresGroup -.-> python/dictionaries("Dictionaries") python/FileHandlingGroup -.-> python/file_opening_closing("Opening and Closing Files") python/FileHandlingGroup -.-> python/file_reading_writing("Reading and Writing Files") python/FileHandlingGroup -.-> python/file_operations("File Operations") python/FileHandlingGroup -.-> python/with_statement("Using with Statement") python/PythonStandardLibraryGroup -.-> python/data_collections("Data Collections") subgraph Lab Skills python/tuples -.-> lab-132428{{"Различные способы представления записей"}} python/dictionaries -.-> lab-132428{{"Различные способы представления записей"}} python/file_opening_closing -.-> lab-132428{{"Различные способы представления записей"}} python/file_reading_writing -.-> lab-132428{{"Различные способы представления записей"}} python/file_operations -.-> lab-132428{{"Различные способы представления записей"}} python/with_statement -.-> lab-132428{{"Различные способы представления записей"}} python/data_collections -.-> lab-132428{{"Различные способы представления записей"}} end

Изучение набора данных

Начнем наше путешествие с внимательного изучения набора данных, с которым мы будем работать. Файл ctabus.csv представляет собой CSV-файл (Comma-Separated Values, значения, разделенные запятыми). CSV-файлы - это распространенный способ хранения табличных данных, где каждая строка представляет собой строку, а значения внутри строки разделены запятыми. Этот конкретный файл содержит данные о ежедневном количестве пассажиров автобусной системы Chicago Transit Authority (CTA) (Управление общественного транспорта Чикаго) за период с 1 января 2001 года по 31 августа 2013 года.

Разархивируйте файл и удалите zip-файл:

cd /home/labex/project
unzip ctabus.csv.zip
rm ctabus.csv.zip

Чтобы понять структуру этого файла, мы сначала заглянем внутрь него. Мы будем использовать Python для чтения файла и вывода нескольких строк. Откройте терминал и запустите следующий код Python:

f = open('/home/labex/project/ctabus.csv')
print(next(f))  ## Read the header line
print(next(f))  ## Read the first data line
print(next(f))  ## Read the second data line
f.close()

В этом коде мы сначала открываем файл с помощью функции open и присваиваем его переменной f. Функция next используется для чтения следующей строки из файла. Мы используем ее три раза: первый раз для чтения строки заголовка, которая обычно содержит имена столбцов в наборе данных. Второй и третий разы мы читаем первую и вторую строки данных соответственно. Наконец, мы закрываем файл с помощью метода close, чтобы освободить системные ресурсы.

Вы должны увидеть вывод, похожий на этот:

route,date,daytype,rides

3,01/01/2001,U,7354

4,01/01/2001,U,9288

Этот вывод показывает, что файл имеет 4 столбца данных. Давайте разберем, что представляет собой каждый столбец:

  1. route: Это название или номер автобусного маршрута. Это первый столбец (столбец 0) в наборе данных.
  2. date: Это строка даты в формате MM/DD/YYYY. Это второй столбец (столбец 1).
  3. daytype: Это код типа дня, который является третьим столбцом (столбец 2).
    • U = Воскресенье/Праздник (Sunday/Holiday)
    • A = Суббота (Saturday)
    • W = Будний день (Weekday)
  4. rides: Этот столбец записывает общее количество пассажиров в виде целого числа. Это четвертый столбец (столбец 3).

Столбец rides сообщает нам, сколько людей сели в автобус на определенном маршруте в определенный день. Например, из приведенного выше вывода мы видим, что 7354 человека проехали на автобусе номер 3 1 января 2001 года.

Теперь давайте выясним, сколько строк в файле. Знание количества строк даст нам представление о размере нашего набора данных. Запустите следующий код Python:

with open('/home/labex/project/ctabus.csv') as f:
    line_count = sum(1 for line in f)
    print(f"Total lines in the file: {line_count}")

В этом коде мы используем оператор with для открытия файла. Преимущество использования with заключается в том, что он автоматически закрывает файл, когда мы заканчиваем с ним работать. Затем мы используем выражение-генератор (1 for line in f) для создания последовательности единиц, по одной для каждой строки в файле. Функция sum складывает все эти единицы, давая нам общее количество строк в файле. Наконец, мы выводим результат.

Это должно вывести примерно 577 564 строки, что означает, что мы имеем дело со значительным набором данных. Этот большой набор данных предоставит нам достаточно данных для анализа и извлечения информации.

Измерение использования памяти с разными методами хранения

На этом этапе мы рассмотрим, как различные способы хранения данных могут повлиять на использование памяти. Использование памяти является важной частью программирования, особенно при работе с большими наборами данных. Чтобы измерить память, используемую нашим кодом на Python, мы будем использовать модуль tracemalloc Python. Этот модуль очень полезен, так как позволяет отслеживать выделение памяти, выполняемое Python. С его помощью мы можем увидеть, сколько памяти потребляют наши методы хранения данных.

Метод 1: Хранение всего файла как одной строки

Начнем с создания нового файла на Python. Перейдите в каталог /home/labex/project и создайте файл с именем memory_test1.py. Вы можете использовать текстовый редактор для открытия этого файла. После открытия файла добавьте в него следующий код. Этот код прочитает весь контент файла как одну строку и измерит использование памяти.

## memory_test1.py
import tracemalloc

def test_single_string():
    ## Start tracking memory
    tracemalloc.start()

    ## Read the entire file as a single string
    with open('/home/labex/project/ctabus.csv') as f:
        data = f.read()

    ## Get memory usage statistics
    current, peak = tracemalloc.get_traced_memory()

    print(f"File length: {len(data)} characters")
    print(f"Current memory usage: {current/1024/1024:.2f} MB")
    print(f"Peak memory usage: {peak/1024/1024:.2f} MB")

    ## Stop tracking memory
    tracemalloc.stop()

if __name__ == "__main__":
    test_single_string()

После добавления кода сохраните файл. Теперь, чтобы запустить этот скрипт, откройте терминал и выполните следующую команду:

python3 /home/labex/project/memory_test1.py

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

File length: 12361039 characters
Current memory usage: 11.80 MB
Peak memory usage: 23.58 MB

Точные числа могут отличаться на вашей системе, но обычно вы заметите, что текущее использование памяти составляет около 12 МБ, а пиковое использование памяти - около 24 МБ.

Метод 2: Хранение в виде списка строк

Далее мы протестируем другой способ хранения данных. Создайте новый файл с именем memory_test2.py в том же каталоге /home/labex/project. Откройте этот файл в редакторе и добавьте следующий код. Этот код читает файл и хранит каждую строку как отдельную строку в списке, а затем измеряет использование памяти.

## memory_test2.py
import tracemalloc

def test_list_of_strings():
    ## Start tracking memory
    tracemalloc.start()

    ## Read the file as a list of strings (one string per line)
    with open('/home/labex/project/ctabus.csv') as f:
        lines = f.readlines()

    ## Get memory usage statistics
    current, peak = tracemalloc.get_traced_memory()

    print(f"Number of lines: {len(lines)}")
    print(f"Current memory usage: {current/1024/1024:.2f} MB")
    print(f"Peak memory usage: {peak/1024/1024:.2f} MB")

    ## Stop tracking memory
    tracemalloc.stop()

if __name__ == "__main__":
    test_list_of_strings()

Сохраните файл и запустите скрипт, используя следующую команду в терминале:

python3 /home/labex/project/memory_test2.py

Вы должны увидеть вывод, похожий на следующий:

Number of lines: 577564
Current memory usage: 43.70 MB
Peak memory usage: 43.74 MB

Обратите внимание, что использование памяти значительно увеличилось по сравнению с предыдущим методом хранения данных как одной строки. Это происходит потому, что каждая строка в списке является отдельным объектом строки Python, и каждый объект имеет свою собственную накладную нагрузку по памяти.

Понимание разницы в использовании памяти

Разница в использовании памяти между двумя подходами демонстрирует важный концепт в программировании на Python, называемый накладной нагрузкой объекта. Когда вы храните данные в виде списка строк, каждая строка является отдельным объектом Python. Каждый объект имеет некоторые дополнительные требования к памяти, которые включают:

  1. Заголовок объекта Python (обычно 16 - 24 байта на объект). Этот заголовок содержит информацию о объекте, такую как его тип и счетчик ссылок.
  2. Фактическое строковое представление, которое хранит символы строки.
  3. Заполнение для выравнивания памяти. Это дополнительное пространство, добавленное для обеспечения правильного выравнивания адреса памяти объекта для эффективного доступа.

С другой стороны, когда вы храните весь контент файла как одну строку, есть только один объект, и, следовательно, только один набор накладных расходов. Это делает его более экономичным по памяти, если учитывать общий размер данных.

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

Работа со структурированными данными с использованием кортежей

До сих пор мы занимались хранением необработанных текстовых данных. Но когда дело доходит до анализа данных, обычно нужно преобразовать данные в более организованные и структурированные форматы. Это упрощает выполнение различных операций и получение полезной информации из данных. На этом этапе мы научимся считывать данные в виде списка кортежей с использованием модуля csv. Кортежи - это простая и полезная структура данных в Python, которая может хранить несколько значений.

Создание функции-читателя с использованием кортежей

Создадим новый файл с именем readrides.py в каталоге /home/labex/project. Этот файл будет содержать код для чтения данных из CSV-файла и их хранения в виде списка кортежей.

## readrides.py
import csv
import tracemalloc

def read_rides_as_tuples(filename):
    '''
    Read the bus ride data as a list of tuples
    '''
    records = []
    with open(filename) as f:
        rows = csv.reader(f)
        headings = next(rows)     ## Skip headers
        for row in rows:
            route = row[0]
            date = row[1]
            daytype = row[2]
            rides = int(row[3])
            record = (route, date, daytype, rides)
            records.append(record)
    return records

if __name__ == '__main__':
    tracemalloc.start()

    rows = read_rides_as_tuples('/home/labex/project/ctabus.csv')

    current, peak = tracemalloc.get_traced_memory()
    print(f'Number of records: {len(rows)}')
    print(f'First record: {rows[0]}')
    print(f'Second record: {rows[1]}')
    print(f'Memory Use: Current {current/1024/1024:.2f} MB, Peak {peak/1024/1024:.2f} MB')

Этот скрипт определяет функцию read_rides_as_tuples. Вот что она делает пошагово:

  1. Она открывает CSV-файл, указанный параметром filename. Это позволяет нам получить доступ к данным в файле.
  2. Она использует модуль csv для разбора каждой строки файла. Функция csv.reader помогает нам разделить строки на отдельные значения.
  3. Она извлекает четыре поля (маршрут, дата, тип дня и количество поездок) из каждой строки. Эти поля важны для нашего анализа данных.
  4. Она преобразует поле 'rides' в целое число. Это необходимо, так как данные в CSV-файле изначально имеют строковый формат, а для вычислений нужен числовой тип.
  5. Она создает кортеж с этими четырьмя значениями. Кортежи неизменяемы, то есть их значения нельзя изменить после создания.
  6. Она добавляет кортеж в список с именем records. Этот список будет содержать все записи из CSV-файла.

Теперь запустим скрипт. Откройте терминал и введите следующую команду:

python3 /home/labex/project/readrides.py

Вы должны увидеть вывод, похожий на следующий:

Number of records: 577563
First record: ('3', '01/01/2001', 'U', 7354)
Second record: ('4', '01/01/2001', 'U', 9288)
Memory Use: Current 89.12 MB, Peak 89.15 MB

Обратите внимание, что использование памяти увеличилось по сравнению с предыдущими примерами. Это связано с несколькими причинами:

  1. Теперь мы храним данные в структурированном формате (кортежи). Структурированные данные обычно требуют больше памяти, так как имеют определенную организацию.
  2. Каждое значение в кортеже является отдельным объектом Python. Объекты Python имеют определенную накладную нагрузку, которая способствует увеличению использования памяти.
  3. У нас есть дополнительная структура списка, которая хранит все эти кортежи. Списки также занимают память для хранения своих элементов.

Преимущество использования этого подхода заключается в том, что наши данные теперь правильно структурированы и готовы к анализу. Мы можем легко получить доступ к определенным полям каждой записи по индексу. Например:

## Example of accessing tuple elements (add this to readrides.py file to try it)
first_record = rows[0]
route = first_record[0]
date = first_record[1]
daytype = first_record[2]
rides = first_record[3]
print(f"Route: {route}, Date: {date}, Day type: {daytype}, Rides: {rides}")

Однако получение доступа к данным по числовому индексу не всегда интуитивно понятно. Можно легко забыть, какой индекс соответствует какому полю, особенно при работе с большим количеством полей. На следующем этапе мы рассмотрим другие структуры данных, которые могут сделать наш код более читаемым и поддерживаемым.

✨ Проверить решение и практиковаться

Сравнение различных структур данных

В Python структуры данных используются для организации и хранения связанных данных. Они похожи на контейнеры, которые хранят различные типы информации в структурированном виде. На этом этапе мы сравним различные структуры данных и посмотрим, сколько памяти они используют.

Создадим новый файл с именем compare_structures.py в каталоге /home/labex/project. Этот файл будет содержать код для чтения данных из CSV-файла и их хранения в различных структурах данных.

## compare_structures.py
import csv
import tracemalloc
from collections import namedtuple

## Define a named tuple for rides data
RideRecord = namedtuple('RideRecord', ['route', 'date', 'daytype', 'rides'])

## A named tuple is a lightweight class that allows you to access its fields by name.
## It's like a tuple, but with named attributes.

## Define a class with __slots__ for memory optimization
class SlottedRideRecord:
    __slots__ = ['route', 'date', 'daytype', 'rides']

    def __init__(self, route, date, daytype, rides):
        self.route = route
        self.date = date
        self.daytype = daytype
        self.rides = rides

## A class with __slots__ is a memory - optimized class.
## It avoids using an instance dictionary, which saves memory.

## Define a regular class for rides data
class RegularRideRecord:
    def __init__(self, route, date, daytype, rides):
        self.route = route
        self.date = date
        self.daytype = daytype
        self.rides = rides

## A regular class is an object - oriented way to represent data.
## It has named attributes and can have methods.

## Function to read data as tuples
def read_as_tuples(filename):
    records = []
    with open(filename) as f:
        rows = csv.reader(f)
        next(rows)  ## Skip headers
        for row in rows:
            record = (row[0], row[1], row[2], int(row[3]))
            records.append(record)
    return records

## This function reads data from a CSV file and stores it as tuples.
## Tuples are immutable sequences, and you access their elements by numeric index.

## Function to read data as dictionaries
def read_as_dicts(filename):
    records = []
    with open(filename) as f:
        rows = csv.reader(f)
        headers = next(rows)  ## Get headers
        for row in rows:
            record = {
                'route': row[0],
                'date': row[1],
                'daytype': row[2],
                'rides': int(row[3])
            }
            records.append(record)
    return records

## This function reads data from a CSV file and stores it as dictionaries.
## Dictionaries use key - value pairs, so you can access elements by their names.

## Function to read data as named tuples
def read_as_named_tuples(filename):
    records = []
    with open(filename) as f:
        rows = csv.reader(f)
        next(rows)  ## Skip headers
        for row in rows:
            record = RideRecord(row[0], row[1], row[2], int(row[3]))
            records.append(record)
    return records

## This function reads data from a CSV file and stores it as named tuples.
## Named tuples combine the efficiency of tuples with the readability of named access.

## Function to read data as regular class instances
def read_as_regular_classes(filename):
    records = []
    with open(filename) as f:
        rows = csv.reader(f)
        next(rows)  ## Skip headers
        for row in rows:
            record = RegularRideRecord(row[0], row[1], row[2], int(row[3]))
            records.append(record)
    return records

## This function reads data from a CSV file and stores it as instances of a regular class.
## Regular classes allow you to add methods to your data.

## Function to read data as slotted class instances
def read_as_slotted_classes(filename):
    records = []
    with open(filename) as f:
        rows = csv.reader(f)
        next(rows)  ## Skip headers
        for row in rows:
            record = SlottedRideRecord(row[0], row[1], row[2], int(row[3]))
            records.append(record)
    return records

## This function reads data from a CSV file and stores it as instances of a slotted class.
## Slotted classes are memory - optimized and still provide named access.

## Function to measure memory usage
def measure_memory(func, filename):
    tracemalloc.start()

    records = func(filename)

    current, peak = tracemalloc.get_traced_memory()

    ## Demonstrate how to use each data structure
    first_record = records[0]
    if func.__name__ == 'read_as_tuples':
        route, date, daytype, rides = first_record
    elif func.__name__ == 'read_as_dicts':
        route = first_record['route']
        date = first_record['date']
        daytype = first_record['daytype']
        rides = first_record['rides']
    else:  ## named tuples and classes
        route = first_record.route
        date = first_record.date
        daytype = first_record.daytype
        rides = first_record.rides

    print(f"Structure type: {func.__name__}")
    print(f"Record count: {len(records)}")
    print(f"Example access: Route={route}, Date={date}, Rides={rides}")
    print(f"Current memory: {current/1024/1024:.2f} MB")
    print(f"Peak memory: {peak/1024/1024:.2f} MB")
    print("-" * 50)

    tracemalloc.stop()

    return current

if __name__ == "__main__":
    filename = '/home/labex/project/ctabus.csv'

    ## Run all memory tests
    print("Memory usage comparison for different data structures:\n")

    results = []
    for reader_func in [
        read_as_tuples,
        read_as_dicts,
        read_as_named_tuples,
        read_as_regular_classes,
        read_as_slotted_classes
    ]:
        memory = measure_memory(reader_func, filename)
        results.append((reader_func.__name__, memory))

    ## Sort by memory usage (lowest first)
    results.sort(key=lambda x: x[1])

    print("\nRanking by memory efficiency (most efficient first):")
    for i, (name, memory) in enumerate(results, 1):
        print(f"{i}. {name}: {memory/1024/1024:.2f} MB")

Запустите скрипт, чтобы увидеть результаты сравнения:

python3 /home/labex/project/compare_structures.py

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

Понимание различных структур данных

  1. Кортежи (Tuples):

    • Кортежи - это легковесные и неизменяемые последовательности. Это означает, что после создания кортежа вы не можете изменить его элементы.
    • Вы получаете доступ к элементам кортежа по их числовому индексу, например, record[0], record[1] и т.д.
    • Они очень экономичны по памяти, так как имеют простую структуру.
    • Однако они могут быть менее читаемыми, так как вам нужно помнить индекс каждого элемента.
  2. Словари (Dictionaries):

    • Словари используют пары ключ - значение, что позволяет получать доступ к элементам по их именам.
    • Они более читаемые, например, вы можете использовать record['route'], record['date'] и т.д.
    • Они имеют более высокое использование памяти из - за накладных расходов хеш - таблицы, используемой для хранения пар ключ - значение.
    • Они гибкие, так как вы можете легко добавлять или удалять поля.
  3. Именнованные кортежи (Named Tuples):

    • Именнованные кортежи сочетают в себе эффективность кортежей и возможность доступа к элементам по имени.
    • Вы можете получать доступ к элементам с использованием точечной нотации, например, record.route, record.date и т.д.
    • Они неизменяемы, как и обычные кортежи.
    • Они более экономичны по памяти, чем словари.
  4. Обычные классы (Regular Classes):

    • Обычные классы следуют объектно - ориентированному подходу и имеют именованные атрибуты.
    • Вы можете получать доступ к атрибутам с использованием точечной нотации, например, record.route, record.date и т.д.
    • Вы можете добавлять методы в обычный класс, чтобы определить поведение.
    • Они используют больше памяти, так как каждый экземпляр имеет словарь экземпляра для хранения своих атрибутов.
  5. Классы с __slots__:

    • Классы с __slots__ - это классы, оптимизированные по памяти. Они не используют словарь экземпляра, что экономит память.
    • Они по - прежнему обеспечивают именованный доступ к атрибутам, например, record.route, record.date и т.д.
    • Они ограничивают добавление новых атрибутов после создания объекта.
    • Они более экономичны по памяти, чем обычные классы.

Когда использовать каждый подход

  • Кортежи: Используйте кортежи, когда память является критическим фактором и вам нужен только простой доступ к данным по индексу.
  • Словари: Используйте словари, когда вам нужна гибкость, например, когда поля в ваших данных могут различаться.
  • Именнованные кортежи: Используйте именнованные кортежи, когда вам нужна как читаемость, так и экономия памяти.
  • Обычные классы: Используйте обычные классы, когда вам нужно добавить поведение (методы) к вашим данным.
  • Классы с __slots__: Используйте классы с __slots__, когда вам нужно поведение и максимальная экономия памяти.

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

✨ Проверить решение и практиковаться

Итоги

В этом практическом занятии (лабораторной работе) вы узнали различные способы представления записей в Python и проанализировали их эффективность с точки зрения использования памяти. Сначала вы поняли структуру базового набора данных в формате CSV и сравнили методы хранения необработанного текста. Затем вы работали со структурированными данными, используя кортежи, и реализовали пять различных структур данных: кортежи, словари, именованные кортежи, обычные классы и классы с __slots__.

Основные выводы: различные структуры данных предлагают компромиссы между эффективностью использования памяти, читаемостью и функциональностью. Накладные расходы на объекты в Python оказывают значительное влияние на использование памяти при работе с большими наборами данных, и выбор структуры данных может существенно повлиять на потребление памяти. Именованные кортежи и классы с __slots__ являются хорошими компромиссами между эффективностью использования памяти и читаемостью кода. Эти концепции ценны для разработчиков на Python в области обработки данных, особенно при работе с большими наборами данных, где эффективность использования памяти имеет решающее значение.