Как переопределить атрибут базового класса python
Перейти к содержимому

Как переопределить атрибут базового класса python

  • автор:

Как переопределить атрибут базового класса python

В прошлой статье класс Employee полностью перенимал функционал класса Person:

class Person: def __init__(self, name): self.__name = name # имя человека @property def name(self): return self.__name def display_info(self): print(f"Name: ") class Employee(Person): def work(self): print(f" works")

Но что, если мы хотим что-то изменить из этого функционала? Например, добавить работнику через конструктор, новый атрибут, который будет хранить компанию, где он работает или изменить реализацию метода display_info. Python позволяет переопределить функционал базового класса.

Например, изменим классы следующим образом:

class Person: def __init__(self, name): self.__name = name # имя человека @property def name(self): return self.__name def display_info(self): print(f"Name: ") class Employee(Person): def __init__(self, name, company): super().__init__(name) self.company = company def display_info(self): super().display_info() print(f"Company: ") def work(self): print(f" works") tom = Employee("Tom", "Microsoft") tom.display_info() # Name: Tom # Company: Microsoft

Здесь в классе Employee добавляется новый атрибут — self.company , который хранит компания работника. Соответственно метод __init__() принимает три параметра: второй для установки имени и третий для установки компании. Но если в базом классе определен конструктор с помощью метода __init__, и мы хотим в производном классе изменить логику конструктора, то в конструкторе производного класса мы должны вызвать конструктор базового класса. То есть в конструкторе Employee надо вызвать конструктор класса Person.

Для обращения к базовому классу используется выражение super() . Так, в конструкторе Employee выполняется вызов:

super().__init__(name)

Это выражение будет представлять вызов конструктора класса Person, в который передается имя работника. И это логично. Ведь имя работника устанавливается именно в конструкторе класса Person. В самом конструкторе Employee лишь устанавливаем свойство company.

Кроме того, в классе Employee переопределяется метод display_info() — в него добавляется вывод компании работника. Причем мы могли определить этот метод следующим образом:

def display_info(self): print(f"Name: ") print(f"Company: ")

Но тогда строка вывода имени повторяла бы код из класса Person. Если эта часть кода совпадает с методом из класса Person, то нет смысла повторяться, поэтому опять же с помощью выражения super() обращаемся к реализации метода display_info в классе Person:

def display_info(self): super().display_info() # обращение к методу display_info в классе Person print(f"Company: ")

Затем мы можем вызвать вызвать конструктор Employee для создания объекта этого класса и вызвать метод display_info:

tom = Employee("Tom", "Microsoft") tom.display_info()

Консольный вывод программы:

Name: Tom Company: Microsoft

Проверка типа объекта

При работе с объектами бывает необходимо в зависимости от их типа выполнить те или иные операции. И с помощью встроенной функции isinstance() мы можем проверить тип объекта. Эта функция принимает два параметра:

isinstance(object, type)

Первый параметр представляет объект, а второй — тип, на принадлежность к которому выполняется проверка. Если объект представляет указанный тип, то функция возвращает True. Например, возьмем следующую иерархию классов Person-Employee/Student:

class Person: def __init__(self, name): self.__name = name # имя человека @property def name(self): return self.__name def do_nothing(self): print(f" does nothing") # класс работника class Employee(Person): def work(self): print(f" works") # класс студента class Student(Person): def study(self): print(f" studies") def act(person): if isinstance(person, Student): person.study() elif isinstance(person, Employee): person.work() elif isinstance(person, Person): person.do_nothing() tom = Employee("Tom") bob = Student("Bob") sam = Person("Sam") act(tom) # Tom works act(bob) # Bob studies act(sam) # Sam does nothing

Здесь класс Employee определяет метод work(), а класс Student — метод study.

Здесь также определена функция act , которая проверяет с помощью функции isinstance , представляет ли параметр person определнный тип, и зависимости от результатов проверки обращается к определенному методу объекта.

Наследование. Атрибуты private и protected

Мы продолжаем изучение темы «наследование». На этом занятии мы увидим, как влияет режим доступа private и protected атрибутов при наследовании классов.

  • _attribute (с одним подчеркиванием) – режим доступа protected (служит для обращения внутри класса и во всех его дочерних классах)
  • __attribute (с двумя подчеркиваниями) – режим доступа private (служит для обращения только внутри класса).
