Классы в Python

Введение

Классы нужно называть с заглавной буквы и использовать CamelCase.

Создается новый класс с помощью class

Объект данного класса создаётся с помощью =

объект = ИмяКласса()

Самый простой пример использования

class Site: pass my_site = Site()

Создан класс Site и его экземпляр my_site

Этот процесс называется инстанцированием (instantiation).

В англоязычной среде экземпляр класса называется instance (instance of the class).

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

print(dir(my_site))

['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__']

__class__

C помощью встроенного атрибута __class__ можно увидеть принадлежность my_site классу Site.

Также для этой цели можно использовать функцию type()

class Site: pass my_site = Site() print(my_site.__class__) # либо print(type(my_site))

<class '__main__.Site'>

Когда мы создаём новый экземпляр класса

my_site = Site()

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

Из этой информации можно выделить минимум две важные детали:

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

Во-вторых, у метод __init__() есть параметр по-умолчанию, который нужен для получения доступа к памяти, которую под объект выделил метод __new__(). Именно этот параметр принято называть self.

Классы без кастомных атрибутов и методов могут использоваться для обработки ошибок

class MyAPIException(Exception): """ My API exception. """

Тем не менее основные преимущества ООП раскрываются именно через использование атрибутов и методов.

Атрибуты

Атрибуты это свойства объекта. Бытовой пример: у велосипеда есть страна производитель , масса, цвет, количество скоростей, цена и так далее.

У сайта в интернете есть, url, год делегирования домена, автор, основная тема.

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

Синтаксис при создании и при доступе следующий

объект.имя_атрибута

Это равносильно следующему синтаксису с явным обращением к __dict__()

объект.__dict__["имя_атрибута"]

Создадим класс Site и объект hh с адресом сайта.

class Site: pass hh = Site() hh.url = "https://www.heihei.ru"

В предыдущей главе мы изучали пустой экземпляр. Теперь можно посмотреть какой эффект даёт создание аттрибута

print(dir(hh))

['__class__', '__delattr__', '__dict__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__', '__weakref__', 'url']

Видно, что к дефолтным атрибутам добавился атрибут url

В следующем примере создадим два экземпляра одного и того же класса Employee

class Employee: pass emp_1 = Employee() emp_2 = Employee() print(emp_1) print(emp_2)

python emp_ex.py

<__main__.Employee object at 0x7fca67c26fd0> <__main__.Employee object at 0x7fca67c26fa0>

Объекты emp_1 и emp_2 имеют разные адреса в памяти.

Зададим несколько атрибутов

class Employee: pass emp_1 = Employee() emp_2 = Employee() emp_1.first = 'Yuri' emp_1.last = 'Gagarin' emp_1.email = 'Yuri.Gagarin@vostok.su' emp_1.pay = 1000 emp_2.first = 'Test' emp_2.last = 'User' emp_2.email = 'Test.User@vostok.su' emp_2.pay = 1234 print(emp_1.email) print(emp_2.email)

python emp_ex.py

Yuri.Gagarin@vostok.su Test.User@vostok.su

__init__

Создавать атрибуты способом из предыдущего параграфа не всегда удобно.

Обычно используют метод __init__()

Важно понимать следующее

Каждый раз когда создаётся новый объект данного класса сразу после метода __new__() вызывается метод __init__().

Разница в том, что в прошлом параграфе мы не вносили в него никаких изменений и он просто создавал объект.

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

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

Далее разберёмся с тем что такое self и продолжим изучение __init__()

self

Когда создается новый объект класса метод __new__() выделяет память под этот объект и передаёт его в метод __init__()

То есть метод __init__() изначально ожидает получить хотя бы один аргумент.

Если в метод __init__() нужно добавить какой-то функционал важно понимать, что первый параметр уже занят. Его рекомендуется называть self хотя работать будет и любое другое разрешенное название.

self.атрибут = значение

Или если вы делаете атрибут, который не должен быть доступен извне - добавьте перед именем _

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

self._атрибут = значение

Создадим класс Cat, у которого будет один атрибут, отвечающий за породу - breed.

