Аватарка пользователя Andrei Smykov
Andrei Smykov

Как получить отрицательную длину len()≤0 на Python

Рассказываем, возможно ли получить негативное значение из встроенной функции len() в Python, как это работает и зачем это нужно.

4821

Возможно ли получить негативное значение из встроенной функции len() в Python?

Встроенная функция len() возвращает длину (количество элементов) объекта. Интуитивно понятно, что количество элементов в коллекции не может быть отрицательным. Оно должно быть равно 0 или больше.

Однако, я задумался над этим вопросом когда встретил следующий рабочий Python код:

			if len(files_names) <= 0:
    # code
		

Условие пропускается, если коллекция не пустая. Если же она пустая, то мы попадаем в if блок. На первый взгляд это выглядит как незначительная ошибка из-за невнимательности. Но все же давайте исследуем ситуации когда len() может вернуть отрицательное значение.

Встроенная функция len()

Прежде всего, давайте взглянем на документацию:

Return the length (the number of items) of an object. The argument may be a sequence (such as a string, bytes, tuple, list, or range) or a collection (such as a dictionary, set, or frozen set).

Несколько примеров использования len() с последовательностями и коллекциями:

			>>> names = ['Hanna', 'Georg', 'Richard']
>>> len(names)
3
>>> name = 'Theodor'
>>> len(name)
>>>
7
>>> manager = (35,  'Thea')
>>> len(manager)
2
>>> plants_ph = {'fiddle_leaf': 6, 'bird_of_paradise': 5.5 }
>>> len(plants_ph)
2
>>> unique_plants = {'hazel', 'azalea', 'daisy', 'fern'}
>>> len(unique_plants)
4
		

Для тех кто хочет глубже погрузиться в реализацию может обратиться к исходникам

Специальный метод __len__

Встроенная функция len() работает как фасад к дандер методу __len__() объекта (см. док). Вы можете явно вызвать его для встроенных типов, таких как последовательности или коллекции:

			>>> names = ['Hanna', 'Georg', 'Richard']
>>> names.__len__()
3
>>> plants_ph = {'fiddle_leaf': 6, 'bird_of_paradise': 5.5 }
>>> plants_ph.__len__()
2
>>> unique_plants = {'hazel', 'azalea', 'daisy', 'fern'}
>>> unique_plants.__len__()
4
		

Но если вы попытаетесь использовать len() с кастомным классом без определения __len__, то это не сработает, и вы получите исключение:

			>>> class Plant:
>>>    pass
>>>
>>> len(Plant())
TypeError: object of type 'Plant' has no len()
		

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

Возврат отрицательного значения

Итак, чтобы проверить первоначальную гипотезу  —  может ли len() вернуть отрицательное значение, нам нужно создать пользовательский тип и определить специальный метод __len__, возвращающий такое значение.

Сначала сэмулируем пользовательский тип контейнера и определим __len__(), возвращающий небольшое отрицательное целое число:

			>>> class MyList:
>>>     def __len__(self):
>>>         return -1
>>>
>>> len(MyList())
ValueError: __len__() should return >= 0
>>>
>>> MyList().__len__()
-1
		

В результате это приводит к ValueError, что соответствует задокументированному поведению, хотя ничто не мешает нам явно вызвать __len__.

Как насчет возврата большого отрицательного числа?

			>>> import sys
>>> class MyList:
>>>     def __len__(self):
>>>         return -sys.maxsize - 2**10
>>>
>>> len(MyList())
ValueError: __len__() should return >= 0
		

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

Однако, согласно баг-трекеру и pull request раньше наблюдалась ошибка переполнения OverflowError. Вы можете проверить это на предыдущих версиях python (например, Python 3.6.15).

Предыдущая реализация, возвращающая OverflowError, имела ограничения на длину в реализации CPython, которая все еще работает для больших положительных чисел для Python 3.11.2:

CPython implementation detail: In CPython, the length is required to be at most sys.maxsize. If the length is larger than sys.maxsize some features (such as len()) may raise OverflowError.
			>>> import sys
>>> class MyList:
>>>     def __len__(self):
>>>         return sys.maxsize + 2 ** 10
>>> 
>>> len(MyList())
OverflowError: cannot fit 'int' into an index-sized integer
		

Итак, мы не можем заставить len() возвращать отрицательное значение, потому что выходные данные __len__ тщательно валидируются. Вот почему невозможно перейти в условие:

			if len(obj) < 0:
    print('negative length')
		

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

Поиска метода __len__

В качестве продвинутого примера давайте рассмотрим поиск метода __len__ и обратимся к соответствующему параграфу о специальных методах:

For custom classes, implicit invocations of special methods are only guaranteed to work correctly if defined on an object’s type, not in the object’s instance dictionary.

Это объяснение нелегко уловить сразу. В нем говорится о 2 случаях определения __len__:

  • определение в словаре экземпляра объекта
  • определение в типе объекта (работает корректно)

Мы рассмотрим оба случая с примерами для пояснения. Сначала создайте пользовательский тип без __len__ и назначьте его в качестве атрибута экземпляру объекта:

			>>> class ImperfectList:
>>>     pass
>>>
>>> imperfect_list = ImperfectList()
>>> imperfect_list.__dict__         # object's instance dictionary is empty
{}
>>>
>>> imperfect_list.__len__ = lambda: 100
>>> imperfect_list.__dict__
{'__len__': <function <lambda> at 0x7fc8d86ba040>}
>>>
>>> ImperfectList.__dict__.keys()
dict_keys(['__module__', '__dict__', '__weakref__', '__doc__'])
>>>
>>> len(imperfect_list)
TypeError: object of type 'ImperfectList' has no len()
		

Можно увидеть, что неявный вызов __len__ через len() вызывает исключение, поскольку __len__ добавляется в качестве атрибута экземпляра вне класса. Тем не менее, если мы определим метод __len__ внутри определения типа, он будет работать корректно:

			>>> class PerfectList:
>>>     def __len__(self):
>>>         return 7               
>>>
>>> perfect_list = PerfectList()
>>> perfect_list.__dict__
{}
>>>
>>> PerfectList.__dict__.keys()
dict_keys(['__module__', '__len__', '__dict__', '__weakref__', '__doc__'])
>>>
>>> len(perfect_list)
7
		

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

***

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

			if len(files_names) < 0:
		

Не стоит забывать, что ошибки имеют тенденцию накапливаться вместе. Когда вы где-то сталкиваетесь с незначительной проблемой, существует высокая вероятность того, что вы обнаружите в близлежащем коде более серьезные проблемы. Дважды перепроверяйте смежный код, чтобы убедиться, что все работает должным образом.

***
4821