class Geom: name = 'Geom' def __init__(self, x1, y1, x2, y2): print(f"инициализатор Geom для ") self.__x1 = x1 self.__y1 = y1 self.__x2 = x2 self.__y2 = y2 class Rect(Geom): def __init__(self, x1, y1, x2, y2, fill='red'): super().__init__(x1, y1, x2, y2) self.__fill = fill

Здесь мы пытаемся в инициализаторе базового класса Geom сформировать приватные локальные свойства с координатами прямоугольника. Дополнительно в инициализаторе самого класса создается приватное свойство __fill. Ниже создадим объект класса Rect:

r = Rect(0, 0, 10, 20)

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

print(r.__dict__)

После запуска программы увидим следующие строчки: инициализатор Geom для
Смотрите, локальные свойства с координатами имеют префикс _Geom, то есть, префикс того класса, в котором они непосредственно были прописаны. Несмотря на то, что параметр self является ссылкой на объект класса Rect. Это особенность поведения (формирования) приватных атрибутов в базовых классах. У них всегда добавляется префикс именно базового класса, а не класса объекта self. А вот последнее свойство __fill имеет ожидаемый префикс _Rect, так как оно было создано в классе Rect. Что из этого следует? Во-первых, мы, конечно же, не можем обратиться в свойствам-координатам в дочернем классе Rect. Если в нем прописать метод get_coords():

def get_coords(self): return (self.__x1, self.__y1, self.__x2, self.__y2)

а, затем, вызвать через объект класса Rect:

r.get_coords()

то увидим ошибку AttributeError. Но если перенести этот метод в базовый класс Geom, то все сработает без ошибок, так как приватным свойствам будет добавлен правильный префикс _Geom. Возможно, вам кажется это немного запутанным? Но давайте вспомним, а для чего вообще нужны и когда используются приватные атрибуты. Мы говорили, что это закрытые от внешнего вмешательства свойства или методы текущего класса, доступные только внутри этого класса и недоступные из других, в том числе и из дочерних классов. Именно поэтому приватные атрибуты жестко привязываются к текущему классу, в котором они создаются, так как по логике предполагается их использовать только внутри этого класса и больше нигде. Если же нам нужно определить закрытые атрибуты, доступные в текущем классе и во всех его дочерних классах, то для этого следует использовать метод определения protected – одно нижнее подчеркивание. Поэтому правильнее было бы создавать свойства-координаты в базовом инициализаторе в режиме protected:

class Geom: name = 'Geom' def __init__(self, x1, y1, x2, y2): print(f"инициализатор Geom для ") self._x1 = x1 self._y1 = y1 self._x2 = x2 self._y2 = y2 class Rect(Geom): def __init__(self, x1, y1, x2, y2, fill='red'): super().__init__(x1, y1, x2, y2) self._fill = fill def get_coords(self): return (self._x1, self._y1, self._x2, self._y2)

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

r = Rect(0, 0, 10, 20) print(r.__dict__) r.get_coords()

После запуска программы увидим следующие строчки: инициализатор Geom для
Опять же, как я ранее отмечал, режим доступа protected в реальности никак не ограничивает доступ к атрибутам объектов класса или самого класса. Например, мы можем обратиться к координатам напрямую через экземпляр класса:

print(r._x1)

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

Атрибуты private и protected на уровне класса

Все также работает и с атрибутами уровня класса. Например, сейчас мы совершенно спокойно можем обратиться к свойству name класса Geom через объект класса Rect:

print(r.name)

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

print(r._name)

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

print(r.__name)
class Rect(Geom): def __init__(self, x1, y1, x2, y2, fill='red'): super().__init__(x1, y1, x2, y2) self._fill = fill self._name = self.__name

Но в Geom мы можем к ней обращаться:

class Geom: __name = 'Geom' def __init__(self, x1, y1, x2, y2): print(f"инициализатор ") self._x1 = x1 self._y1 = y1 self._x2 = x2 self._y2 = y2

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

class Geom: . def __verify_coord(self, coord): return 0  coord  100

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

class Rect(Geom): def __init__(self, x1, y1, x2, y2, fill='red'): super().__init__(x1, y1, x2, y2) super().__verify_coord(x1) self._fill = fill

Примеры работы с классами в Python