class Cat: def __init__(kitty): kitty.breed = "burma" my_cat = Cat() print(my_cat.breed)

burma

У класса Cat минимум два недостатка:

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

Во-вторых, значение атрибута breed всегда будет burma, оно не передаётся в класс как аргумент.

Внесём исправления

class Cat: def __init__(self, breed): self.breed = breed

Теперь, если вы поменяете название класса, внутри переименовывать ничего не нужно.

Переименовываем Cat на Dog

class Dog: def __init__(self, breed): self.breed = breed

Если у класса есть атрибут, который __init__() ожидает получить как аргумент, нужно внимательнее отнестить к созданию объекта

my_dog = Dog()

Уже не сработает

Traceback (most recent call last): File "/home/andrei/python/sample.py", line 6, in <module> my_dog = Dog() TypeError: __init__() missing 1 required positional argument: 'breed'

Теперь при создании объекта нужно задавать желаемое значение атрибута

my_dog = Dog("husky")

Либо в методе __init__() нужно указать значение по умолчанию

class Dog: def __init__(self, breed="aidi"): self.breed = breed

Новичкам может показаться странной строка

self.breed = breed

Здесь главное усвоить следующее:

Именно self.breed - задаёт название атрибута, которое будет у объекта данного класса.

В данном случае атрибут будет называться breed

breed - это название параметра у метода __init__().

В данном случае - первый аргумент, передаваемый вами в метод __init__() называется breed

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

class Dog: def __init__(self, arg1): self.breed = arg1

Теперь попробую объяснить тоже самое, но немного по-другому и уже с двумя атрибутами

class Dog: def __init__(self, name, age): self.name = name self.age = age

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

Зелёным я выделил названия параметров. Часто в примерах у атрибута и параметра одинаковые названия. Это не обязательное условие, которое может помешать пониманию сути.

Сравните следующие объявления классов.

Только формальные названия из которых ничего не понятно

class Dog: def __init__(self, arg1, arg2): self.attr1 = arg1 self.attr2 = arg2 pet = Dog("Tuzik", 2) print(pet.attr1) print(pet.attr2)

Понятные названия, но у атрибутов и у параметров __init__() они разные

class Dog: def __init__(self, name, age): self.nick = name self.years = age pet = Dog("Tuzik", 2) print(pet.nick) print(pet.year)

Понятные и одинаковые названия

class Dog: def __init__(self, name, age): self.name = name self.age = age pet = Dog("Tuzik", 2) print(pet.name) print(pet.age)

Результат вызова у всех трёх вариантов одинаковый

Tuzik
2

То что названия одинаковые не является проблемой так как переданные в метод аргументы попадают в пространство имён метода, а то что используется после self. - это в пространстве имён атрибутов нового экземпляра класса в его внутреннем словаре.

Если вам кажется, что self.name = name это избыточный код , изучите статью «Python dataclass»

Важно понимать, что метод __init__() может создавать атрибуты не по принципу один к одному - получил аргумет и записал его в атрибут а комбинируя различные аргументы

class Employee: def __init__(self, name, surname): self.fullname = name + "_" + surname tester = Employee("Max", "Petrov") print(tester.fullname)

В примере выше при создании объекта tester мы в __init__() передали аргументы name и surname, на основе которых был создан атрибут fullname.

Никаких отдельных атрибутов для name и surname создано не было и попытка обратиться к ним приведёт к ошибкам

AttributeError: 'Employee' object has no attribute 'name'
AttributeError: 'Employee' object has no attribute 'surname'

Пример с тремя атрибутами, сделаем третий атрибут логическим типом (boolean)

class Dog: def __init__(self, breed, name, spots): self.breed = breed self.name = name self.spots = spots my_dog = Dog("husky", "Barbos", True) print(my_dog.breed, my_dog.name, my_dog.spots)

husky Barbos True

В начале статье мы обсуждали атрибут __dict__

Сейчас в классе есть целых три кастомных атрибута и можно убедиться, что словарь уже не пустой

… print(my_dog.__dict__)

