Null в Python: Понимание объекта Python NoneType

Источник: «Null in Python: Understanding Python's NoneType Object»
В этой статье вы узнаете о NoneType — объекте None, выполняющем роль null в Python. Этот объект представляет собой пустоту, и его можно использовать для обозначения параметров по умолчанию и даже для того, чтобы показать, когда у вас нет результата. None — инструмент, позволяющий делать все из ничего!

Если у вас есть опыт работы с другими языками программирования, например C или Java, то вы, скорее всего, слышали о понятии null. Во многих языках оно используется для обозначения указателя, который ни на что не указывает, для обозначения пустой переменной или для обозначения параметров по умолчанию, которые еще не заданы. В этих языках null часто определяется как 0, но null в Python отличается.

В Python для определения null объектов и переменных используется ключевое слово None. Хотя None служит для тех же целей, что и null в других языках, это совершенно другой зверь. Как и null в Python, None не определяется как 0 или любое другое значение. В Python None — это объект и гражданин первого класса!

Понимание Null в Python

None — это значение, которое функция возвращает, когда в функции нет оператора return:

>>> def has_no_return():
... pass
>>> has_no_return()
>>> print(has_no_return())
None

Когда вы вызываете has_no_return(), вы ничего не видите на выходе. Однако когда вы напечатаете его вызов, вы увидите скрытое значение None, которое он возвращает.

На самом деле None так часто появляется в качестве возвращаемого значения, что Python REPL не будет печатать None, если вы явно не укажите:

>>> None
>>> print(None)
None

None сам по себе не имеет вывода, но при печати он отображает None в консоли.

Интересно, что сама функция print() не имеет возвращаемого значения. Если вы попытаетесь напечатать вызов print(), вы получите None:

>>> print(print("Hello, World!"))
Hello, World!
None

Это может показаться странным, но print(print("...")) показывает None, который возвращает внутренний print().

None также часто используется в качестве сигнала для отсутствующих параметров или параметров по умолчанию. Например, None дважды появляется в документации list.sort:

>>> help(list.sort)
Help on method_descriptor:

sort(...)
L.sort(key=None, reverse=False) -> None -- stable sort *IN PLACE*

Здесь None — это значение по умолчанию для параметра key, а также подсказка типа для возвращаемого значения. Точный вывод help может варьироваться от платформы к платформе. Вы можете получить другой вывод при запуске этой команды в интерпретаторе, но он будет похож.

Использование Null объекта Python None

Часто вы будете использовать None как часть сравнения. Одним из примеров является ситуация, когда вам нужно проверить, имеет ли какой-либо результат или параметр значение None. Возьмите результат, который вы получаете от re.match. Соответствовало ли регулярное выражение заданной строке? Вы увидите один из двух результатов:

  1. Возвращает объект Match: Ваше регулярное выражение нашло совпадение.
  2. Возвращает объект None: Ваше регулярное выражение не нашло соответствия.

В приведённом ниже блоке кода проверяется, соответствует ли шаблон "Goodbye" строке:

>>> import re
>>> match = re.match(r"Goodbye", "Hello, World!")
>>> if match is None:
... print("It doesn't match.")
It doesn't match.

Здесь используется is None, чтобы проверить, соответствует ли шаблон строке "Hello, World!". Этот блок кода демонстрирует важное правило, о котором следует помнить, когда вы проверяете None:

Операторы равенства можно обмануть, когда вы сравниваете пользовательские объекты, которые переопределяют их:

>>> class BrokenComparison:
... def __eq__(self, other):
... return True
>>> b = BrokenComparison()
>>> b == None # Equality operator
True
>>> b is None # Identity operator
False

Оператор равенства == возвращает неверный ответ. Оператор идентификации is, с другой стороны, нельзя обмануть, потому что вы не можете его переопределить.

None является ложным, что означает, что not None это True. Если всё, что вы хотите знать, это является ли результат ложным, то достаточно теста подобного следующему:

>>> some_result = None
>>> if some_result:
... print("Got a result!")
... else:
... print("No result.")
...
No result.

Вывод не показывает вам, что some_result точно равен None, а только, что он ложный. Если вы хотите знать, есть ли у вас объект None, то используйте is и is not.

Следующие объекты также являются ложными:

Объявление Null переменных в Python

В некоторых языках переменные появляются из объявления. Им необязательно присваивать начальное значение. В этих языках начальное значение по умолчанию для некоторых типов переменных может быть null. Однако в Python переменные оживают благодаря операторам присваивания. Взгляните на следующий блок кода:

>>> print(bar)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
NameError: name 'bar' is not defined
>>> bar = None
>>> print(bar)
None

Вы можете увидеть, что переменная со значением None отличается от неопределённой переменной. Всё переменные в Python создаются путём присваивания. Переменная начнёт жизнь как null в Python, только если вы присвоите ей значение None.

Использование None в качестве параметра по умолчанию

Очень часто вы будете использовать None в качестве значения по умолчанию для необязательного параметра. Есть очень веская причина для использования None, а не мутабельный тип, такой как список. Представьте такую функцию:

def bad_function(new_elem, starter_list=[]):
starter_list.append(new_elem)
return starter_list

bad_function() содержит неприятный сюрприз. Она отлично работает, когда вы вызываете её с существующим списком:

>>> my_list = ['a', 'b', 'c']
>>> bad_function('d', my_list)
['a', 'b', 'c', 'd']

Она без проблем добавляет d в конец списка.

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

>>> bad_function('a')
['a']
>>> bad_function('b')
['a', 'b']
>>> bad_function('c')
['a', 'b', 'c']

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

Правильный способ построить эту функцию — использовать None в качестве значения по умолчанию, затем протестировать его и создать экземпляр нового списка по мере необходимости:

>>> def good_function(new_elem, starter_list=None):
... if starter_list is None:
... starter_list = []
... starter_list.append(new_elem)
... return starter_list
...
>>> good_function('e', my_list)
['a', 'b', 'c', 'd', 'e']
>>> good_function('a')
['a']
>>> good_function('b')
['b']
>>> good_function('c')
['c']

good-function() ведёт себя так, как вы хотите, создавая новый список при каждом вызове, когда вы не передаёте существующий список. Это работает, потому что ваш код будет выполнять строки 2 и 3 каждый раз, когда вызывается функция с параметром по умолчанию.

Использование None в качестве значения Null в Python

Что вы делаете, когда None является допустимым входным объектом? Например, что, если функция good_function() может либо добавить элемент в список, либо нет, а None будет допустимым элементом добавления? В этом случае вы можете определить класс специально для использования по умолчанию, в то же время отличаясь от None:

>>> class DontAppend: pass
...
>>> def good_function(new_elem=DontAppend, starter_list=None):
... if starter_list is None:
... starter_list = []
... if new_elem is not DontAppend:
... starter_list.append(new_elem)
... return starter_list
...
>>> good_function(starter_list=my_list)
['a', 'b', 'c', 'd', 'e']
>>> good_function(None, my_list)
['a', 'b', 'c', 'd', 'e', None]

Класс DontAppend служит сигналом не добавлять, поэтому для этого не нужен None. Это освобождает от добавления None, когда вы хотите.

Вы можете использовать этот метод, когда None также может быть возвращаемым значением. Например, dict.get возвращает None по умолчанию, если ключ не найден в словаре. Если бы None был допустимым значением в вашем словаре, вы могли бы вызвать dict.get следующим образом:

>>> class KeyNotFound: pass
...
>>> my_dict = {'a':3, 'b':None}
>>> for key in ['a', 'b', 'c']:
... value = my_dict.get(key, KeyNotFound)
... if value is not KeyNotFound:
... print(f"{key}->{value}")
...
a->3
b->None

Мы определили собственный класс KeyNotFound. Теперь вместо того, что бы возвращать None, когда ключа нет в словаре, можем вернуть KeyNotFound. Это позволяет проще возвращать None, если это фактическое значение в словаре.

Расшифровка None в Трассировке

Когда NoneType появляется в вашей трассировке, это означает, что то, чего вы не ожидали, что это будет None, на самом деле было None, и вы пытались использовать это таким образом, каким нельзя использовать None. Почти всегда это происходит потому, что вы пытаетесь вызвать для него метод.

Например, вы много раз вызывали append() для my_list в примере выше, но если my_list каким-то образом станет чем-то другим, кроме списка, то append() завершится ошибкой:

>>> my_list.append('f')
>>> my_list
['a', 'b', 'c', 'd', 'e', None, 'f']
>>> my_list = None
>>> my_list.append('g')
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'NoneType' object has no attribute 'append'

