Статические свойства и методы классов

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

class Point:
    count = 0

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

pt = Point()
pt2 = Point()
pt3 = Point()

все они непосредственно обращаются к этому свойству:

В этом легко убедиться, изменив значение count непосредственно в классе Point:

Point.count = 10

все три экземпляра выведут в консоль именно это значение:

print( pt.count, pt2.count, pt3.count )

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

pt.count = 0

то это не значит, что мы меняем статическую переменную в классе Point, мы создаем здесь локальное свойство в экземпляре класса pt. И при выводе значений в консоль:

print( pt.count, pt2.count, pt3.count )

увидим, что первое значение 0, а остальные по 10.

То же самое происходит и при вызове конструктора:

def __init__(self, x = 0, y = 0):
    self.coordX = x
    self.coordY = y

Здесь в каждом экземпляре класса создаются локальные свойства coordX и coordY:

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

class Point:
    __count = 0
 
    def __init__(self, x = 0, y = 0):
        Point.__count += 1
        self.coordX = x
        self.coordY = y

И, далее, пропишем геттер для получения этого значения:

def getCount(self):
    return Point.__count

Все это можно использовать так:

pt = Point()
pt2 = Point()
pt3 = Point()
print( pt.getCount() )

Увидим значение 3. Причем, обращение к методу также происходит напрямую через класс Point:

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

Point.getCount()

здесь обязательно нужно передать первый параметр:

Point.getCount(pt)

или, даже так:

Point.getCount(1)

потому что, нам абсолютно все равно, чему равен self, мы с ним не работаем. Давайте попробуем убрать его из метода:

def getCount():

Смотрите, среда PyCharm нам подсказывает, что так делать нельзя, параметр необходим. Но, мы то знаем, что будем использовать этот метод как статический и это явно нужно указать интерпретатору Python. Для этого воспользуемся специальным декоратором функций:

@staticmethod
def getCount():
    return Point.__count

и теперь можем обращаться к этому методу без указания каких-либо параметров:

print( pt.getCount(), Point.getCount() )

причем, это также работает и с экземплярами классов. То есть, мы создали чисто статический метод, который вызывается непосредственно из класса Point  и может работать только со свойствами и методами этого класса.

Конечно, мы всегда в экземплярах класса можем перегрузить метод getCount, записать, например, так:

def getCount():
    return 5
 
pt.getCount = getCount

Тогда в pt будет создано новое свойство getCount, которое будет ссылаться на объект-функцию и, соответственно, ее вызывать:

print( pt.getCount(), Point.getCount() )

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

Класс-синглетон (singleton)

В заключение приведу один довольно популярный прием в программировании – создание класса-синглетона, то есть, класс, для которого можно создать только один экземпляр. Для этого добавим в наш класс Point приватную переменную

__instance = None

и выполним перегрузку метода

def __new__(cls, *args, **kwargs):
    if not isinstance(cls.__instance, cls):
        cls.__instance = super().__new__(cls)
    else:
        print("Экземпляр класса Point уже создан!")
 
    return cls.__instance

который вызывается в момент создания экземпляра класса. Здесь мы проверяем: если статическое свойство __instance не является создаваемым классом, значит, мы еще ни разу не создавали экземпляр. В этом случае обращаемся к базовому классу и через него создаем экземпляр текущего класса Point. Результат (ссылку на него) сохраняем в нашем статическом свойстве __instance и в конце возвращаем его. При повторной попытке создать экземпляр, условие будет ложным, мы увидим строку «Экземпляр класса Point уже создан» и вернем ссылку на ранее созданный объект.

Протестируем эту программу, создадим несколько экземпляров и выведем их id:

pt = Point()
pt2 = Point()
pt3 = Point()
print( id(pt), id(pt2), id(pt3), sep="\n" )

Как видите, все три переменные ссылаются на один и тот же объект, чего мы и хотели добиться.

Это одна из самых простых реализаций singleton (синглетона), но имеет ряд недостатков. Один из них, возможность обойти перегрузку метода __new__ через создание дочерних экземпляров классов, которые бы наследовались от класса Point. Но это уже детали. Я вам хотел лишь в целом показать как используются статические свойства классов на практике.

Задания для самоподготовки

1. Объявите класс Rectangle (прямоугольник), в котором имеется статический метод, вычисляющий площадь прямоугольника. Этот метод принимает два параметра (ширину и длину), вызывается в конструкторе для вычисления площади конкретного прямоугольника и результат присваивается локальному свойству создаваемого экземпляра класса.

2. Создайте класс Dog (собака), в каждом его экземпляре создавайте несколько локальных свойств (например: имя, возраст, порода) и сделайте так, чтобы можно было создавать не более пяти экземпляров этого класса.