Python — объектно-ориентированный язык с начала его существования. Поэтому, создание и использование классов и объектов в Python просто и легко. Эта статья поможет разобраться на примерах в области поддержки объектно-ориентированного программирования Python. Если у вас нет опыта работы с объектно-ориентированным программированием (OOП), ознакомьтесь с вводным курсом или учебным пособием, чтобы понять основные понятия.

Создание классов

Оператор class создает новое определение класса. Имя класса сразу следует за ключевым словом class , после которого ставиться двоеточие:

class ClassName: """Необязательная строка документации класса""" class_suite
  • У класса есть строка документации, к которой можно получить доступ через ClassName.__doc__ .
  • class_suite состоит из частей класса, атрибутов данных и функции.

Пример создания класса на Python:

 
class Employee: """Базовый класс для всех сотрудников""" emp_count = 0 def __init__(self, name, salary): self.name = name self.salary = salary Employee.emp_count += 1 def display_count(self): print('Всего сотрудников: %d' % Employee.empCount) def display_employee(self): print('Имя: <>. Зарплата: <>'.format(self.name, self.salary))
  • Переменная emp_count — переменная класса, значение которой разделяется между экземплярами этого класса. Получить доступ к этой переменной можно через Employee.emp_count из класса или за его пределами.
  • Первый метод __init__() — специальный метод, который называют конструктором класса или методом инициализации. Его вызывает Python при создании нового экземпляра этого класса.
  • Объявляйте другие методы класса, как обычные функции, за исключением того, что первый аргумент для каждого метода self . Python добавляет аргумент self в список для вас; и тогда вам не нужно включать его при вызове этих методов.

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

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

 
# Это создаст первый объект класса Employee emp1 = Employee("Андрей", 2000) # Это создаст второй объект класса Employee emp2 = Employee("Мария", 5000)

Доступ к атрибутам

Получите доступ к атрибутам класса, используя оператор . после объекта класса. Доступ к классу можно получить используя имя переменой класса:

 
emp1.display_employee() emp2.display_employee() print("Всего сотрудников: %d" % Employee.emp_count)

Теперь, систематизируем все.

 
class Employee: """Базовый класс для всех сотрудников""" emp_count = 0 def __init__(self, name, salary): self.name = name self.salary = salary Employee.emp_count += 1 def display_count(self): print('Всего сотрудников: %d' % Employee.emp_count) def display_employee(self): print('Имя: <>. Зарплата: <>'.format(self.name, self.salary)) # Это создаст первый объект класса Employee emp1 = Employee("Андрей", 2000) # Это создаст второй объект класса Employee emp2 = Employee("Мария", 5000) emp1.display_employee() emp2.display_employee() print("Всего сотрудников: %d" % Employee.emp_count)

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

Имя: Андрей. Зарплата: 2000 Имя: Мария. Зарплата: 5000 Всего сотрудников: 2 

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

 
emp1.age = 7 # Добавит атрибут 'age' emp1.age = 8 # Изменит атрибут 'age' del emp1.age # Удалит атрибут 'age'

Вместо использования привычных операторов для доступа к атрибутам вы можете использовать эти функции:

  • getattr(obj, name [, default]) — для доступа к атрибуту объекта.
  • hasattr(obj, name) — проверить, есть ли в obj атрибут name .
  • setattr(obj, name, value) — задать атрибут. Если атрибут не существует, он будет создан.
  • delattr(obj, name) — удалить атрибут.
 
hasattr(emp1, 'age') # возвращает true если атрибут 'age' существует getattr(emp1, 'age') # возвращает значение атрибута 'age' setattr(emp1, 'age', 8) #устанавливает атрибут 'age' на 8 delattr(empl, 'age') # удаляет атрибут 'age'

Встроенные атрибуты класса

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

  • __dict__ — словарь, содержащий пространство имен класса.
  • __doc__ — строка документации класса. None если, документация отсутствует.
  • __name__ — имя класса.
  • __module__ — имя модуля, в котором определяется класс. Этот атрибут __main__ в интерактивном режиме.
  • __bases__ — могут быть пустые tuple, содержащие базовые классы, в порядке их появления в списке базового класса.

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

 
class Employee: """Базовый класс для всех сотрудников""" emp_count = 0 def __init__(self, name, salary): self.name = name self.salary = salary Employee.empCount += 1 def display_count(self): print('Всего сотрудников: %d' % Employee.empCount) def display_employee(self): print('Имя: <>. Зарплата: <>'.format(self.name, self.salary)) print("Employee.__doc__:", Employee.__doc__) print("Employee.__name__:", Employee.__name__) print("Employee.__module__:", Employee.__module__) print("Employee.__bases__:", Employee.__bases__) print("Employee.__dict__:", Employee.__dict__)

