Jaki jest związek między modelem danych Pythona a wbudowanymi funkcjami?

Kiedy czytam odpowiedzi Pythona na Stack Overflow, nadal widzę kilka osób mówiących użytkownikom, Aby używali bezpośrednio metod specjalnych metod modelu danych lub atrybutów.

Widzę wtedy sprzeczne Rady (czasami od siebie) mówiące, aby tego nie robić, a zamiast tego używać wbudowanych funkcji i operatorów bezpośrednio.

Dlaczego? Jaka jest zależność pomiędzy specjalnymi metodami "dunder" a atrybutami modelu danych Pythona i wbudowane funkcje ?

Kiedy mam używać specjalnych nazw?

Author: Community, 2016-10-27

2 answers

Jaki jest związek pomiędzy modelem danych Pythona a wbudowanymi funkcjami?

  • wbudowane operatory używają bazowych metod lub atrybutów datamodel.
  • wbudowane i operatory mają bardziej eleganckie zachowanie i ogólnie są bardziej kompatybilne do przodu.
  • specjalne metody datamodelu są semantycznie niepublicznymi interfejsami.
  • wbudowane operatory i operatory językowe mają być interfejsem użytkownika do zachowania realizowane specjalnymi metodami.

W związku z tym, powinieneś używać wbudowanych funkcji i operatorów tam, gdzie to możliwe, zamiast specjalnych metod i atrybutów datamodelu.

[79]}semantycznie wewnętrzne interfejsy API są bardziej narażone na zmiany niż interfejsy publiczne. Chociaż Python nie uważa niczego za "prywatny" i ujawnia wewnętrzne elementy, nie oznacza to, że nadużywanie tego dostępu nie jest dobrym pomysłem. Czynienie tego wiąże się z następującymi zagrożeniami:
  • możesz znaleźć podczas aktualizacji programu wykonywalnego Pythona lub przełączania się na inne implementacje Pythona (takie jak PyPy, IronPython lub Jython, lub innej nieprzewidzianej implementacji) pojawia się więcej zmian.)
  • twoi koledzy prawdopodobnie źle pomyślą o twoich umiejętnościach językowych i sumienności, i uznają to za zapach kodu, doprowadzając Ciebie i resztę twojego kodu do większej kontroli.
  • wbudowane funkcje są łatwe do przechwycenia zachowania. Stosowanie specjalnych metod bezpośrednio ogranicza moc Twojego Pythona do introspekcji i debugowania.

W głębi

Wbudowane funkcje i operatory wywołują specjalne metody i używają specjalnych atrybutów w modelu danych Pythona. Są czytelną i konserwowalną okleiną, która ukrywa wnętrza przedmiotów. Ogólnie rzecz biorąc, użytkownicy powinni używać wbudowanych i operatorów podanych w języku, a nie wywoływać specjalnych metod lub używać specjalnych atrybutów bezpośrednio.

Wbudowane funkcje i operatory mogą również mieć zachowanie awaryjne lub bardziej eleganckie niż bardziej prymitywne metody specjalne datamodel. Na przykład:

  • next(obj, default) pozwala na podanie domyślnej wartości zamiast podnoszenia StopIteration, gdy iterator się wyczerpie, podczas gdy obj.__next__() nie.
  • str(obj) powróci do obj.__repr__(), gdy obj.__str__() nie jest dostępna - podczas gdy wywołanie obj.__str__() bezpośrednio wywoła błąd atrybutu.
  • obj != other fallsback to not obj == other w Pythonie 3 Gdy Nie __ne__ - wywołanie obj.__ne__(other) nie zajęłoby zaleta tego.

(wbudowane funkcje mogą być również łatwo przysłonięte, jeśli jest to konieczne lub pożądane, na globalnym zasięgu modułu lub modułu builtins, aby jeszcze bardziej dostosować zachowanie.)

Mapowanie wbudowanych operatorów do datamodelu

Tutaj jest mapowanie, z uwagami, wbudowanych funkcji i operatorów do odpowiednich specjalnych metod i atrybutów, których używają lub zwracają-zauważ, że zwykle zasada jest taka, że wbudowana funkcja Zwykle mapuje do nie jest to jednak na tyle spójne, aby uzasadnić podanie tej mapy poniżej: {]}

