Что я должен использовать: класс или словарь?

105

У меня есть класс, содержащий только поля и никаких методов, например:

class Request(object):

    def __init__(self, environ):
        self.environ = environ
        self.request_method = environ.get('REQUEST_METHOD', None)
        self.url_scheme = environ.get('wsgi.url_scheme', None)
        self.request_uri = wsgiref.util.request_uri(environ)
        self.path = environ.get('PATH_INFO', None)
        # ...

Это можно легко перевести на диктант. Класс более гибкий для будущих дополнений и может работать быстрее __slots__. Так будет ли вместо этого использование dict? Будет ли диктатор быстрее класса? И быстрее класса со слотами?

Deamon
источник
2
Я всегда использую словари для хранения данных, и мне это кажется вариантом использования. в некоторых случаях dictможет иметь смысл наследование класса от до. Отличное преимущество: при отладке просто скажите, print(request)и вы сразу увидите всю информацию о состоянии. при более классическом подходе вам придется писать свои собственные __str__методы, что отстой, если вам приходится делать это все время.
поток
Они взаимозаменяемы stackoverflow.com/questions/1305532/…
Алексей
Если класс имеет смысл и понятен другим, почему бы и нет? Кроме того, если вы определяете много классов, например, с общими интерфейсами, почему бы и нет? Но Python не поддерживает мощные объектно-ориентированные концепции, такие как C ++.
MasterControlProgram
4
@Ralf, что ООП не поддерживает Python?
qwr

Ответы:

34

Зачем вам делать из этого словарь? В чем преимущество? Что произойдет, если позже вы захотите добавить код? Куда пойдет ваш __init__код?

Классы предназначены для объединения связанных данных (и обычно кода).

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

Держите это в классе.

adw
источник
Я бы создал своего рода фабричный метод, создающий dict вместо метода класса __init__. Но ты прав: я бы разделил вещи, которые принадлежат друг другу.
deamon
89
не могу не согласиться с вами: словари, наборы, списки и кортежи - все это для объединения связанных данных. никоим образом не предполагается, что значения словаря должны или должны быть одного и того же типа данных, как раз наоборот. во многих списках и наборах значения будут иметь один и тот же тип, но это в основном потому, что нам нравится перечислять их вместе. Я действительно считаю, что широкое использование классов для хранения данных является злоупотреблением oop; Когда вы задумываетесь о проблемах сериализации, вы легко понимаете, почему.
поток
4
Это ОБЪЕКТ-ориентированное программирование, а не класс-ориентированное программирование по одной причине: мы имеем дело с объектами. Объект характеризуется 2 (3) свойствами: 1. состояние (члены), 2. поведение (методы) и 3. экземпляр, которые можно описать несколькими словами. Следовательно, классы предназначены для объединения состояния и поведения.
friendzis
14
Я отметил это, потому что вы всегда должны по умолчанию использовать более простую структуру данных, где это возможно. В этом случае словаря будет достаточно по прямому назначению. Вопрос where would your __init__ code go?по поводу. Это могло бы убедить менее опытного разработчика в том, что следует использовать только классы, поскольку метод инициализации не используется в словаре. Абсурд.
Ллойд Мур
1
@Ralf Класс Foo - это просто тип, такой как int и string. Вы храните значение в целочисленном типе или в переменной foo целочисленного типа? Тонкая, но важная смысловая разница. В таких языках, как C, это различие не слишком актуально за пределами академических кругов. Хотя в большинстве ОО-языков есть поддержка переменных класса, что делает различие между классом и объектом / экземпляром чрезвычайно актуальным - храните ли вы данные в классе (совместно используемом для всех экземпляров) или в конкретном объекте? Не
кусайся
44

Используйте словарь, если вам не нужен дополнительный механизм класса. Вы также можете использовать namedtupleгибридный подход:

>>> from collections import namedtuple
>>> request = namedtuple("Request", "environ request_method url_scheme")
>>> request
<class '__main__.Request'>
>>> request.environ = "foo"
>>> request.environ
'foo'

Различия в производительности здесь будут минимальными, хотя я был бы удивлен, если бы словарь не был быстрее.

Катриэль
источник
15
«Различия в производительности здесь будут минимальными , хотя я был бы удивлен, если бы словарь не был значительно быстрее». Это не вычисляет. :)
mipadi
1
@mipadi: это правда. Теперь исправлено: p
Katriel
Dict в 1,5 раза быстрее, чем namedtuple, и в два раза быстрее, чем класс без слотов. Проверьте мой пост об этом ответе.
alexpinho98
@ alexpinho98: Я очень старался найти «пост», о котором вы говорите, но не могу его найти! вы можете предоставить URL. Благодарность!
Дэн Облингер
@DanOblinger Я предполагаю, что он имеет в виду свой ответ ниже.
Адам Льюис,
37

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

  • Вся ваша логика живет в одной функции
  • Легко обновляется и остается инкапсулированным
  • Если вы измените что-либо позже, вы можете легко сохранить интерфейс прежним