Когда этот код выполняется, он возвращает такой результат:

Employee.__doc__: Базовый класс для всех сотрудников Employee.__name__: Employee Employee.__module__: __main__ Employee.__bases__: (,) Employee.__dict__: , 'display_count': , 'display_employee': , '__dict__': , '__weakref__': > 

Удаление объектов (сбор мусора)

Python автоматически удаляет ненужные объекты (встроенные типы или экземпляры классов), чтобы освободить пространство памяти. С помощью процесса ‘Garbage Collection’ Python периодически восстанавливает блоки памяти, которые больше не используются.

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

Когда объект присваивают новой переменной или добавляют в контейнер (список, кортеж, словарь), количество ссылок объекта увеличивается. Количество ссылок на объект уменьшается, когда он удаляется с помощью del , или его ссылка выходит за пределы видимости. Когда количество ссылок достигает нуля, Python автоматически собирает его.

a = 40 # создали объект b = a # увеличивает количество ссылок c = [b] # увеличивает количество ссылок del a # уменьшает количество ссылок b = 100 # уменьшает количество ссылок c[0] = -1 # уменьшает количество ссылок

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

Пример работы __del__()
Деструктор __del__() выводит имя класса того экземпляра, который должен быть уничтожен:

 
class Point: def __init__(self, x=0, y=0): self.x = x self.y = y def __del__(self): class_name = self.__class__.__name__ print('<> уничтожен'.format(class_name)) pt1 = Point() pt2 = pt1 pt3 = pt1 print(id(pt1), id(pt2), id(pt3)) # выведите id объектов del pt1 del pt2 del pt3

Когда вышеуказанный код выполняется и выводит следующее:

17692784 17692784 17692784 Point уничтожен

В идеале вы должны создавать свои классы в отдельном модуле. Затем импортировать их в основной модуль программы с помощью import SomeClass .

Наследование класса в python

Наследование — это процесс, когда один класс наследует атрибуты и методы другого. Класс, чьи свойства и методы наследуются, называют Родителем или Суперклассом. А класс, свойства которого наследуются — класс-потомок или Подкласс.

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

Класс наследник наследует атрибуты своего родительского класса. Вы можете использовать эти атрибуты так, как будто они определены в классе наследнике. Он может переопределять элементы данных и методы родителя.

Синтаксис наследования класса

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

class SubClassName(ParentClass1[, ParentClass2, . ]): """Необязательная строка документации класса""" class_suite

Пример наследования класса в Python

 
class Parent: # объявляем родительский класс parent_attr = 100 def __init__(self): print('Вызов родительского конструктора') def parent_method(self): print('Вызов родительского метода') def set_attr(self, attr): Parent.parent_attr = attr def get_attr(self): print('Атрибут родителя: <>'.format(Parent.parent_attr)) class Child(Parent): # объявляем класс наследник def __init__(self): print('Вызов конструктора класса наследника') def child_method(self): print('Вызов метода класса наследника') c = Child() # экземпляр класса Child c.child_method() # вызов метода child_method c.parent_method() # вызов родительского метода parent_method c.set_attr(200) # еще раз вызов родительского метода c.get_attr() # снова вызов родительского метода

Когда этот код выполняется, он выводит следующий результат:

Вызов конструктора класса наследника Вызов метода класса наследника Вызов родительского метода Атрибут родителя: 200

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

class A: # объявите класс A . class B: # объявите класс B . class C(A, B): # C наследуется от A и B . 

Вы можете использовать функции issubclass() или isinstance() для проверки отношений двух классов и экземпляров.

  • Логическая функция issubclass(sub, sup) возвращает значение True , если данный подкласс sub действительно является подклассом sup .
  • Логическая функция isinstance(obj, Class) возвращает True , если obj является экземпляром класса Class или является экземпляром подкласса класса.

Переопределение методов

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

Пример переопределения методов:

 
class Parent: # объявите родительский класс def my_method(self): print('Вызов родительского метода') class Child(Parent): # объявите класс наследник def my_method(self): print('Вызов метода наследника') c = Child() # экземпляр класса Child c.my_method() # метод переопределен классом наследником

