Part 9

Атрибуты класса

Свойства объектов — центральное понятие в объектно-ориентированном программировании. Этот термин охватывает методы и переменные, определенные в определении класса. Опять же, не все программисты Python используют термин "свойства": некоторые предпочитают атрибуты, характеристики или члены. В более широком мире объектно-ориентированного программирования термин "свойства" вполне принят, поэтому здесь мы используем именно его.

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

Переменные класса

У каждого экземпляра класса есть свои конкретные значения для каждого атрибута, определенного в классе, как мы видели в примерах предыдущих разделов. Но что, если мы хотим иметь некоторые данные, общие для разных экземпляров? Здесь появляются переменные класса, также известные как статические переменные. Переменная класса — это переменная, к которой обращаются через сам класс, а не через экземпляры, созданные на основе класса. В любой момент выполнения программы переменная класса имеет одно единственное значение, независимо от того, сколько экземпляров класса создано.

Переменная класса объявляется без префикса self и обычно вне любого определения метода, потому что она должна быть доступна откуда угодно внутри класса или даже снаружи класса.

class SavingsAccount:
    general_rate = 0.03

    def __init__(self, account_number: str, balance: float, interest_rate: float):
        self.__account_number = account_number
        self.__balance = balance
        self.__interest_rate = interest_rate

    def add_interest(self):
        # The total interest rate equals 
        # the general rate + the interest rate of the account
        total_interest = SavingsAccount.general_rate + self.__interest_rate
        self.__balance += self.__balance * total_interest

    @property
    def balance(self):
        return self.__balance

Поскольку переменная general_rate определена внутри класса, но вне любых определений методов, и не использует префикс self, это переменная класса.

К переменной класса обращаются через имя класса, например так:

# The general rate exists independently of any object instances
print("The general interest rate is", SavingsAccount.general_rate)

account = SavingsAccount("12345", 1000, 0.05)
# Add the total interest accrued to the balance on the account
account.add_interest()
print(account.balance)
Пример вывода

The general interest rate is 0.03 1080.0

Итак, к переменным класса обращаются через имя класса, например SavingsAccount.general_rate, а к переменным экземпляра — через имя объектной переменной, например account.balance. Переменная экземпляра, естественно, существует только тогда, когда создан экземпляр класса, но переменная класса доступна везде и в любой момент времени, где доступен сам класс.

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

class SavingsAccount:
    general_rate = 0.03

    def __init__(self, account_number: str, balance: float, interest_rate: float):
        self.__account_number = account_number
        self.__balance = balance
        self.__interest_rate = interest_rate

    def add_interest(self):
        # The total interest rate equals 
        # the general rate + the interest rate of the account
        total_interest = SavingsAccount.general_rate + self.__interest_rate
        self.__balance += self.__balance * total_interest

    @property
    def balance(self):
        return self.__balance

    @property
    def total_interest(self):
        return self.__interest_rate + SavingsAccount.general_rate
account1 = SavingsAccount("12345", 100, 0.03)
account2 = SavingsAccount("54321", 200, 0.06)

print("General interest rate:", SavingsAccount.general_rate)
print(account1.total_interest)
print(account2.total_interest)

# The general rate of interest is now 10 percent
SavingsAccount.general_rate = 0.10

print("General interest rate:", SavingsAccount.general_rate)
print(account1.total_interest)
print(account2.total_interest)
Пример вывода

General interest rate: 0.03 0.06 0.09 General interest rate: 0.1 0.13 0.16

Когда общая процентная ставка меняется, итоговая процентная ставка для всех экземпляров класса тоже меняется. Как видно выше, можно добавить метод-геттер с декоратором @property, даже если в классе нет атрибута с таким же именем. Этот метод возвращает сумму общей процентной ставки и процентной ставки конкретного счета.

Посмотрим еще один пример. Класс PhoneNumber используется для определения одного телефонного номера, но он также содержит некоторые коды стран в словаре. Этот словарь — переменная класса, и поэтому он общий для всех экземпляров класса, потому что код страны для телефонных номеров из одной страны всегда одинаков.

class PhoneNumber:
    country_codes = {"Finland": "+358", "Sweden": "+46", "United States": "+1"}

    def __init__(self, name: str, phone_number: str, country: str):
        self.__name = name
        self.__phone_number = phone_number
        self.__country = country

    @property
    def phone_number(self):
        # When the country code prefix is added 
        # the initial zero is removed from the phone number
        return PhoneNumber.country_codes[self.__country] + " " + self.__phone_number[1:]