Брюс Армстронг
источник
Вся ваша логика не живет в одной функции. Класс - это кортеж общего состояния и обычно один или несколько методов. Если вы меняете класс, вам не дается никаких гарантий относительно его интерфейса.
Ллойд Мур
25

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

Я сравнил время, необходимое для создания и изменения переменной в dict, классе new_style и классе new_style со слотами.

Вот код, который я использовал для его тестирования (он немного беспорядочный, но выполняет свою работу).

import timeit

class Foo(object):

    def __init__(self):

        self.foo1 = 'test'
        self.foo2 = 'test'
        self.foo3 = 'test'

def create_dict():

    foo_dict = {}
    foo_dict['foo1'] = 'test'
    foo_dict['foo2'] = 'test'
    foo_dict['foo3'] = 'test'

    return foo_dict

class Bar(object):
    __slots__ = ['foo1', 'foo2', 'foo3']

    def __init__(self):

        self.foo1 = 'test'
        self.foo2 = 'test'
        self.foo3 = 'test'

tmit = timeit.timeit

print 'Creating...\n'
print 'Dict: ' + str(tmit('create_dict()', 'from __main__ import create_dict'))
print 'Class: ' + str(tmit('Foo()', 'from __main__ import Foo'))
print 'Class with slots: ' + str(tmit('Bar()', 'from __main__ import Bar'))

print '\nChanging a variable...\n'

print 'Dict: ' + str((tmit('create_dict()[\'foo3\'] = "Changed"', 'from __main__ import create_dict') - tmit('create_dict()', 'from __main__ import create_dict')))
print 'Class: ' + str((tmit('Foo().foo3 = "Changed"', 'from __main__ import Foo') - tmit('Foo()', 'from __main__ import Foo')))
print 'Class with slots: ' + str((tmit('Bar().foo3 = "Changed"', 'from __main__ import Bar') - tmit('Bar()', 'from __main__ import Bar')))

И вот результат ...

Создание ...

Dict: 0.817466186345
Class: 1.60829183597
Class_with_slots: 1.28776730003

Изменение переменной ...

Dict: 0.0735140918748
Class: 0.111714198313
Class_with_slots: 0.10618612142

Итак, если вы просто храните переменные, вам нужна скорость, и вам не потребуется выполнять много вычислений, я рекомендую использовать dict (вы всегда можете просто создать функцию, которая выглядит как метод). Но, если вам действительно нужны классы, помните - всегда используйте __ slots __ .

Заметка:

Я тестировал «класс» с обеими new_style и old_style классов. Оказывается, классы old_style быстрее создавать, но медленнее изменять (не сильно, но существенно, если вы создаете много классов в тесном цикле (совет: вы делаете это неправильно)).

Также время для создания и изменения переменных на вашем компьютере может отличаться, так как мой старый и медленный. Убедитесь, что вы проверили его сами, чтобы увидеть «реальные» результаты.

Редактировать:

Позже я протестировал namedtuple: я не могу его изменить, но для создания 10000 образцов (или чего-то подобного) потребовалось 1,4 секунды, поэтому словарь действительно самый быстрый.

Если я изменю функцию dict, чтобы включить ключи и значения и вернуть dict вместо переменной, содержащей dict, когда я ее создаю, это даст мне 0,65 вместо 0,8 секунды.

class Foo(dict):
    pass

Создание похоже на класс со слотами, а изменение переменной - самое медленное (0,17 секунды), поэтому не используйте эти классы . перейти к dict (скорость) или к классу, производному от объекта ('синтаксическая конфета')

Alexpinho98
источник
Я хотел бы увидеть числа для подкласса dict(я полагаю, без переопределенных методов?). Работает ли он так же, как класс нового стиля, написанный с нуля?
Бенджамин Ходжсон
12

Я согласен с @adw. Я бы никогда не изобразил «объект» (в объектно-ориентированном смысле) со словарем. Словари объединяют пары имя / значение. Классы представляют объекты. Я видел код, в котором объекты представлены словарями, и неясно, какова фактическая форма этого объекта. Что происходит, когда определенных имен / ценностей нет? Что мешает клиенту вообще что-либо вставлять. Или пытаться вообще что-нибудь вытащить. Форма вещи всегда должна быть четко обозначена.

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