Когда этот код выполняется, он производит следующий результат:

Вызов метода наследника

Популярные базовые методы

В данной таблице перечислены некоторые общие функции. Вы можете переопределить их в своих собственных классах.

Метод, описание и пример вызова
1 __init__(self [, args. ]) — конструктор (с любыми необязательными аргументами)
obj = className(args)
2 __del__(self) — деструктор, удаляет объект
del obj
3 __repr__(self) — программное представление объекта
repr(obj)
4 __str__(self) — строковое представление объекта
str(obj)

Пример использования __add__

Предположим, вы создали класс Vector для представления двумерных векторов. Что происходит, когда вы используете дополнительный оператор для их добавления? Скорее всего, Python будет против.

Однако вы можете определить метод __add__ в своем классе для добавления векторов и оператор + будет вести себя так как нужно.

 
class Vector: def __init__(self, a, b): self.a = a self.b = b def __str__(self): return 'Vector (<>, <>)'.format(self.a, self.b) def __add__(self, other): return Vector(self.a + other.a, self.b + other.b) v1 = Vector(2, 10) v2 = Vector(5, -2) print(v1 + v2)

При выполнении этого кода, мы получим:

Vector(7, 8)

Приватные методы и атрибуты

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

Пример приватного атрибута:

 
class JustCounter: __secret_count = 0 def count(self): self.__secret_count += 1 print(self.__secret_count) counter = JustCounter() counter.count() counter.count() print(counter.__secret_count)

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

1 2 Traceback (most recent call last): File "test.py", line 12, in print(counter.__secret_count) AttributeError: 'JustCounter' object has no attribute '__secret_count'

Вы можете получить доступ к таким атрибутам, так object._className__attrName . Если вы замените свою последнюю строку следующим образом, то она будет работать.

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

У меня есть класс BaseWeapon как сделать так, чтобы после наследования его другим классом обязательно нужно было переопределять атрибут self.stats ? Предполагаю, что нужно использовать модуль abc из стандартной библиотеки. Класс BaseWeapon:

class BaseWeapon: class Stats: def __init__(self, power): self.power = power def __init__(self, owner, name): self._owner = owner self._name = name self._stats = None @property def stats(self): return self._stats @stats.setter def stats(self, another_stats): if isinstance(another_stats, self.Stats): self._stats = another_stats else: raise TypeError(f'Value must be Stats class, not ') 

Примеры ожидаемых подклассов:

class NoWeapon(BaseWeapon): def __init__(self, owner): super().__init__(owner, 'Empty') self.stats = self.Stats(power=1) class Claws(BaseWeapon): def __init__(self, owner): super().__init__(owner, 'Claws') self.stats = self.Stats(power=10) 

Отслеживать
149k 12 12 золотых знаков 59 59 серебряных знаков 132 132 бронзовых знака
задан 11 мая 2020 в 15:58
Code Is my life Code Is my life
166 8 8 бронзовых знаков

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

11 мая 2020 в 16:26
@Евгений Какой именно параметр? _stats или может property stats?
11 мая 2020 в 16:28
не объявлять _stats внутри родительского __init__ . Ведь его вам нужно обязать переопределять?
11 мая 2020 в 16:30
чёт фигня-все мечи 10 повара будут ? пусть лучше клас принимает этотот стат при создании
11 мая 2020 в 16:31
@CodeIsmylife документация/описание класса
11 мая 2020 в 16:37

1 ответ 1

Сортировка: Сброс на вариант по умолчанию

Вы можете вовсе не объявлять _stats в родительском классе, но всё также обращаться к нему в методах этого класса.
Результатом такого решения будет то, что при попытке вызвать любой метод класса будет вызван AttributeError , если _stats не был определён в классе-наследнике.
Фактически это указывает на то, что _stats нужен, но его ещё нет, потому его нужно определить.

Не забывайте указывать это в документации либо описании класса.

class BaseWeapon: """ Дочерний класс обязан определить атрибут _stats """ class Stats: def __init__(self, power): self.power = power def __init__(self, owner, name): self._owner = owner self._name = name # self._stats = None - убрать @property def stats(self): return self._stats @stats.setter def stats(self, another_stats): if isinstance(another_stats, self.Stats): self._stats = another_stats else: raise TypeError(f'Value must be Stats class, not ') 

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *