Powinienem użyć klasy czy słownika?

103

Mam klasę, która zawiera tylko pola i nie ma metod, na przykład:

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)
        # ...

Można to łatwo przełożyć na dyktando. Klasa jest bardziej elastyczna dla przyszłych dodatków i może być szybka z __slots__. Czy zatem korzystanie z dyktowania przyniesie korzyści? Czy dyktand byłby szybszy niż klasa? I szybciej niż klasa z automatami?

demon
źródło
2
Zawsze używam słowników do przechowywania danych i wygląda to na przypadek użycia. w niektórych przypadkach wyprowadzanie klasy z dictmoże mieć sens. niezła zaleta: kiedy debugujesz, po prostu powiedz, print(request)a zobaczysz wszystkie informacje o stanie. przy bardziej klasycznym podejściu będziesz musiał napisać własne __str__metody, co jest do bani, jeśli musisz to robić cały czas.
przepływ
Jeśli zajęcia mają sens i są jasne dla innych, dlaczego nie? Ponadto, jeśli zdefiniujesz na przykład wiele klas ze wspólnymi interfejsami, dlaczego nie? Ale Python nie obsługuje potężnych koncepcji obiektowych, takich jak C ++.
MasterControlProgram
4
@Ralf Jakie OOP nie obsługuje Python?
qwr

Odpowiedzi:

33

Dlaczego miałbyś zrobić z tego słownik? Jaka jest zaleta? Co się stanie, jeśli później zechcesz dodać kod? Gdzie poszedłby twój __init__kod?

Klasy służą do grupowania powiązanych danych (i zwykle kodu).

Słowniki służą do przechowywania relacji klucz-wartość, gdzie zwykle wszystkie klucze są tego samego typu, a wszystkie wartości są również tego samego typu. Czasami mogą być przydatne do grupowania danych, gdy nie wszystkie nazwy kluczy / atrybutów są znane z góry, ale często jest to znak, że coś jest nie tak z projektem.

Niech to będzie klasa.

adw
źródło
Stworzyłbym rodzaj metody fabrycznej, tworząc dyktando zamiast metody klasy __init__. Ale masz rację: oddzieliłbym się od rzeczy, które należą do siebie.
deamon
89
nie mogłem się z tobą nie zgodzić: słowniki, zbiory, listy i krotki są po to, aby łączyć powiązane dane. w żaden sposób nie istnieje przypuszczenie, że wartości słownika powinny lub muszą być tego samego typu danych, wręcz przeciwnie. na wielu listach i zestawach wartości będą tego samego typu, ale dzieje się tak głównie dlatego, że lubimy je razem wyliczać. tak naprawdę uważam, że powszechne stosowanie klas do przechowywania danych jest nadużyciem OOP; kiedy myślisz o problemach z serializacją, możesz łatwo zobaczyć, dlaczego.
przepływ
4
Jest to programowanie zorientowane na OBJECT, a nie zorientowane na klasę: mamy do czynienia z obiektami. Obiekt charakteryzuje się 2 (3) właściwościami: 1. stan (elementy) 2. zachowanie (metody) i 3. instancję można opisać kilkoma słowami. Dlatego klasy służą do łączenia razem stanu i zachowania.
friendzis
14
Oznaczyłem to, ponieważ w miarę możliwości zawsze powinieneś domyślnie korzystać z prostszej struktury danych. W takim przypadku słownik jest wystarczający do zamierzonego celu. Pytanie where would your __init__ code go?dotyczy. Może to przekonać mniej doświadczonego programistę, że należy używać tylko klas, ponieważ w słowniku nie jest używana metoda init . Absurdalny.
Lloyd Moore
1
@Ralf Klasa Foo to tylko typ, jak int i string. Czy przechowujesz wartość w postaci typu integer lub zmiennej foo typu integer? Subtelna, ale ważna różnica semantyczna. W językach takich jak C to rozróżnienie nie jest zbyt istotne poza kręgami akademickimi. Chociaż większość języków obiektowych obsługuje zmienne klasowe, co sprawia, że ​​rozróżnienie między klasą a obiektem / instancją jest niezwykle istotne - czy przechowujesz dane w klasie (współdzielone przez wszystkie instancje) czy w określonym obiekcie? Nie
daj
44

