Режимы доступа, геттеры и сеттеры

Теперь, когда мы научились создавать экземпляры классов и объявлять в них атрибуты и методы пришла пора рассмотреть важную тему ограничения доступа к данным и методам класса извне. Это основа механизма инкапсуляции. Также здесь посмотрим как происходит обращение к закрытым (приватным) свойствам через специальные методы: геттеры и сеттеры. И, затем, уже на следующих занятиях разовьем этот подход и поговорим о создании объектов-свойств (property) класса и дескрипторах. Фактически, это одна большая и единая тема. Для простоты восприятия я разобью ее на несколько занятий. Здесь мы разберемся с основами работы с закрытыми переменными класса, а следующее занятие будет естественным продолжением этой темы.

Итак, начнем с реализации принципа инкапсуляции в Python. Давайте вернемся к нашему классу представления точки на плоскости:

class Point:
    def __init__(self, x = 0, y = 0):
        self.x = x; self.y = y

Смотрите, когда мы создаем экземпляр этого класса:

pt = Point(1, 2)

то мы имеем полный доступ ко всем его локальным переменным:

print( pt.x, pt.y )

а, значит, можем изменить их вне этого класса:

pt.x = 100
pt.y = "abc"

и присвоить любые значения, в том числе и недопустимые (строку). Чтобы программист не мог произвольным образом задавать атрибуты их следует «закрывать» от вмешательства извне. В Python возможны следующие варианты доступа к данным:

  • <имя атрибута> (без одного или двух подчеркиваний вначале) – публичное свойство (public);
  • _<имя атрибута> (с одним подчеркиванием) – режим доступа protected (можно обращаться только внутри класса и во всех его дочерних классах)
  • __<имя атрибута> (с двумя подчеркиваниями) – режим доступа private (можно обращаться только внутри класса).

Как видите, ничего сложного нет. Пока мы будем использовать или публичные свойства (public) или закрытые (private). О режиме protected поговорим, когда будем рассматривать наследование классов.

Итак, создадим в конструкторе два приватных атрибута:

self.__x = x; self.__y = y

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

print( pt.__x )

менять и использовать их можно только внутри экземпляра класса.

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

def setCoords(self, x, y):
    self.__x = x; self.__y = y

Теперь, мы можем вызвать этот метод и указать нужные координаты для приватных атрибутов:

pt = Point()
pt.setCoords(10, 20)

Чтобы увидеть изменение данных, давайте добавим еще один метод, у которого будет префикс get (получить):

def getCoords(self):
    return self.__x, self.__y

И, далее, вызовем его:

print( pt.getCoords() )

Такие методы в ООП называются сеттерами и геттерами. Их назначение не только передавать значения между приватными атрибутами класса, но и проверять их корректность. Например, в нашем случае координаты должны быть числами. Поэтому, прежде чем обновлять значения переменных, следует проверить их тип данных. Для этого можно воспользоваться функцией isinstance и записать сеттер следующим образом:

def setCoords(self, x, y):
    if (isinstance(x, int) or isinstance(x, float)) and \
        (isinstance(y, int) or isinstance(y, float)) :
        self.__x = x
        self.__y = y    else:
        print("Координаты должны быть числами")

Обратите внимание вот на этот слеш. Он используется когда конструкцию в языке Python нужно записать в несколько строчек. Он, говорит интерпретатору языка, что следующая строчка – это продолжение первой. Итак, здесь мы проверяем, что обе переданные переменные x и y должны иметь тип int или float и только после этого приватным атрибутам экземпляра класса будут присвоены эти значения. Иначе, выводится сообщение, что координаты должны быть числами. Давайте проверим как это будет работать:

pt.setCoords(10, 20)
print( pt.getCoords() )
pt.setCoords("10", 20)
print( pt.getCoords() )

Как видите, все работает именно так, как мы и хотели.

Продолжим совершенствовать наш класс Point. Вот такие проверки корректности данных, часто реализуют в виде вспомогательных методов, которые доступны только внутри класса, то есть, их делают приватными (закрытыми). Приватный метод объявляется также как и приватная переменная – двумя подчеркиваниями, например, так:

def __checkValue(x):
    if isinstance(x, int) or isinstance(x, float):
        return True
    return False

Мы здесь объявили метод без параметра self, т.к. ему не нужен доступ к экземпляру класса и вызываться он будет непосредственно через класс:

if Point.__checkValue(x) and Point.__checkValue(y) :

Запускаем программу и видим, что все работает также. Но, при этом, доступа к этому методу извне нет, он приватный.

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

_Имя класса__имя переменной
_Имя класса__имя метода

Например, мы можем изменить приватную переменную __x, так:

pt._Point__x = 100

и теперь она равна 100. Или, вызвать скрытый метод:

Point._Point__checkValue(4)

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

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

  • __setattr(self, key, value)__ – автоматически вызывается при изменении свойства key класса;
  • __getattribute__(self, item) – автоматически вызывается при получении свойства класса с именем item;
  • __getattr__(self, item) – автоматически вызывается при получении несуществующего свойства item класса;
  • __delattr__(self, item) – автоматически вызывается при удалении свойства item (не важно: существует оно или нет).

Например, переопределив метод:

def __getattribute__(self, item):
    if item == "_Point__x":
        raise ValueError("Private attribute")
    else:
        return object.__getattribute__(self, item)

мы не сможем извне узнать значение приватной переменной __x:

print( pt._Point__x )

выдаст исключение.  Обратите внимание, что для всех остальных свойств мы должны вызывать этот метод базового класса, в данном случае – это класс object. Иначе, мы не сможем работать с атрибутами класса. Что такое базовый класс мы еще будем говорить в теме «Наследование». Пока просто запомните этот момент.

Далее, переопределяя метод:

def __setattr__(self, key, value):
    if key == "WIDTH":
        raise AttributeError
    else:
        self.__dict__[key] = value

мы запрещаем изменять свойство WIDTH нашего класса. При попытке это сделать:

pt.WIDTH = 5

сработает исключение AttributeError. А иначе, мы в экземпляре класса меняем значение свойства с именем key на значение value. Обратите внимание, что внутри __setattr__ менять свойства следует только через словарь __dict__. Если записать здесь что-то вроде:

self.__x = value

то у нас метод __setattr__ начнет рекуррентно вызываться, пока не переполнится стек вызова функций.

Наконец, методы __delattr__ и __getattr__ можно перегрузить так:

def __getattr__(self, item):
    print("__getattr__: "+item)
def __delattr__(self, item):
    print("__delattr__: "+item)

Еще один дополнительный контроль за локальными свойствами экземпляров классов можно сделать с помощью коллекции:

__slots__ = ["__x", "__y"]

и при попытке создать какие-либо дополнительные локальные атрибуты:

pt.z = 1

возникнет ошибка. И, обратите внимание, в slots можно указывать имена только локальных свойств экземпляров, записать туда имена переменных самого класса нельзя. Вот такая запись приведет к ошибке:

__slots__ = ["__x", "__y", "WIDTH"]

На этом мы завершим первую часть темы работы с атрибутами класса. На следующем занятии продолжим и поговорим о создании объектов-свойств (property) и довольно интересном и гибком механизме – дескрипторах.