builtins/     special methods/
operators  -> datamodel               NOTES (fb == fallback)

repr(obj)     obj.__repr__()          provides fb behavior for str
str(obj)      obj.__str__()           fb to __repr__ if no __str__
bytes(obj)    obj.__bytes__()         Python 3 only
unicode(obj)  obj.__unicode__()       Python 2 only
format(obj)   obj.__format__()        format spec optional.
hash(obj)     obj.__hash__()
bool(obj)     obj.__bool__()          Python 3, fb to __len__
bool(obj)     obj.__nonzero__()       Python 2, fb to __len__
dir(obj)      obj.__dir__()
vars(obj)     obj.__dict__            does not include __slots__
type(obj)     obj.__class__           type actually bypasses __class__ -
                                      overriding __class__ will not affect type
help(obj)     obj.__doc__             help uses more than just __doc__
len(obj)      obj.__len__()           provides fb behavior for bool
iter(obj)     obj.__iter__()          fb to __getitem__ w/ indexes from 0 on
next(obj)     obj.__next__()          Python 3
next(obj)     obj.next()              Python 2
reversed(obj) obj.__reversed__()      fb to __len__ and __getitem__
other in obj  obj.__contains__(other) fb to __iter__ then __getitem__
obj == other  obj.__eq__(other)
obj != other  obj.__ne__(other)       fb to not obj.__eq__(other) in Python 3
obj < other   obj.__lt__(other)       get >, >=, <= with @functools.total_ordering
complex(obj)  obj.__complex__()
int(obj)      obj.__int__()
float(obj)    obj.__float__()
round(obj)    obj.__round__()
abs(obj)      obj.__abs__()

Moduł operator ma length_hint, który ma zabezpieczenie zaimplementowane odpowiednią specjalną metodą, jeśli __len__ nie jest zaimplementowany:

length_hint(obj)  obj.__length_hint__() 

Przerywane Poszukiwania

Przerywane wyszukiwania są kontekstowe. Bez specjalnej implementacji metod, najpierw poszukaj w hierarchii klas deskryptorów danych( takich jak właściwości i szczeliny), a następnie w instancji __dict__ (na przykład zmienne), następnie w hierarchii klas dla deskryptorów Nie-danych (jak metody). Specjalne metody implementują następujące zachowania:

obj.attr      obj.__getattr__('attr')       provides fb if dotted lookup fails
obj.attr      obj.__getattribute__('attr')  preempts dotted lookup
obj.attr = _  obj.__setattr__('attr', _)    preempts dotted lookup
del obj.attr  obj.__delattr__('attr')       preempts dotted lookup

Deskryptory

Deskryptory są nieco zaawansowane - możesz pominąć te wpisy i wrócić później-przypomnij sobie, że instancja deskryptora znajduje się w hierarchii klas (jak metody, sloty i właściwości). Deskryptor danych implementuje albo __set__ albo __delete__:

obj.attr        descriptor.__get__(obj, type(obj)) 
obj.attr = val  descriptor.__set__(obj, val)
del obj.attr    descriptor.__delete__(obj)

Gdy klasa jest instancjowana (zdefiniowana), następująca metoda deskryptora __set_name__ jest wywoływana, jeśli jakiś deskryptor ma ją poinformować deskryptor o jego nazwie atrybutu. (Jest to nowość w Pythonie 3.6.) cls jest taki sam jak type(obj) powyżej, a 'attr' oznacza nazwę atrybutu:

class cls:
    @descriptor_type
    def attr(self): pass # -> descriptor.__set_name__(cls, 'attr') 

Items (notacja dolna)

Zapis dolny jest również kontekstowy:]}
obj[name]         -> obj.__getitem__(name)
obj[name] = item  -> obj.__setitem__(name, item)
del obj[name]     -> obj.__delitem__(name)

Specjalny przypadek dla podklas dict, __missing__ jest wywoływane, jeśli __getitem__ nie znajdzie klucza:

obj[name]         -> obj.__missing__(name)  

Operatory

Istnieją również specjalne metody dla +, -, *, @, /, //, %, divmod(), pow(), **, <<, >>, &, ^, | operatory, na przykład:

obj + other   ->  obj.__add__(other), fallback to other.__radd__(obj)
obj | other   ->  obj.__or__(other), fallback to other.__ror__(obj)
W zależności od tego, która z tych wartości jest większa, w zależności od tego, która z tych wartości jest większa.]}
obj += other  ->  obj.__iadd__(other)
obj |= other  ->  obj.__ior__(other)

I operacje jednostkowe:

+obj          ->  obj.__pos__()
-obj          ->  obj.__neg__()
~obj          ->  obj.__invert__()

Menedżery Kontekstu

Menedżer kontekstu definiuje __enter__, która jest wywoływana przy wprowadzaniu bloku kodu (jego wartość zwracana, Zwykle self, jest aliasowana przez as), i __exit__, która jest gwarantowana przy opuszczaniu bloku kodu, z informacjami o wyjątkach.

with obj as cm:     ->  cm = obj.__enter__()
    raise Exception('message')
->  obj.__exit__(Exception, Exception('message'), traceback_object)

Jeśli __exit__ otrzyma wyjątek i następnie zwraca wartość false, po opuszczeniu metody będzie ją ponownie uruchamiać.

Jeśli nie ma wyjątku, __exit__ otrzymujeNone dla tych trzech argumentów, a wartość zwracana jest bez znaczenia:

with obj:           ->  obj.__enter__()
    pass
->  obj.__exit__(None, None, None)

Niektóre Metaklasy Specjalne Metody

Podobnie, klasy mogą mieć specjalne metody (ze swoich metaklas), które wspierają abstrakcyjne klasy bazowe:]}
isinstance(obj, cls) -> cls.__instancecheck__(obj)
issubclass(sub, cls) -> cls.__subclasscheck__(sub)

Ważne jest to, że podczas gdy builtins jak next i bool nie zmieniają się między Pythonem 2 i 3, podstawowe nazwy implementacji zmieniają się.

W ten sposób korzystanie z wbudowanych oferuje również większą kompatybilność.

Kiedy mam używać specjalnych nazw?

W Pythonie Nazwy zaczynające się od podkreślenia są semantycznie niepublicznymi nazwami dla użytkowników. Podkreśleniem jest sposób Stwórcy powiedzenie: "Ręce precz, nie dotykaj."

Jest to nie tylko kulturowe, ale także w traktowaniu API przez Pythona. gdy pakiet __init__.py używa import * do dostarcza API z podpakietu, jeśli podpakiet nie zawiera __all__, wyklucza Nazwy zaczynające się od podkreślenia. Nie można również wykluczyć podpakietu __name__.

Narzędzia autocompletion IDE są mieszane pod względem nazw zaczynających się od podkreślenia jako Niepubliczne. Jednak bardzo doceniam, że nie widzę __init__, __new__, __repr__, __str__, __eq__, itd. (ani żaden z użytkowników nie stworzył interfejsów niepublicznych) kiedy wpisuję nazwę obiektu i kropka.

Tak twierdzę:

Specjalne metody "dunder" nie są częścią publicznego interfejsu. Unikaj używania ich bezpośrednio.

Kiedy ich używać?

Głównym przypadkiem użycia jest implementacja własnego obiektu lub podklasy wbudowanego obiektu.

Staraj się używać ich tylko wtedy, gdy jest to absolutnie konieczne. Oto kilka przykładów:

Użyj specjalnego atrybutu __name__ na funkcjach lub klasach

Kiedy udekorujemy funkcja, zazwyczaj otrzymujemy w zamian funkcję wrappera, która ukrywa pomocne informacje o funkcji. Użyjemy dekoratora @wraps(fn), aby upewnić się, że nie utracimy tej informacji, ale jeśli potrzebujemy nazwy funkcji, musimy użyć atrybutu __name__ bezpośrednio:

from functools import wraps

def decorate(fn): 
    @wraps(fn)
    def decorated(*args, **kwargs):
        print('calling fn,', fn.__name__) # exception to the rule
        return fn(*args, **kwargs)
    return decorated

Podobnie, wykonuję następujące czynności, gdy potrzebuję nazwy klasy obiektu w metodzie (używanej na przykład w __repr__):

def get_class_name(self):
    return type(self).__name__
          # ^          # ^- must use __name__, no builtin e.g. name()
          # use type, not .__class__