Użyj słownika, chyba że potrzebujesz dodatkowego mechanizmu klasy. Możesz również użyć namedtuplepodejścia hybrydowego:

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

Różnice w wydajności tutaj będą minimalne, chociaż zdziwiłbym się, gdyby słownik nie był szybszy.

Katriel
źródło
15
„Różnice w wydajności tutaj będą minimalne , chociaż zdziwiłbym się, gdyby słownik nie był znacznie szybszy”. To się nie liczy. :)
mipadi
1
@mipadi: to prawda. Teraz naprawione: p
Katriel
Dykt jest 1,5 raza szybszy niż nazwany trzykrotny i dwa razy szybszy niż klasa bez slotów. Sprawdź mój post na tej odpowiedzi.
alexpinho98
@ alexpinho98: Bardzo się starałem znaleźć „post”, do którego się odnosisz, ale nie mogę go znaleźć! czy możesz podać adres URL. Dzięki!
Dan Oblinger
@DanOblinger Zakładam, że ma na myśli swoją odpowiedź poniżej.
Adam Lewis
37

Klasa w Pythonie to dykt pod spodem. Zachowanie klasy powoduje pewne obciążenie, ale bez profilera nie da się tego zauważyć. W tym przypadku uważam, że skorzystasz z zajęć, ponieważ:

  • Cała twoja logika żyje w jednej funkcji
  • Jest łatwy w aktualizacji i pozostaje zamknięty
  • Jeśli zmienisz cokolwiek później, możesz łatwo zachować ten sam interfejs
Bruce Armstrong
źródło
Cała twoja logika nie opiera się na jednej funkcji. Klasa to krotka wspólnego stanu i zwykle jedna lub więcej metod. Jeśli zmienisz klasę, nie masz żadnych gwarancji co do jej interfejsu.
Lloyd Moore
25

Myślę, że użycie każdego z nich jest dla mnie zbyt subiektywne, abym mógł się nad tym zastanowić, więc po prostu trzymam się liczb.

Porównałem czas potrzebny do utworzenia i zmiany zmiennej w dict, klasie new_style i klasie new_style ze slotami.

Oto kod, którego użyłem do przetestowania (jest trochę niechlujny, ale spełnia swoje zadanie).

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')))

A oto wynik ...

Tworzę…

Dict: 0.817466186345
Class: 1.60829183597
Class_with_slots: 1.28776730003

Zmiana zmiennej ...

Dict: 0.0735140918748
Class: 0.111714198313
Class_with_slots: 0.10618612142

Tak więc, jeśli przechowujesz tylko zmienne, potrzebujesz szybkości i nie będzie wymagało to wykonywania wielu obliczeń, zalecam użycie dyktowania (zawsze możesz po prostu utworzyć funkcję, która wygląda jak metoda). Ale jeśli naprawdę potrzebujesz zajęć, pamiętaj - zawsze używaj __ miejsc __ .

Uwaga:

Przetestowałem „Klasę” z obydwoma new_style i klas old_style. Okazuje się, że klasy w starym stylu są szybsze w tworzeniu, ale wolniej się je modyfikuje (niewiele, ale istotne, jeśli tworzysz wiele klas w ciasnej pętli (wskazówka: robisz to źle)).

Również czasy tworzenia i zmieniania zmiennych mogą się różnić na twoim komputerze, ponieważ mój jest stary i powolny. Upewnij się, że sam to przetestujesz, aby zobaczyć „prawdziwe” wyniki.

Edytować:

Później przetestowałem namedtuple: nie mogę go zmodyfikować, ale utworzenie 10000 próbek (lub czegoś podobnego) zajęło 1,4 sekundy, więc słownik jest rzeczywiście najszybszy.