Код вызывает распространённую ошибку AttributeError, потому что базовый объект, my_list, больше не является списком. Он установлен в None у которого нет атрибута append(), поэтому код выдаёт исключение.

Когда видите такую трассировку в своём коде, сначала найдите атрибут, вызвавший ошибку. Здесь это append(). Оттуда вы увидите объект, для которого пытались его вызвать. В данном случае это my_list, как видно из кода чуть выше трассировки. Наконец, выясните, как этот объект стал None, и предпримите необходимые шаги для исправления кода.

Проверка на Null в Python

Есть два случая проверки типов, когда в Python понадобится null. Первый случай — когда вы возвращаете None:

>>> def returns_None() -> None:
... pass

Этот случай подобен тому, когда у вас вообще нет оператора return, который возвращает None по умолчанию.

Второй случай немного сложнее. Здесь вы принимаете или возвращаете значение, которое может None, но также может быть и другим (одиночным) типом. Этот случай подобен тому, что мы делали с re.match выше, когда возвращался либо объект Match, либо None.

Процесс аналогичен для параметров:

from typing import Any, List, Optional
def good_function(new_elem:Any, starter_list:Optional[List]=None) -> List:
pass

Мы изменяем good_function() сверху и импортируем Optional из typing, чтобы вернуть Optional[Match].

Заглянем под капот

Во многих других языках null — это просто синоним 0, но null в Python — это полноценный объект:

>>> type(None)
<class 'NoneType'>

Эта строка показывает, что None является объектом и имеет тип NoneType.

None сам по себе встроен в язык как null в Python:

>>> dir(__builtins__)
['ArithmeticError', ..., 'None', ..., 'zip']

Здесь вы можете увидеть None в списке __builtins__, который представляет собой словарь, хранимый интерпретатором для модуля builtins.

None — это ключевое слово, как True или False. Но из-за этого вы не можете получить None напрямую из __builtins__, как могли бы, например, ArithmeticError. Однако вы можете получить его также с помощью трюка getattr():

>>> __builtins__.ArithmeticError
<class 'ArithmeticError'>
>>> __builtins__.None
File "<stdin>", line 1
__builtins__.None
^
SyntaxError: invalid syntax
>>> print(getattr(__builtins__, 'None'))
None

Когда используете getattr(), вы можете получить настоящий None из __builtins__, чего нельзя сделать, просто запросив его с помощью __builtins__.None.

Несмотря на то, что Python печатает слово NoneType во многих сообщениях об ошибках, NoneType не является идентификатором в Python. Это не в builtins. Вы можете добрать до него только с type(None).

None это синглтон. То есть класс NoneType всегда даёт один и тот же единственный экземпляр None. В вашей программе Python есть только дин None:

>>> my_None = type(None)()  # Create a new instance
>>> print(my_None)
None
>>> my_None is None
True

Даже если вы попытаетесь создать новый экземпляр, вы всё равно получите существующий None.

Вы можете доказать, что None и my_None являются одним и тем же объектом, с помощью id():

>>> id(None)
4465912088
>>> id(my_None)
4465912088

Тот факт, что id выводит одно и тоже целочисленное значение для None и my_None, означает, что они фактически являются одним и тем же объектом.

Если вы попытаетесь присвоить значение объекту None, вы получите SyntaxError:

>>> None = 5
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
SyntaxError: can't assign to keyword
>>> None.age = 5
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'NoneType' object has no attribute 'age'
>>> setattr(None, 'age', 5)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'NoneType' object has no attribute 'age'
>>> setattr(type(None), 'age', 5)
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: can't set attributes of built-in/extension type 'NoneType'

Приведённые выше примеры показывают, что нельзя изменить None или NoneType. Они настоящие константы.

Также нельзя создать подкласс NoneType:

>>> class MyNoneType(type(None)):
... pass
...
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: type 'NoneType' is not an acceptable base type

Эта трассировка показывает, что интерпретатор не позволит создать новый класс, наследуемы от type(None).

Заключение

None — это мощный инструмент в наборе инструментов Python. Подобно True и False, None является иммутабельным ключевым словом. Как null в Python, вы используете его, чтобы пометить отсутствующие значения и результаты, и даже параметры по умолчанию, где это гораздо лучший выбор, чем мутабельные типы.

Теперь вы можете:

Дополнительные материалы

Предыдущая Статья

Python: Идентичность и равенство объектов

Следующая Статья

SQL-инъекция