Używanie specjalnych atrybutów do pisania niestandardowych klas lub podklasowanych builtins

Kiedy chcemy zdefiniować własne zachowanie, musimy użyć nazw modeli danych.

To ma sens, ponieważ jesteśmy wykonawcami, te atrybuty nie są dla nas prywatne.

class Foo(object):
    # required to here to implement == for instances:
    def __eq__(self, other):      
        # but we still use == for the values:
        return self.value == other.value
    # required to here to implement != for instances:
    def __ne__(self, other): # docs recommend for Python 2.
        # use the higher level of abstraction here:
        return not self == other  

Jednak, nawet w tym przypadku, nie używamy self.value.__eq__(other.value) lub not self.__eq__(other) (zobacz moją odpowiedź tutaj , aby udowodnić, że ta ostatnia może prowadzić do nieoczekiwanego zachowania.) Zamiast tego powinniśmy użyć wyższego poziomu abstrakcji.

Kolejny punkt, w którym musimy użyć specjalnego nazwy metod są wtedy, gdy jesteśmy w implementacji dziecka i chcemy przekazać je rodzicowi. Na przykład:

class NoisyFoo(Foo):
    def __eq__(self, other):
        print('checking for equality')
        # required here to call the parent's method
        return super(NoisyFoo, self).__eq__(other) 

Podsumowanie

Specjalne metody pozwalają użytkownikom zaimplementować interfejs dla wewnętrznych obiektów.

Używaj wbudowanych funkcji i operatorów, gdzie tylko możesz. Używaj specjalnych metod tylko wtedy, gdy nie ma udokumentowanego publicznego API.

 34
Author: Aaron Hall,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-09-26 19:50:54

Pokażę użycie, o którym najwyraźniej nie pomyślałeś, skomentuję przykłady, które pokazałeś i argumentuję przeciwko roszczeniu o prywatności z twojej własnej odpowiedzi.


Zgadzam się z Twoją własną odpowiedzią, że na przykład len(a) powinno być używane, a nie a.__len__(). Ująłbym to tak: len istnieje więc możemy go używać, a __len__ istnieje więc len możemy go używać . Lub jednak to naprawdę działa wewnętrznie, ponieważ len(a) może być rzeczywiście dużo szybciej , przynajmniej na przykład dla list i ciągi:

>>> timeit('len(a)', 'a = [1,2,3]', number=10**8)
4.22549770486512
>>> timeit('a.__len__()', 'a = [1,2,3]', number=10**8)
7.957335462257106

>>> timeit('len(s)', 's = "abc"', number=10**8)
4.1480574509332655
>>> timeit('s.__len__()', 's = "abc"', number=10**8)
8.01780160432645

Ale poza definiowaniem tych metod w moich klasach do użycia przez wbudowane funkcje i operatory, czasami używam ich również w następujący sposób:]}

Powiedzmy, że muszę dać funkcję filtra jakiejś funkcji i chcę użyć zestawu s jako filtra. Nie zamierzam tworzyć dodatkowej funkcji lambda x: x in s ani def f(x): return x in s. Nie. Mam już idealną funkcję, którą mogę wykorzystać: metodę __contains__ zestawu. To prostsze i bardziej bezpośrednie. I jeszcze szybciej, jak pokazano tutaj (ignoruj, że zapisuję to jako f tutaj, to tylko dla tego czasu demo):

>>> timeit('f(2); f(4)', 's = {1, 2, 3}; f = s.__contains__', number=10**8)
6.473739433621368
>>> timeit('f(2); f(4)', 's = {1, 2, 3}; f = lambda x: x in s', number=10**8)
19.940786514456924
>>> timeit('f(2); f(4)', 's = {1, 2, 3}\ndef f(x): return x in s', number=10**8)
20.445680107760325

Więc podczas gdy nie bezpośrednio wywołam magiczne metody jak s.__contains__(x), czasami przekazuję je gdzieś jaksome_function_needing_a_filter(s.__contains__). I myślę, że to jest całkowicie w porządku, i lepiej niż alternatywa lambda / def.