Jeśli zmienię funkcję dict tak, aby zawierała klucze i wartości oraz aby zwracała dict zamiast zmiennej zawierającej dict, kiedy ją utworzę, otrzymam 0,65 zamiast 0,8 sekundy.

class Foo(dict):
    pass

Tworzenie jest jak klasa ze szczelinami, a zmiana zmiennej jest najwolniejsza (0,17 sekundy), więc nie używaj tych klas . wybierz dict (prędkość) lub klasę pochodną obiektu („cukierek składniowy”)

alexpinho98
źródło
Chciałbym zobaczyć liczby dla podklasy dict(bez nadpisanych metod, jak sądzę?). Czy działa tak samo, jak klasa w nowym stylu, która została napisana od podstaw?
Benjamin Hodgson
12

Zgadzam się z @adw. Nigdy nie przedstawiłbym „obiektu” (w sensie OO) w słowniku. Słowniki agregują pary nazwa / wartość. Klasy reprezentują obiekty. Widziałem kod, w którym obiekty są reprezentowane za pomocą słowników i nie jest jasne, jaki jest rzeczywisty kształt rzeczy. Co się dzieje, gdy nie ma określonej nazwy / wartości? Co ogranicza klientowi możliwość wkładania czegokolwiek do środka. Albo próby wydobycia czegokolwiek. Kształt rzeczy powinien być zawsze jasno określony.

Podczas korzystania z Pythona ważne jest, aby budować z dyscypliną, ponieważ język umożliwia autorowi strzelanie sobie w stopę na wiele sposobów.

jaydel
źródło
9
Odpowiedzi na SO są sortowane według głosów, a odpowiedzi z taką samą liczbą głosów są sortowane losowo. Proszę więc wyjaśnić, kogo masz na myśli, mówiąc „ostatni plakat”.
Mike DeSimone
4
A skąd wiesz, że coś, co ma tylko pola i nie ma funkcjonalności, jest „obiektem” w sensie OO?
Muhammad Alkarouri
1
Mój papierkiem lakmusowym brzmi: „czy struktura tych danych jest naprawiona?”. Jeśli tak, użyj obiektu, w przeciwnym razie dyktu. Odejście od tego spowoduje tylko zamieszanie.
weberc2
Musisz przeanalizować kontekst problemu, który rozwiązujesz. Przedmioty powinny być stosunkowo oczywiste, gdy już zaznajomisz się z tym krajobrazem. Osobiście uważam, że zwrócenie słownika powinno być ostatecznością, chyba że to, co zwracasz, POWINNO być mapowaniem par nazwa / wartość. Widziałem zbyt wiele niechlujnego kodu, w którym wszystko przekazywane i przekazywane z metod jest tylko słownikiem. To jest leniwe. Uwielbiam języki dynamicznie wpisywane, ale podoba mi się też, że mój kod jest przejrzysty i logiczny. Umieszczenie wszystkiego w słowniku może być wygodą, która ukrywa znaczenie.
jaydel
5

Poleciłbym zajęcia, ponieważ są to różnego rodzaju informacje związane z prośbą. Gdyby ktoś korzystał ze słownika, spodziewałbym się, że przechowywane dane będą o wiele bardziej podobne. Wytyczna, do której sam się kieruję, jest taka, że ​​jeśli chcę zapętlić cały zestaw par klucz-> wartość i coś zrobić, używam słownika. W przeciwnym razie dane najwyraźniej mają znacznie większą strukturę niż podstawowe mapowanie klucz-> wartość, co oznacza, że ​​prawdopodobnie lepszą alternatywą byłaby klasa.

Dlatego trzymaj się klasy.