Jaydel
источник
9
Ответы на SO сортируются по голосам, а ответы с одинаковым количеством голосов расположены в случайном порядке. Так что поясните, пожалуйста, кого вы имеете в виду под «последним плакатом».
Майк Дезимоун
4
И как узнать, что то, что имеет только поля и не имеет функций, является «объектом» в объектно-ориентированном смысле?
Мухаммад Алькарури,
1
Моя лакмусовая бумажка - «зафиксирована ли структура этих данных?». Если да, то используйте объект, иначе dict. Отказ от этого только создаст путаницу.
weberc2
Вы должны проанализировать контекст решаемой проблемы. Объекты должны быть относительно очевидными, если вы знакомы с этим ландшафтом. Лично я считаю, что возвращение словаря должно быть вашим последним средством, если только то, что вы возвращаете, НЕ ДОЛЖНО быть отображением пар имя / значение. Я видел слишком много неаккуратного кода, где все, что передается и выходит из методов, - это просто словарь. Это лениво. Мне нравятся языки с динамической типизацией, но мне также нравится, чтобы мой код был ясным и логичным. Запихивание всего в словарь может быть удобством, которое скрывает смысл.
jaydel 07
5

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

Следовательно, придерживайтесь класса.

Стигма
источник
2
Я полностью не согласен. Нет причин ограничивать использование словарей вещами, которые нужно перебирать. Они нужны для отображения.
Катриэль
1
Прочтите, пожалуйста, повнимательнее. Я сказал, что может захотеть зацикливаться , но не будет . Для меня использование словаря предполагает большое функциональное сходство между ключами и значениями. Например, имена с возрастом. Использование класса означало бы, что различные «ключи» имеют значения с совершенно разными значениями. Например, возьмем класс PErson. Здесь «имя» может быть строкой, а «друзья» - списком, словарем или другим подходящим объектом. Вы бы не перебирали все эти атрибуты при обычном использовании этого класса.
Стигма
1
Я думаю, что различие между классами и словарями в Python нечеткое из-за того, что первые реализованы с использованием вторых (не считая «слотов»). Я знаю, что это немного смутило меня, когда я впервые изучал язык (наряду с тем фактом, что классы были объектами и, следовательно, экземплярами некоторых загадочных метаклассов).
Мартино
4

Если все, что вы хотите достичь, - это синтаксическая конфетка, например, obj.bla = 5вместо obj['bla'] = 5, особенно если вам нужно много повторять это, возможно, вы захотите использовать какой-то простой класс контейнера, как в предложении martineaus. Тем не менее, код там довольно раздутый и излишне медленный. Вы можете сделать это просто так:

class AttrDict(dict):
    """ Syntax candy """
    __getattr__ = dict.__getitem__
    __setattr__ = dict.__setitem__
    __delattr__ = dict.__delitem__

Другой причиной для перехода на namedtuples или класс __slots__может быть использование памяти. Dicts требуют значительно больше памяти, чем типы списков, так что об этом стоит подумать.

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

Майкл
источник
types.SimpleNamespace(доступно с Python 3.3) является альтернативой пользовательскому AttrDict.
Кристиан Чупиту,
4

Возможно, получится съесть свой торт и съесть его. Другими словами, вы можете создать что-то, что обеспечивает функциональность как класса, так и экземпляра словаря. См. Рецепт Dɪᴄᴛɪᴏɴᴀʀʏ ᴡɪᴛʜ ᴀᴛᴛʀɪʙᴜᴛᴇ-sᴛʏʟᴇ ss от ActiveState и комментарии о том, как это сделать.

Если вы решите использовать обычный класс, а не подкласс, я обнаружил, что рецепт Tʜᴇ sɪᴍᴘʟᴇ ʙᴜᴛ ʜᴀɴᴅʏ "ᴄᴏʟʟᴇᴄᴛᴏʀ ᴏғ ᴀ ʙᴜɴᴄʜ ᴏғ ɴᴀᴍᴇᴅ sᴛᴜғғ" ᴄʟᴀss (от Алекса Мартелли) очень гибкий и полезный для такого рода вещей. похоже, что вы делаете (т.е. создаете относительно простой агрегатор информации). Поскольку это класс, вы можете легко расширить его функциональность, добавив методы.

Наконец, следует отметить, что имена членов класса должны быть допустимыми идентификаторами Python, а ключи словаря - нет, поэтому словарь предоставит большую свободу в этом отношении, потому что ключи могут быть чем угодно хэшируемыми (даже тем, что не является строкой).

Обновить

Класс object(у которого нет __dict__) названный подкласс SimpleNamespace(у которого он есть) был добавлен в typesмодуль Python 3.3 и является еще одной альтернативой.

Мартино
источник
-2
class ClassWithSlotBase:
    __slots__ = ('a', 'b',)

def __init__(self):
    self.a: str = "test"
    self.b: float = 0.0


def test_type_hint(_b: float) -> None:
    print(_b)


class_tmp = ClassWithSlotBase()

test_type_hint(class_tmp.a)

Я рекомендую класс. Если вы используете класс, вы можете получить подсказку типа, как показано. И класс поддерживает автоматическое завершение, когда класс является аргументом функции.

введите описание изображения здесь

Низкое качество
источник