paulas_no = PhoneNumber("Paula Pythons", "050 1234 567", "Finland")
print(paulas_no.phone_number)
Пример вывода

+358 50 1234 567

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

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

class PhoneNumber:
    country_codes = {"Finland": "+358", "Sweden": "+46", "United States": "+1"}

    def __init__(self, name: str, phone_number: str, country: str):
        self.__name = name
        # This is a call to the phone_number.setter method
        self.phone_number = phone_number
        # This is a call to the country.setter method
        self.country = country

    # the getter method for phone_number combines the country code 
    # and the attribute phone_number
    @property
    def phone_number(self):
        # the initial zero is removed as the country code is prefixed
        return PhoneNumber.country_codes[self.__country] + " " + self.__phone_number[1:]

    @phone_number.setter
    def phone_number(self, number):
        # Making sure the number contains only numbers and space characters
        for character in number:
            if character not in "1234567890 ":
                raise ValueError("A phone number can only contain numbers and spaces")
        self.__phone_number = number

    # a getter for only the number itself without the country code
    @property
    def local_number(self):
        return self.__phone_number

    @property
    def country(self):
        return self.__country

    @country.setter
    def country(self, country):
        # Making sure the country is a key in the dictionary of country codes
        if country not in PhoneNumber.country_codes:
            raise ValueError("This country is not on the list.")
        self.__country = country

    @property
    def name(self):
        return self.__name

    @name.setter
    def name(self, name):
        self.__name = name

    def __str__(self):
        return f"{self.phone_number} ({self.__name})"
if __name__ == "__main__":
    pn = PhoneNumber("Peter Pythons", "040 111 1111", "Sweden")
    print(pn)
    print(pn.phone_number)
    print(pn.local_number)
Пример вывода

+46 40 111 1111 (Peter Pythons) +46 40 111 1111 040 111 1111

Loading

Методы класса

Метод класса, также называемый статическим методом, — это метод, который не привязан ни к одному конкретному экземпляру класса. Метод класса можно вызвать без создания каких-либо экземпляров класса.

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

Метод класса определяется с помощью аннотации @classmethod. Первый параметр всегда cls. Имя переменной cls похоже на параметр self. Разница в том, что cls указывает на класс, а self указывает на экземпляр класса. Ни один из этих параметров не включается в список аргументов при вызове функции; Python автоматически подставляет подходящее значение.

В следующем примере у нас есть класс, моделирующий регистрацию транспортных средств. Класс Registration содержит статический метод для проверки, является ли номерной знак допустимым. Этот метод является статическим методом класса, потому что полезно иметь возможность проверить допустимость номерного знака еще до создания хотя бы одного объекта Registration:

class Registration:
    def __init__(self, owner: str, make: str, year: int, license_plate: str):
        self.__owner = owner
        self.__make = make
        self.__year = year

        # Call the license_plate.setter method
        self.license_plate = license_plate

    @property
    def license_plate(self):
        return self.__license_plate

    @license_plate.setter
    def license_plate(self, plate):
        if Registration.license_plate_valid(plate):
            self.__license_plate = plate
        else:
            raise ValueError("The license plate is not valid")

    # A class method for validating the license plate
    @classmethod
    def license_plate_valid(cls, plate: str):
        if len(plate) < 3 or "-" not in plate:
            return False

        # Check the beginning and end sections of the plate separately
        letters, numbers = plate.split("-")

        # the beginning section can have only letters
        for character in letters:
            if character.lower() not in "abcdefghijklmnopqrstuvwxyzåäö":
                return False

        # the end section can have only numbers
        for character in numbers:
            if character not in "1234567890":
                return False

        return True
registration = Registration("Mary Motorist", "Volvo", "1992", "abc-123")

if Registration.license_plate_valid("xyz-789"):
    print("This is a valid license plate!")
Пример вывода

This is a valid license plate!

Допустимость номерного знака можно проверить даже без создания ни одного экземпляра класса, например с помощью Registration.license_plate_valid("xyz-789"). Тот же метод вызывается внутри конструктора класса. Обратите внимание: даже внутри конструктора к этому методу обращаются через имя класса, а не через self!

Loading
Вы дошли до конца этого раздела! Перейти к следующему разделу:

Текущие баллы можно посмотреть в синем индикаторе в правом нижнем углу страницы.