Piętno
źródło
2
Całkowicie się nie zgadzam. Nie ma powodu, aby ograniczać użycie słowników do rzeczy do iteracji. Służą do tworzenia map.
Katriel
1
Przeczytaj uważnie, proszę. Powiedziałem może chcieć pętli nad , nie będzie . Dla mnie użycie słownika oznacza, że ​​istnieje duże podobieństwo funkcjonalne między kluczami a wartościami. Na przykład imiona z wiekiem. Użycie klasy oznaczałoby, że różne „klucze” mają wartości o bardzo różnych znaczeniach. Na przykład weź klasę PErson. W tym przypadku „nazwa” będzie ciągiem, a „przyjaciele” może być listą, słownikiem lub innym odpowiednim obiektem. Nie zapętliłbyś wszystkich tych atrybutów w ramach normalnego użycia tej klasy.
Stigma
1
Myślę, że rozróżnienie między klasami i słownikami jest niewyraźne w Pythonie przez fakt, że te pierwsze są zaimplementowane przy użyciu tych drugich (bez względu na to, że są to „sloty”). Wiem, że trochę mnie to zdezorientowało, kiedy po raz pierwszy uczyłem się języka (razem z faktem, że klasy były obiektami, a więc instancjami jakichś tajemniczych metaklas).
martineau
4

Jeśli wszystko, co chcesz achive składnia jest jak cukierek obj.bla = 5zamiast obj['bla'] = 5, szczególnie jeśli trzeba powtórzyć, że dużo, to może chcesz używać jakiś zwykły klasy Pojemnik martineaus sugestii. Niemniej jednak kod jest dość rozdęty i niepotrzebnie wolny. Możesz to uprościć:

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

Innym powodem przejścia na namedtuples lub klasę z __slots__może być użycie pamięci. Dykty wymagają znacznie więcej pamięci niż typy list, więc warto o tym pomyśleć.

W każdym razie, w Twoim konkretnym przypadku wydaje się, że nie ma motywacji do odejścia od obecnej implementacji. Wydaje się, że nie zarządzasz milionami tych obiektów, więc nie są wymagane żadne typy wyprowadzane z listy. I faktycznie zawiera pewną logikę funkcjonalną w ramach __init__, więc nie powinieneś też mieć z tym AttrDict.

Michał
źródło
types.SimpleNamespace(dostępne od Pythona 3.3) jest alternatywą dla niestandardowego AttrDict.
Cristian Ciupitu
4

Możliwe, że będzie można mieć ciasto i je też zjeść. Innymi słowy, możesz stworzyć coś, co zapewnia funkcjonalność zarówno instancji klasy, jak i słownika. Zobacz przepis na Dɪᴄᴛɪᴏɴᴀʀʏ ᴡɪᴛʜ ᴀᴛᴛʀɪʙᴜᴛᴇ-sᴛʏʟᴇ ᴀᴄᴄᴇss ActiveState i komentarze na temat sposobów zrobienia tego.

Jeśli zdecydujesz się użyć zwykłej klasy zamiast podklasy, stwierdziłem, że przepis Tʜᴇ sɪᴍᴘʟᴇ ʙᴜᴛ ʜᴀɴᴅʏ "ᴄᴏʟʟᴇᴄᴛᴏʀ ᴏғ ᴀ ʙᴜɴᴄʜ ᴏғ ɴᴀᴍᴇᴅ sᴛᴜғғ" ᴄʟᴀss (autorstwa Alexa Martellego ) jest bardzo elastyczny i przydatny do tego typu rzeczy wygląda na to, że robisz (tj. stwórz względnie prosty agregator informacji). Ponieważ jest to klasa, możesz łatwo rozszerzyć jej funkcjonalność, dodając metody.

Na koniec należy zauważyć, że nazwy członków klas muszą być legalnymi identyfikatorami Pythona, ale klucze słownikowe nie - więc słownik zapewniłby większą swobodę w tym względzie, ponieważ klucze mogą być hashowane dowolnie (nawet coś, co nie jest ciągiem znaków).

Aktualizacja

Klasa object(która nie ma __dict__) podklasy o nazwie SimpleNamespace(która ją ma) została dodana do typesmodułu Python 3.3 i jest kolejną alternatywą.

martineau
źródło