Рассмотрим объект «Программист», который задаётся именем, должностью и количеством отработанных часов. Каждая должность имеет собственный оклад (заработную плату за час работы). В нашей импровизированной компании существуют 3 должности:
- Junior — с окладом 10 тугриков в час;
- Middle — с окладом 15 тугриков в час;
- Senior — с окладом 20 тугриков в час по умолчанию и +1 тугрик за каждое новое повышение.
Напишите класс Programmer
, который инициализируется именем и должностью (отработка у нового работника равна нулю). Класс реализует следующие методы:
work(time)
— отмечает новую отработку в количестве часовtime
;rise()
— повышает программиста;info()
— возвращает строку для бухгалтерии в формате: <имя> <количество отработанных часов>ч. <накопленная зарплата>тгр.
Примечание
Ваше решение должно содержать только классы и функции.
В решении не должно быть вызовов инициализации требуемых классов.
Пример
Ввод
programmer = Programmer('Васильев Иван', 'Junior')
programmer.work(750)
print(programmer.info())
programmer.rise()
programmer.work(500)
print(programmer.info())
programmer.rise()
programmer.work(250)
print(programmer.info())
programmer.rise()
programmer.work(250)
print(programmer.info())
Вывод
Васильев Иван 750ч. 7500тгр.
Васильев Иван 1250ч. 15000тгр.
Васильев Иван 1500ч. 20000тгр.
Васильев Иван 1750ч. 25250тгр.
Решение
Очень хорошая задача на проектирование и управление достаточно сложными для начинающих данными.
Мы начнем с примитивного решения, которое доведем до ума в несколько простых шагов.
Для начала определимся c набором атрибутов нашего класса. Из условия задачи следует что нам необходимо хранить имя (name), ставка оплаты (wage), отработанное время (work_time) и суммарный заработок (salary).
Все эти переменные должны быть привязаны к классу и поэтому их имя будет начинаться в self. Есть еще словарь сопоставления позиции и ставки. Его можно так же привязать в качестве атрибута, а можно оставить пока простой переменной, так как пока он нам потребуется только в процессе инициализации объекта.
Словарь заполняем согласно условиям, имя берем из входных данных, ставку берем из словаря сопоставляя ее с позицией на которую принимаем программиста. Переменные отработанного времени и выплат инициализируем нулем.
Теперь нужно продумать три метода – work() который занимается учетом времени и вычислением выплат, метод rise() который отвечает за логику повышения и установки надбавки и метод info() который выводит информацию о программисте.
Вычисление отработаного времени задача элементарная, просто прибавляем входную переменную время к отработанному времени. Так же поступаем и с зарплатой – увеличиваем ее на произведение ставки на дополнительное время. Таким образом work() содержит всего две операции.
С методом rise() чуть сложнее, но сейчас мы пойдем самым простым путем. Легко можно заметить, что ставки отличаются на 5 тугриков, а при достижении значения в 20 тугриков каждое повышение приносит один дополнительный тугрик. Таким образом нам можно обойтись проверкой на меньше ли ставка 20 тугриков и если да, то поднять ее на 5, а если нет, то всего на 1 тугрик.
Метод info() содержит всего одну операцию – формирование строки из имеющихся у нас параметров по шаблону из задания.
Код реализующий эту логику представлен в решении номер 1. Этого решения достаточно, чтобы пройти все тесты, но это решение имеет ряд недостатков о которых мы поговорим далее.
Одним из главных недостаков кода является то, что наша программа рассчитана только на одинаковое повышение ставок от должности к должности, что не позволяет нам реализовать схему когда джуниор, миддл и сеньор имеют ставки 10, 14 и 20 тугриков соответсвенно. Давайте устраним этот недостаток, а заодно избавимся от словаря внутри функции инициализации, так как в этом случае она будет создаваться заново для каждого программиста в штате, и если их будет 1000, то мы будем иметь 1000 табличек. А если она будет атрибутом, а не временной переменной, то мы будем постоянно хранить эти 1000 табличек в памяти. Это ведет к еще одному неудобству – если мы решим сменить ставки во время работы программы придется делать это для каждого программиста отдельно. Гораздо удобнее иметь глобальную в рамках класса переменную и тогда изменение таблички немедленно отразиться на всех без исключения программистах.
Делается это переносом переменной из функции __init__() в зону сразу за объявлением класса. Пусть вас не смущает отсутствие префикса self – глобальные данные в пределах класса автоматически получают атрибут, дающий возможность использовать эти переменные в любом экземпляре класса.
Вторым отличием нашего класса будет более продвинутая система повышения (rise()). Она будет проверять на какой позиции работает программист и присваивать ему следующую. Но если он достиг потолка, то будет просто начислять ему бонус, который будет добавляться к окладу. Для этого нам стоит предусмотреть соотвествующий атрибут. Еще одно изменение в данных – отказ от ставки в пользу словаря, возвращающего ставку по позиции программиста.
В остальном программа осталась без изменений.
Код реализующий эту логику представлен в решении номер 2. Это решение будет примерно соотвествовать тому, что от вас могли бы ожидать при проверке решения задания.
У этого решения несмотря на большую гибкость все еще есть недостатки.
Например, нам придется вручную править сетку ставок в дух местах – словаре и в методе rise().
Давайте попробуем избавиться от этого недостатка. Решение предоставленное ниже не выходит за рамки изученного материала, но тем не менее, достаточно сложно для начинающих, потому что использует приемы, которые ранее не были показаны в теоретическом материале и не использовались в решениях.
Итак давайте подумаем как было бы удобнее всего оформить логику повышений и назначения ставок. Очевидно, что ставки все еще удобно хранить в словаре, но было бы здорово, если бы нам не надо было заботиться о том, какую позицию займет наш программист после повышения. Очевидно что для этого нам надо иметь список позиций отсортированный по размеру ставки. Это решение дает нам еще одно преимущество – мы можем добавлять ставки в словарь в любом порядке, сортировка расставит все по своим местам.
Итак вводим дополнительный список, который содержит “табель о рангах” от низшей позиции к высшей. Таким образом, повышение возможно до тех пор, пока текущая позиция не равна последнему элементу этого списка.
Реализация этой логики программы представлена в решении 3.
Посмотреть код
Решение
# simple
class Programmer:
def __init__(self, name, position) -> None:
rank = {
'Junior': 10,
'Middle': 15,
'Senior': 20
}
self.name = name
self.wage = rank[position]
self.work_time = 0
self.salary = 0
def work(self, time):
self.work_time += time
self.salary += self.wage * time
def info(self):
return f'{self.name} {self.work_time}ч. {self.salary}тгр.'
def rise(self):
if self.wage < 20:
self.wage += 5
else:
self.wage += 1
Решение
class Programmer:
__rank = {
'Junior': 10,
'Middle': 15,
'Senior': 20,
}
def __init__(self, name, position):
self.name = name
self.position = position
self.bonus = 0
self.work_time = 0
self.salary = 0
def work(self, time):
self.work_time += time
self.salary += (self.__rank[self.position] + self.bonus) * time
def info(self):
return f'{self.name} {self.work_time}ч. {self.salary}тгр.'
def rise(self):
match self.position:
case 'Junior':
self.position = 'Middle'
case 'Middle':
self.position = 'Senior'
case 'Senior':
self.bonus += 1
Решение
class Programmer:
__wage = {
'Junior': 10,
'Middle': 15,
'Senior': 20,
}
__ranks = list(dict(sorted(__wage.items(), key=lambda item: item[1])).keys()) # noqa
def __init__(self, name, position) -> None:
self.__name = name
self.__position = position
self.__bonus = 0
self.__work_time = 0
self.__salary = 0
def work(self, time):
self.__work_time += time
self.__salary += (self.__wage[self.__position] + self.__bonus) * time
def info(self):
return f'{self.__name} {self.__work_time}ч. {self.__salary}тгр.'
def rise(self):
if self.__position != self.__ranks[-1]:
index = self.__ranks.index(self.__position)
self.__position = self.__ranks[index + 1]
else:
self.__bonus += 1
скажите, пожалуйста,
index = list(self.__ranks).index(self.__position)
можно использовать без list()??
index = self.__ranks.index(self.__position)
Да, конечно. Два лишних преобразования в решении. Вероятнее всего,
был изначально словарем.
Потом я решил преобразовать ее в список наверху, и забыл избавиться от преобразования внизу,
Исправил. Спасибо за внимательность.
Вам большое спасибо за знания))
__ranks = list(dict(sorted(__wage.items(), key=lambda item: item[1])).keys()) # noqa
По другому получается никак не получить список ключей?
либо циклом записаться в список
Тут нам надо получить не список ключей, а список ключей отсортированых по хранящемся в словаре значениям.
Способов, как обычно, больше чем один. Выбор конкретного зависит от предпочтений.
Здравствуйте, Сергей.
Подскажите, зачем __rank во 2-ом решении и self.__name = name в 3-ем. В материалах этого нет. Где поискать теорию?
Это объясняется в тексте, на этой странице. Грубо говоря, это глобальная для всех экземпляров класса переменная. Одна на все экземпляры. Иногда бывает очень удобно, но как и любые глобальные переменные требует особой внимательности при использовании.
Хотя вот подумал, что возможно вопрос касается двух подчеркиваний перед именем переменной.
В python есть некое соглашение, которое говорит, что существует два типа имен к которым не стоит обращаться напрямую – они начинаются с одного знака подчеркивания и с двух.
Один знак означает приватную сущность. К ней можно обратиться напрямую, но не стоит. Лучше воспользоваться методами, которые обеспечивают взаимодействие с этим объектом.
Два знака подчеркивания приводят к тому, что в коде становится невозможно обратиться к объекту, потому что ее имя преобразуется в имякласса__имяобъекта. Тем не менее вы все еще можете получить к ним доступ и изменить.
И все же с объектами начинающимися со знаков подчеркивания лучше напрямую не работать, а поискать в классе соответствующие методы для работы с ними.
Сергей, здравствуйте.
Благодарю за оперативный ответ.
Это как private, protected в C#? Только в виде соглашения, а не прямого запрета. Просто переменная будет теперь _Programmer__name?
Если все переменные без двойного подчеркивания, код тоже проходит проверки! Т.е., они поставлены, чтобы напрямую не менять переменные, только через методы?
Просто, __ появилось в переменных в __init__ только в 3-ем решении, почему этого не было в предыдущих, я так и не понял. Это просто правильнее? Про __rank во 2-ом решении объяснение дается: “Гораздо удобнее иметь глобальную в рамках класса переменную и тогда изменение таблички немедленно отразиться на всех без исключения программистах”.
Цель третьего решения – показать вариант, чуть выходящий за рамки выданной теории и познакомить с новыми сущностями и подходами о которых не упомянули в основном материале..
В нем я попробовал показать максимально правильное с точки зрения философии python и ООП решение.
В python ни в каком виде нет констант, но есть определенные соглашения. Это вполне укладывается в философию языка, и роль констант выполняют соглашения.
“Закрытие” переменных класса, которое появилось в третьем решении уходит корнями в одно из базовых концепций ООП – все взаимодействие со значениями в классе должно осуществляться только с помощью специальных “сеттеров” – методов, которые устанавливают эти значения.Смысл в том, что при установке значения сеттер может совершать дополнительные, скрытые от программиста действия, необходимые для правильного выполнения программы. При прямом присвоении эти действия, как вы понимаете не выполнятся и можно получить неконсистентные данные.
Грубо говоря, в ООП programmer.set_salaty(salary) правильно, а programmer.salary = salary – не очень. Использование двух подчерков помогает программисту усложнить изменения переменных внутри класса напрямую, у него все еще остается такая возможность, но по сути про нее знают только те люди, которые четко понимают что и зачем они делают. Да и синтаксис получается не очень удобный, поэтому чаще проще делать так как надо, а не так, как хочется.
Как любая концепция эта практика не без греха и иногда программисты во имя увеличения производительности нарушают это правило. Тем не менее эта практика относится к одной из самых правильных в ООП – закрывай все, что не предполагается изменять извне в обход сеттеров.
Сергей, большое спасибо за обстоятельный ответ. Да, я тоже нашел информацию про сеттеры и геттеры.
Жаль, что зная о вашем ресурсе, проверял до раздела 5.1. ООП по ресурсу https://github.com/Pavellver/Yandex_handbook_answers/
Там только ответы, никаких пояснений нет. Полистаю предыдущие темы, сразу наткнулся на “читерский” способ спрямления вложенных списков.
Ряд задач пропустил, т.к. сам не решил и решение там не понял.
Спасибо.