Как определить класс в Python?

Я знаю, как писать скрипты на Python, и теперь хочу изучить объектно-ориентированный Python. Как мне создать свой первый класс Python?

Создание класса Python означает написание кода, который позволяет вам объединить данные и поведение, которые наилучшим образом описывают определенную концепцию или сущность. Данные в классе представлены атрибутами, а поведение класса задается методами.

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

В рамках приложения прогноза погоды мы создадим класс, представляющий город.

В этом конкретном контексте город может иметь различные атрибуты:

  • температура
  • тип погоды
  • вероятность осадков (процент)
  • влажность (процент)
  • скорость ветра (км/ч)

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

Начнем с атрибутов класса. Первым шагом будет определение конструктора:

class City:
  
    def __init__(self, temperature, weather_type, precipitation_chance, humidity, wind_speed):
        self.temperature = temperature
        self.weather_type = weather_type
        self.precipitation_chance = precipitation_chance
        self.humidity = humidity
        self.wind_speed = wind_speed

Имя метода-конструктора для класса в Python — __init__. Первый параметр, принимаемый этим методом, — self, который представляет экземпляр этого класса, также известный как объект.

Давайте проясним эту концепцию, прежде чем продолжить…

Класс — это шаблон или чертеж, позволяющий создавать объекты (или экземпляры классов). Например, мы можем использовать класс City для создания двух объектов: одного для прогнозов Лондона и одного для Парижа.

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

Параметр self является первым параметром в методах класса и ссылается на экземпляр класса.

Давайте посмотрим, как можно создать объект типа Город:

london = City(21, 'Sunny', 0.1, 0.63, 10)
print(london)
print(london.__dict__)

Первая строка создает объект с именем London. Когда мы создаем объект, метод __init__ класса City вызывается автоматически.

Как видите, при создании объекта мы передаем пять аргументов.

Но это не соответствует числу параметров, принимаемых методом __init__, которое равно шести. Это связано с тем, что параметр self не нужно указывать при построении экземпляра класса, он автоматически ссылается на него.

Вывод первого оператора печати:

<__main__.City object at 0x109755a10>

Он показывает ссылку на объект типа «Город» и его местоположение в памяти.

Вывод второго оператора печати:

{'temperature': 21, 'weather_type': 'Sunny', 'precipitation_chance': 0.1, 'humidity': 0.63, 'wind_speed': 10}

Метод __dict__ выводит пространство имен объекта (как видите, это словарь Python). В этом случае мы можем увидеть значение атрибутов, которые мы установили с помощью конструктора класса City.

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

Добавление метода в наш класс Python

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

Вот этот метод:

def print_summary(self):
        print('Weather forecasts for London - {}\nTemperature: {}°\nChance of precipitation: {}%\nHumidity: {}%\nWind speed: {} km/h\n'
           .format(self.weather_type, self.temperature, int(self.precipitation_chance*100), int(self.humidity*100), self.wind_speed))

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

Мы печатаем атрибуты объекта в удобочитаемом формате.

После создания нашего объекта города мы можем вызвать этот метод с помощью точечной нотации:

london = City(21, 'Sunny', 0.1, 0.63, 10)
london.print_summary()

Вывод:

Weather forecasts for London - Sunny
Temperature: 21°
Chance of precipitation: 10%
Humidity: 63%
Wind speed: 10 km/h

Вы можете увидеть, как этот метод помогает нам создать стандартный способ отображения прогнозов погоды для данного города. Если бы этого метода не было, разные разработчики, использующие наш класс City, отображали бы эту информацию по-разному.

Понимание атрибутов экземпляра

Пять атрибутов, заданных внутри метода __init__ нашего класса, называются атрибутами экземпляра (или переменными экземпляра), поскольку они принадлежат определенному экземпляру нашего класса.

Давайте создадим еще один объект на основе класса City, на этот раз для Рима.

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

Наш класс становится:

class City:
  
    def __init__(self, city_name, temperature, weather_type, precipitation_chance, humidity, wind_speed):
        self.city_name = city_name
        self.temperature = temperature
        self.weather_type = weather_type
        self.precipitation_chance = precipitation_chance
        self.humidity = humidity
        self.wind_speed = wind_speed

    def print_summary(self):
        print('Weather forecasts for {} - {}\nTemperature: {}°\nChance of precipitation: {}%\nHumidity: {}%\nWind speed: {} km/h\n'
           .format(self.city_name, self.weather_type, self.temperature, int(self.precipitation_chance*100), int(self.humidity*100), self.wind_speed))

Я добавил в конструктор новый параметр с именем city_name, а затем установил значение атрибута экземпляра city_name.

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

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

london = City('London', 21, 'Sunny', 0.1, 0.63, 10)
london.print_summary()

rome = City('Rome', 32, 'Sunny', 0.05, 0.67, 5)
rome.print_summary() 

Вот вывод, который мы получаем из метода класса print_summary:

Weather forecasts for London - Sunny
Temperature: 21°
Chance of precipitation: 10%
Humidity: 63%
Wind speed: 10 km/h

Weather forecasts for Rome - Sunny
Temperature: 32°
Chance of precipitation: 5%
Humidity: 67%
Wind speed: 5 km/h

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

print(london.temperature)
print(rome.temperature)
21
32

Как вы можете видеть, значение атрибута экземпляра « температура» меняется между нашими двумя экземплярами класса «Город».

Чтобы обновить этот атрибут экземпляра для объекта london, мы можем сделать следующее:

london.temperature = 23
london.print_summary()

Теперь вывод такой:

Weather forecasts for London - Sunny
Temperature: 23°
Chance of precipitation: 10%
Humidity: 63%
Wind speed: 10 km/h

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

Объявление атрибутов класса в Python

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

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

Вот пример: я хочу добавить атрибут класса к нашему классу City. Наш атрибут класса — это список, содержащий допустимые типы погоды. Цель — иметь возможность проверить weather_type, переданный конструктору, когда мы создаем новый объект.

Чтобы определить атрибут класса, мы делаем следующее (все остальное в нашем классе пока остается неизменным):

class City:
  
    valid_weather_types = ['Sunny', 'Cloudy', 'Rainy']

Экземпляры класса имеют доступ к атрибуту класса, и то же самое относится к самому классу.

Вот что я имею в виду:

print(london.valid_weather_types)
print(rome.valid_weather_types)
print(City.valid_weather_types)

['Sunny', 'Cloudy', 'Rainy']
['Sunny', 'Cloudy', 'Rainy']
['Sunny', 'Cloudy', 'Rainy']

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

def __init__(self, city_name, temperature, weather_type, precipitation_chance, humidity, wind_speed):
        if weather_type not in City.valid_weather_types:
            raise ValueError('Invalid weather type provided.')
        self.city_name = city_name
        self.temperature = temperature
        self.weather_type = weather_type
        self.precipitation_chance = precipitation_chance
        self.humidity = humidity
        self.wind_speed = wind_speed

Как видите, мы вызываем исключение ValueError, если пользователь передает недопустимый тип погоды при создании нового объекта.

Давайте создадим объект с неправильным типом погоды и посмотрим, что произойдет:

athens = City('Athens', 34, 'Partly cloudy', 0.02, 0.81, 4)

Вот ошибка, которую мы получаем:

Traceback (most recent call last):
  File "city.py", line 25, in <module>
    athens = City('Athens', 34, 'Partly cloudy', 0.02, 0.81, 4)
  File "city.py", line 7, in __init__
    raise ValueError('Invalid weather type provided.')
ValueError: Invalid weather type provided.

Как и ожидалось, мы получаем ошибку ValueError из-за недопустимого типа погоды.

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

Вот что я имею в виду:

athens = City('Athens', 34, 'Cloudy', 0.02, 0.81, 4)
athens.weather_type = 'Partly cloudy'
print(athens.weather_type)

Partly cloudy

Мы создали объект athens, используя допустимый weather_type, а затем установили его значение на недопустимый weather_type вне конструктора, не получив никаких ошибок.

Я покажу вам, как мы можем улучшить валидацию в другом уроке. На данный момент цель — начать знакомиться с классами Python.

Пространства имен классов и экземпляров Python

Чтобы понять, как работают атрибуты класса и экземпляра, важно познакомить с концепцией пространства имен, применяемой к классу Python.

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

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

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

Вот пространство имен для класса City:

print(City.__dict__)

{'__module__': '__main__', 'valid_weather_types': ['Sunny', 'Cloudy', 'Rainy'], '__init__': <function City.__init__ at 0x105b01710>, 'print_summary': <function City.print_summary at 0x105b0eb90>, '__dict__': <attribute '__dict__' of 'City' objects>, '__weakref__': <attribute '__weakref__' of 'City' objects>, '__doc__': None}

А вот пространство имен для экземпляра класса london:

print(london.__dict__)

{'city_name': 'London', 'temperature': 21, 'weather_type': 'Sunny', 'precipitation_chance': 0.1, 'humidity': 0.63, 'wind_speed': 10}

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

Итак, как я могу сделать следующее, если пространство имен экземпляра не содержит атрибут valid_weather_types?

print(london.valid_weather_types)

['Sunny', 'Cloudy', 'Rainy']

Вот логика, которой следует Python:

  • Найдите атрибут в пространстве имен экземпляра.
  • Если атрибут отсутствует в пространстве имен экземпляра, то посмотрите в пространстве имен класса (именно там Python находит атрибут класса valid_weather_types).

Теперь предположим, что мы делаем следующее:

london.valid_weather_types = ['Sunny', 'Cloudy', 'Partly cloudy', 'Rainy']

Изменяем ли мы атрибут класса, который мы определили ранее?

Давайте еще раз посмотрим на пространства имен:

print(City.__dict__)
{'__module__': '__main__', 'valid_weather_types': ['Sunny', 'Cloudy', 'Rainy'], '__init__': <function City.__init__ at 0x10c773710>, 'print_summary': <function City.print_summary at 0x10c780b90>, '__dict__': <attribute '__dict__' of 'City' objects>, '__weakref__': <attribute '__weakref__' of 'City' objects>, '__doc__': None}

print(london.__dict__)
{'city_name': 'London', 'temperature': 21, 'weather_type': 'Sunny', 'precipitation_chance': 0.1, 'humidity': 0.63, 'wind_speed': 10, 'valid_weather_types': ['Sunny', 'Cloudy', 'Partly cloudy', 'Rainy']}

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

Таким образом, наше задание добавило новый атрибут экземпляра к экземпляру london.

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

Давайте убедимся, что это так для экземпляра rome:

print(rome.__dict__)
{'city_name': 'Rome', 'temperature': 32, 'weather_type': 'Sunny', 'precipitation_chance': 0.05, 'humidity': 0.67, 'wind_speed': 5}

print(rome.valid_weather_types)
['Sunny', 'Cloudy', 'Rainy']

Как и ожидалось, атрибут valid_weather_types отсутствует в пространстве имен экземпляра rome и разрешается через пространство имен класса.

Как использовать атрибуты класса

Атрибуты класса могут быть полезны разными способами. Вы можете применить этот пример ко многим типам приложений.

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

Давайте определим атрибут класса под названием cities_number. Мы будем увеличивать этот атрибут каждый раз, когда создаем новый экземпляр. Это означает, что нам также придется изменить реализацию конструктора __init__.

Наш класс становится:

class City:
  
    cities_number = 0
    valid_weather_types = ['Sunny', 'Cloudy', 'Rainy']

    def __init__(self, city_name, temperature, weather_type, precipitation_chance, humidity, wind_speed):
        if weather_type not in City.valid_weather_types:
            raise ValueError('Invalid weather type provided.')
        self.city_name = city_name
        self.temperature = temperature
        self.weather_type = weather_type
        self.precipitation_chance = precipitation_chance
        self.humidity = humidity
        self.wind_speed = wind_speed
        City.cities_number += 1

[the print_summary method doesn't change]

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

london = City('London', 21, 'Sunny', 0.1, 0.63, 10)
print(City.cities_number)
1

rome = City('Rome', 32, 'Sunny', 0.05, 0.67, 5)
print(City.cities_number)
2

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

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

Определение константы с использованием атрибута класса

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

В нашем примере я хочу иметь возможность предоставить температуру в градусах Цельсия и Фаренгейта. Для этого мне нужен способ преобразовать значение Цельсия, предоставленное через конструктор, в его представление в градусах Фаренгейта.

Формула для перевода температуры по Цельсию (Tc) в температуру по Фаренгейту (Tf) следующая:

Tf = Tc * 9/5+32

В нашем классе определим 9/5 (что равно 1,8) как атрибут класса с именем temperature_conversion_factor.

Затем мы будем использовать этот атрибут класса для печати температуры по Фаренгейту с помощью метода print_summary. Ниже вы можете увидеть обновленный класс:

class City:
    cities_number = 0
    valid_weather_types = ['Sunny', 'Cloudy', 'Rainy']
    temperature_conversion_factor = 1.8

    def __init__(self, city_name, temperature, weather_type, precipitation_chance, humidity, wind_speed):
        if weather_type not in City.valid_weather_types:
            raise ValueError('Invalid weather type provided.')
        self.city_name = city_name
        self.temperature = temperature
        self.weather_type = weather_type
        self.precipitation_chance = precipitation_chance
        self.humidity = humidity
        self.wind_speed = wind_speed
        City.cities_number += 1

    def print_summary(self):
        print('Weather forecasts for {} - {}\nTemperature: {}°C/{}°F\nChance of precipitation: {}%\nHumidity: {}%\nWind speed: {} km/h\n'
           .format(self.city_name, self.weather_type, self.temperature, int(self.temperature*City.temperature_conversion_factor+32), int(self.precipitation_chance*100), int(self.humidity*100), self.wind_speed))

Давайте посмотрим на часть кода, которая преобразует температуру из градусов Цельсия в градусы Фаренгейта:

int(self.temperature*City.temperature_conversion_factor+32)

Следует подчеркнуть, что в этой формуле я использую атрибут класса temperature_conversion_factor.

Учитывая, что это атрибут класса, для ссылки на него я использую имя класса (City), за которым следует точка и имя атрибута.

Если я вызову метод print_summary для объектов london и rome, я получу следующий вывод:

Weather forecasts for London - Sunny
Temperature: 21°C/69°F
Chance of precipitation: 10%
Humidity: 63%
Wind speed: 10 km/h

Weather forecasts for Rome - Sunny
Temperature: 32°C/89°F
Chance of precipitation: 5%
Humidity: 67%
Wind speed: 5 km/h

И это все в этом уроке!

Ниже представлен видеообзор, который поможет вам усвоить больше информации о создании класса в Python:

Заключение

В заключение, в этом уроке мы работали над определением класса Python. Мы начали с метода класса __init__, который работает как конструктор и позволяет создавать новые экземпляры класса (или объекты).

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

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

Следующим шагом было понимание разницы между атрибутами экземпляра и атрибутами класса. Мы также увидели роль пространств имен в том, как атрибуты класса и экземпляра идентифицируются в программе Python.

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

  • отслеживать показатель на уровне класса (например, количество городов, для которых мы предоставляем прогнозы погоды).
  • определите константу, которую вы можете использовать в методах нашего класса.

Теперь у вас достаточно знаний, чтобы начать создавать свои собственные классы!