Moje przemyślenia na temat przykładów, które pokazałeś:

  • przykład 1 : zapytany, jak uzyskać rozmiar listy, odpowiedział items.__len__(). Nawet bez żadnego rozumowania. My werdykt: to jest po prostu złe. Powinno być len(items).
  • przykład 2 : pierwsza wzmianka d[key] = value! A następnie dodaje {[18] } z rozumowaniem "jeśli na klawiaturze brakuje klawiszy z nawiasami kwadratowymi" , co rzadko ma zastosowanie i w co wątpię było poważne. Myślę, że to była tylko stopa w drzwiach na ostatni punkt, wspominając, że w ten sposób możemy wspierać składnię nawiasów kwadratowych w naszych klasach. Co zamienia go z powrotem w sugestię użycia kwadratu nawiasy.
  • Przykład 3 : Sugeruje obj.__dict__. Źle, jak przykład __len__. Ale podejrzewam, że po prostu nie wiedział vars(obj), i mogę to zrozumieć, ponieważ vars jest mniej powszechny/znany, a nazwa różni się od "dict" w __dict__.
  • Przykład 4 : Sugeruje __class__. Powinno być type(obj). Podejrzewam, że jest podobna do historii __dict__, chociaż myślę, że type jest bardziej znana.

O prywatności: we własnej odpowiedzi mówisz, że te metody są "semantycznie prywatne". Zdecydowanie się nie zgadzam. Pojedyncze i podwójne przodujące podkreślniki są do tego przeznaczone, ale nie są to specjalne metody "Dunder/magic" modelu danych z podwójnym wiodącym+kończącym podkreślnikami.

  • dwie rzeczy, których używasz jako argumentów to importowanie zachowania i autocompletion IDE. Ale importowanie i te specjalne metody to różne obszary, a jeden IDE próbowałem (popularny PyCharm) nie zgadza się z Tobą. Stworzyłem klasę / obiekt za pomocą metod _foo i __bar__ i wtedy autocompletion nie zaoferował _foo, ale zaoferował . A kiedy i tak korzystałem z obu metod, PyCharm ostrzegał mnie tylko o _foo (nazywając go "chronionym członkiem"), , a nie o o __bar__.
  • PEP 8 mówi 'słaby" wewnętrzny wskaźnik " jawnie dla pojedynczego podkreślenia wiodącego, a jawnie dla podwójnego wiodącego podkreślenia wspomina nazwę mangling, a później wyjaśnia, że jest to dla "atrybuty, które nie chcę, aby podklasy używały " . Ale komentarz o Double leading+trailing underscores nic takiego nie mówi.
  • strona modelu danych , do której sam link, mówi, że te specjalne nazwy metod to "podejście Pythona do przeciążania operatorów". Nie ma tu nic o prywatności. Słowa private/privacy / protected nie pojawiają się nigdzie na tej stronie.

    Polecam również przeczytać ten artykuł autorstwa Andrzeja Montalti o tych metodach, podkreślając, że "konwencja dunder jest przestrzenią nazw zarezerwowaną dla podstawowego zespołu Pythona" i {66]}"nigdy, przenigdy, nie wymyślaj własnych dunderów"{67]} ponieważ {66]} "rdzenny zespół Pythona zarezerwował nieco brzydką przestrzeń nazw dla siebie" {67]}. W przeciwieństwie do innych postaci, które nie są w stanie stworzyć magicznej magii, nie są w stanie stworzyć magicznej magii. Myślę, że Andrew jest na miejscu - to tylko brzydka przestrzeń nazw podstawowego zespołu. I to dla cel przeciążenia operatora, nie o prywatność (nie o to chodzi, ale o moją i stronę modelu danych).

Oprócz artykułu Andrew sprawdziłem jeszcze kilka innych o tych"magicznych"/" dunder " metodach i nie znalazłem żadnej z nich mówiącej o prywatności w ogóle. Nie o to tu chodzi.

Ponownie powinniśmy użyć len(a), a nie a.__len__(). Ale nie z powodu prywatności.

 10
Author: Stefan Pochmann,
Warning: date(): Invalid date.timezone value 'Europe/Kyiv', we selected the timezone 'UTC' for now. in /var/www/agent_stack/data/www/doraprojects.net/template/agent.layouts/content.php on line 54
2017-05-23 12:03:02