Ответ 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.