Какова связь между моделью данных Python и встроенными функциями?

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

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

Почему это? Какова взаимосвязь между специальными "нездоровыми" методами и атрибутами модели данных Python и встроенных функций builtin functions?

Когда я должен использовать специальные имена?

Ответы

Ответ 1

Какова связь между моделью данных Python и встроенными функциями?

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

Таким образом, вы должны по возможности использовать встроенные функции и операторы, а не специальные методы и атрибуты модели данных.

Семантически внутренние API, скорее всего, изменятся, чем публичные интерфейсы. Хотя Python на самом деле не считает что-то "частным" и предоставляет доступ к внутренним ресурсам, это не означает, что это хорошая идея - злоупотреблять этим доступом. Это сопряжено со следующими рисками:

  • Вы можете столкнуться с более серьезными изменениями при обновлении исполняемого файла Python или переходе на другие реализации Python (например, PyPy, IronPython или Jython или какую-либо другую непредвиденную реализацию).
  • Ваши коллеги, скорее всего, плохо подумают о ваших языковых навыках и добросовестности и сочтут это запахом кода, который приведет вас и весь ваш код к более тщательному анализу.
  • Встроенные функции легко перехватывать поведение. Использование специальных методов напрямую ограничивает возможности вашего Python для самоанализа и отладки.

В глубине

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

Встроенные функции и операторы также могут иметь откат или более элегантное поведение, чем более примитивные специальные методы модели данных. Например:

  • next(obj, default) позволяет вам предоставить значение по умолчанию вместо повышения StopIteration, когда итератор заканчивается, а obj.__next__() нет.
  • str(obj) возвращается к obj.__repr__(), когда obj.__str__() недоступен - тогда как вызов obj.__str__() напрямую вызовет ошибку атрибута.
  • obj != other возвращается к not obj == other в Python 3, когда __ne__ - вызов obj.__ne__(other) не воспользуется этим.

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

Отображение встроенных функций и операторов в модель данных

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

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__()

В модуле operator имеется length_hint, для которого предусмотрен запасной вариант, реализованный соответствующим специальным методом, если __len__ не реализован:

length_hint(obj)  obj.__length_hint__() 

Поиск с точками

Пунктирные поиски являются контекстными. Без специальной реализации метода сначала ищите в иерархии классов дескрипторы данных (например, свойства и слоты), затем в экземпляре __dict__ (для переменных экземпляра), затем в иерархии классов для дескрипторов, не относящихся к данным (например, методы). Специальные методы реализуют следующее поведение:

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

Дескрипторы

Дескрипторы немного продвинуты - не стесняйтесь пропустить эти записи и вернуться позже - вспомните, что экземпляр дескриптора находится в иерархии классов (например, методы, слоты и свойства). Дескриптор данных реализует либо __set__, либо __delete__:

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

Когда создается экземпляр класса (определяется), вызывается следующий метод дескриптора __set_name__, если он есть у какого-либо дескриптора, чтобы сообщить дескриптору имя его атрибута. (Это новое в Python 3.6.) cls такой же, как type(obj) выше, а 'attr' заменяет имя атрибута:

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

Элементы (нижняя запись)

Нижняя запись также является контекстной:

obj[name]         -> obj.__getitem__(name)
obj[name] = item  -> obj.__setitem__(name, item)
del obj[name]     -> obj.__delitem__(name)

Специальный случай для подклассов dict, __missing__ вызывается, если __getitem__ не находит ключ:

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

Операторы

Есть также специальные методы для операторов +, -, *, @, /, //, %, divmod(), pow(), **, <<, >>, &, ^, |, например:

obj + other   ->  obj.__add__(other), fallback to other.__radd__(obj)
obj | other   ->  obj.__or__(other), fallback to other.__ror__(obj)

и операторы на месте для расширенного назначения, +=, -=, *=, @=, /=, //=, %=, **=, <<=, >>=, &=, ^=, |=, например:

obj += other  ->  obj.__iadd__(other)
obj |= other  ->  obj.__ior__(other)

(Если эти операторы на месте не определены, Python возвращается, например, для obj += other к obj = obj + other)