{'breed': 'husky', 'name': 'Barbos', 'spots': True}

Изображение баннера

Статические атрибуты

Можно задавать атрибуты сразу для всех элементов класса. Они называются статическими (static attributes, class object attributes)

class Dog: # CLASS OBJECT ATTRIBUTE # SAME FOR ANY INSTANCE OF A CLASS bioclass = "mammal" def __init__(self, breed, name, spots): self.breed = breed self.name = name self.spots = spots # При создании объекта static атрибут можно не указывать явно my_dog = Dog("husky", "Barbos", True) # Проверим значение bioclass заданное по умолчанию print(Dog.bioclass, my_dog.bioclass)

mammal mammal

… # Переопределим значение bioclass для my_dog my_dog.bioclass = "super mammal" print(Dog.bioclass, my_dog.bioclass)

mammal super mammal

Объекты

Все классы в Python кроме Exception наследуются от object

>>> o = object()
>>> dir(o)

['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__format__', '__ge__', '__getattribute__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '__lt__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '__sizeof__', '__str__', '__subclasshook__']

Пример

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

Объекты и экземпляры класса

Объекты класса (class objects) и экземпляры класса (instance of class) это разные вещи.

class создаёт именованную ссылку на объект класса.

Рассмотрим класс Resolver из статьи про кэширование

import socket class Resolver: def __init__(self): self._cache = {} def __call__(self, host): if host not in self._cache: self._cache[host] = socket.gethostbyname(host) return self._cache[host] def clear(self): self._cache.clear() def has_host(self, host): return host in self._cache

Импортируем в REPL класс Resolver

python >>> from resolver import Resolver

Если с помощью REPL выполнить (evaluate) Resolver

>>> Resolver

REPL покажет представление (representation) объекта класса (class object)

<class 'resolver.Resolver'>

Этот объект класса является вызываемым (callable) чем мы и пользуемся

resolve = Resolver()

Аргументы, которые передаются в объект класса перенаправляются в метод __init__() этого класса, если он определён.

Классы создают новые сущности в момент вызова.

О создании сущностей мы поговорим позже.

def main(): seq = sequence_class(immutable=True) t = seq("Timbuktu") print(t) print(type(t)) def sequence_class(immutable): if immutable: cls = tuple else: cls = list return cls if __name__ == "__main__": main()

python sequence_class.py

('T', 'i', 'm', 'b', 'u', 'k', 't', 'u') <class 'tuple'>

def main(): seq = sequence_class(immutable=False) t = seq("Timbuktu") print(t) print(type(t)) def sequence_class(immutable): return tuple if immutable else list if __name__ == "__main__": main()

python sequence_class.py

['T', 'i', 'm', 'b', 'u', 'k', 't', 'u'] <class 'list'>

__dict__

Объекты в Python имеют вид словарей . Изучить словарь нашего нового объекта my_site можно с помощью атрибута __dict__

class Site: pass my_site = Site() print(my_site.__dict__)

{}

Словарь пустой, потому что класс пустой.

При помощи __dict__ просходит создание атрибутов и обращение к ним.

Хотя обычно для краткости пользуются обращением через .

x.__dict__["a"] # то же самое что x.a

Пример

class Site: pass hh = Site() hh.__dict__["url"] = "https://topbicycle.ru" print(hh.__dict__["url"]) # то же самое что print(hh.url) hh.url = "https://www.heihei.ru" print(hh.__dict__["url"]) # то же самое что print(hh.url)

https://topbicycle.ru https://topbicycle.ru https://www.heihei.ru https://www.heihei.ru

Вернёмся к классу Dog с тремя атрибутами и изучим содержимое __dict__

class Dog: def __init__(self, breed, name, spots): self.breed = breed self.name = name self.spots = spots my_dog = Dog("husky", "Barbos", True) print(my_dog.__dict__)

Сейчас в классе есть целых три кастомных атрибута и можно убедиться, что словарь уже не пустой

{'breed': 'husky', 'name': 'Barbos', 'spots': True}

Автор статьи: Андрей Олегович

Изображение баннера