и унарные операции:

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

Контекстные менеджеры

Диспетчер контекста определяет __enter__, который вызывается при вводе блока кода (его возвращаемое значение, обычно self, связывается с as), и __exit__, который гарантированно вызывается при выходе из блока кода, с информация об исключении.

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

Если __exit__ получает исключение, а затем возвращает ложное значение, оно вызывает его при выходе из метода.

Если нет исключений, __exit__ вместо этого получает None для этих трех аргументов, а возвращаемое значение не имеет смысла:

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

Некоторые специальные методы метакласса

Точно так же классы могут иметь специальные методы (из своих метаклассов), которые поддерживают абстрактные базовые классы:

isinstance(obj, cls) -> cls.__instancecheck__(obj)
issubclass(sub, cls) -> cls.__subclasscheck__(sub)

Важным выводом является то, что хотя встроенные функции, такие как next и bool, не меняются между Python 2 и 3, базовые имена реализации меняются.

Таким образом, использование встроенных функций также обеспечивает более прямую совместимость.

Когда я должен использовать специальные имена?

В Python имена, начинающиеся с подчеркивания, являются семантически закрытыми именами для пользователей. Подчеркивание - это способ создателя сказать: "Не трогайте, не трогайте".

Это не только культурно, но и в обработке Python API. Когда пакет __init__.py использует import * для предоставления API из подпакета, если подпакет не предоставляет __all__, он исключает имена, начинающиеся с подчеркиваний. Подпакет __name__ также будет исключен.

Инструменты автозаполнения IDE смешаны при рассмотрении имен, которые начинаются с подчеркивания, чтобы быть закрытыми. Тем не менее, я очень ценю то, что не вижу __init__, __new__, __repr__, __str__, __eq__ и т.д. (Ни один из пользовательских интерфейсов, созданных пользователем), когда я набираю имя объекта и период.

Таким образом я утверждаю:

Специальные методы dunder не являются частью общедоступного интерфейса. Старайтесь не использовать их напрямую.

Так когда же их использовать?

Основной вариант использования - при реализации собственного пользовательского объекта или подкласса встроенного объекта.

Старайтесь использовать их только в случае крайней необходимости. Вот несколько примеров:

Используйте специальный атрибут __name__ для функций или классов

Когда мы украшаем функцию, мы обычно получаем взамен функцию-обертку, которая скрывает полезную информацию о функции. Мы бы использовали декоратор @wraps(fn), чтобы не потерять эту информацию, но если нам нужно имя функции, нам нужно напрямую использовать атрибут __name__:

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

Точно так же я делаю следующее, когда мне нужно имя класса объекта в методе (используется, например, в __repr__):

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

Использование специальных атрибутов для написания пользовательских классов или встроенных встроенных классов

Когда мы хотим определить пользовательское поведение, мы должны использовать имена моделей данных.

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

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  

Однако даже в этом случае мы не используем self.value.__eq__(other.value) или not self.__eq__(other) (см. мой ответ здесь для доказательства того, что последнее может привести к неожиданному поведению.) Вместо этого мы должны использовать более высокий уровень абстракции.

Еще один момент, когда нам нужно использовать специальные имена методов, это когда мы находимся в дочерней реализации и хотим делегировать родительскому объекту. Например:

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

Заключение

Специальные методы позволяют пользователям реализовывать интерфейс для внутренних объектов.

Используйте встроенные функции и операторы везде, где можете. Используйте специальные методы только там, где нет документированного публичного API.

Ответ 2

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


Я согласен с вашим собственным ответом, что, например, следует использовать len(a), а не a.__len__(). Я бы сказал так: len существует, чтобы мы могли его использовать, и __len__ существует, чтобы len мог использовать его. Или, однако, это действительно работает внутренне, так как len(a) может на самом деле быть намного быстрее, по крайней мере, например, для списков и строк:

>>> 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)',  = "abc"', number=10**8)
4.1480574509332655
>>> timeit('s.__len__()',  = "abc"', number=10**8)
8.01780160432645

Но помимо определения этих методов в моих собственных классах для использования встроенными функциями и операторами, я иногда также использую их следующим образом:

Допустим, мне нужно дать функцию фильтра некоторой функции, и я хочу использовать набор s в качестве фильтра. Я не собираюсь создавать дополнительную функцию lambda x: x in s или def f(x): return x in s. У меня уже есть прекрасная функция, которую я могу использовать: метод set __contains__. Это проще и прямее. И даже быстрее, как показано здесь (не обращайте внимания на то, что я сохраняю его как f здесь, только для этой демонстрации времени):

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

Так что, хотя я не напрямую вызываю магические методы, например, s.__contains__(x), я иногда передаю их где-то, например, some_function_needing_a_filter(s.__contains__). И я думаю, что это прекрасно, и лучше, чем альтернатива лямбда/деф.


Мои мысли о примерах, которые вы показали:

  • Пример 1: На вопрос, как определить размер списка, он ответил items.__len__(). Даже без каких-либо рассуждений. Мой вердикт: это просто неправильно. Должно быть len(items).
  • Пример 2: сначала упоминается d[key] = value! А затем добавляет d.__setitem__(key, value) с аргументом "если на вашей клавиатуре отсутствуют клавиши с квадратными скобками", что редко применяется и, я сомневаюсь, было серьезным. Я думаю, что это был всего лишь шаг в дверь для последнего пункта, упомянув, что именно так мы можем поддерживать синтаксис в квадратных скобках в наших собственных классах. Это возвращает нас к предложению использовать квадратные скобки.
  • Пример 3: предлагает obj.__dict__. Плохо, как в примере с __len__. Но я подозреваю, что он просто не знал vars(obj), и я могу понять это, поскольку vars менее распространен/известен, и название действительно отличается от "dict" в __dict__.
  • Пример 4: предлагает __class__. Должно быть type(obj). Я подозреваю, что это похоже на историю __dict__, хотя я думаю, что type более известен.

О конфиденциальности: в своем ответе вы говорите, что эти методы "семантически приватны". Я категорически не согласен. Для этого используются одинарные и двойные подчеркивания , ведущие, но не специальные модели данных "ошеломление/магия" с двойным подчеркиванием + конечные подчеркивания.

  • В качестве аргументов вы используете две вещи: импорт поведения и автодополнение IDE. Но импорт и эти специальные методы - разные области, и одна из IDE, которую я попробовал (популярный PyCharm), не согласна с вами. Я создал класс/объект с методами _foo и __bar__, а затем автозаполнение не предложило _foo, но сделало предложение __bar__. И когда я все равно использовал оба метода, PyCharm только предупредил меня о _foo (назвав его "защищенным членом"), не о __bar__.
  • PEP 8 говорит "слабый индикатор" внутреннего использования "" явно для одиночного подчеркивания, а явно для двойного ведущего подчеркивает, что упоминает название "искажение", а позже объясняет, что оно для " атрибуты, которые вы не хотите использовать подклассами ". Но комментарий о подчеркивании двойное ведение + трейлинг ничего подобного не говорит.
  • На странице модели данных data model page, на которую вы сами ссылаетесь, говорится, что эти специальные имена special method names являются "подход Pythons к перегрузке операторов". Ничего о конфиденциальности там нет. Слова приват/конфиденциальность/защищенность даже не появляются нигде на этой странице.

    Я также рекомендую прочитать эту статью Эндрю Монталенти об этих методах, подчеркнув, что "соглашение о мошенничестве - это пространство имен, зарезервированное для основной команды Python" и "никогда, никогда не изобретай свои собственные ошибки", потому что "основное Команда Python зарезервировала для себя несколько уродливое пространство имен ". Все это соответствует инструкции PEP 8 "Никогда не изобретайте имена [dunder/magic]; используйте их только как документированные". Я думаю, что Эндрю находится на месте - это просто уродливое пространство имен основной команды. И это с целью перегрузки оператора, а не из-за конфиденциальности (не Эндрю Пойнт, а моя и страница модели данных).

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

Опять же, мы должны использовать len(a), а не a.__len__(). Но не из-за конфиденциальности.