Рубріки: ОсновыТеория

Как создавать классы в Python со знанием дела: разбираем на примерах

Семен Гринштейн

Чтобы создавать классы в Python, нужно использовать ключевое слово class. Ну и еще пара-тройка деталей: прописать инициализацию, создать свойства и методы. Python же простой язык. И, может быть, разглагольствовать на эту тему ни к чему? Для тех, кого не устраивает короткое и «простое» объяснение, мы написали эту статью. Если и после ее прочтения возникнут проблемы, то тогда лучше записаться на курсы к нашим партнерам Mate Academy и Powercode. После них у вас не останется вопросов.

Да, синтаксис изучить достаточно просто. Но сложность кроется в том мире, откуда пришло слово class. Это мир объектно-ориентированного программирования (ООП). Он не является очередной фантазией теоретиков, а наоборот, пытается отобразить реальный мир в виде объектов и связей между ними. 

Так что Python — это (n+1)-й язык, в котором была реализована концепция ООП. 

Что же такое ООП?

Мы уже упомянули слово class. Это формальная модель или шаблон, задающий структуру объекта. А что такое объект? Между классом и объектом такие же отношения, как между чертежом и изделием. Например, есть чертеж табуретки, по которому ее изготавливают. Но табуретки не существуют в живой природе. Приведем пример оттуда: допустим, мы решили смоделировать утку в компьютерной игре. Тогда получается, что объект уже есть, а класса нет. В этом случае нужно понаблюдать за уткой и создать модель, включив в нее нужные нам характеристики и ее поведение. 

В какой-то момент мы поймем, что утка чем-то похожа на других птиц: значит, ее можно отнести к категории (обобщенному, родительскому классу, шаблону) «птицы». С другой стороны, для нашей игры не важно, как устроен организм утки: главное, что она как-то выглядит и как-то крякает. То есть мы абстрагируемся от ненужных для нашей модели нюансов. А кто-то другой добавит эти нюансы в свою модель и реализует, например, утиный анатомический атлас. Но некто третий использует обе модели и на их базе создаст свою. Это в двух словах о том, каким образом формируются модели и каким образом объекты подразделяются на классы.

Утка и Птица — это классы, а утка1, утка2 и птица1, птица2 — объекты (экземпляры классов).

Концепция объектно-ориентированного программирования опирается на три известных утки кита. Давайте познакомимся с ними поближе.

Инкапсуляция

Этим словом обозначают сокрытие нюансов поведения объекта или его характеристик от «посторонних» программистов. Например, над компьютерной игрой работают два программиста. Один из них реализовал класс Утка, а другой просто хочет его использовать и у них случается такой диалог:

— Какой код написать, чтобы твоя утка полетела?

— Ты просто создай объект класса Утка: например, утка1. А потом вызови у этого объекта функцию утка1.летать() (функции, созданные внутри класса, правильнее называть «методами» — о них поговорим в соответствующем разделе — прим.).

И второму программисту, который использует этот класс Утка не нужно думать, как реализована функция летать(). Работает, и хорошо!

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

В ООП принято «экономить» код, время и силы, не делая лишних движений. Например, у нас уже реализован класс Птица. Если нам нужно реализовать класс Утка, мы можем максимально использовать код класса Птица и добавить еще (желательно не сильно много) нового кода. Этот процесс и называется наследованием. 

В примере выше Утка — наследник класса Птица. В то же время Птица — родительский класс по отношению к Утке. Вот так принято выражаться.

Полиморфизм

Сколько способов есть у утки, чтобы поздороваться с вами голосом? Наверное, она просто скажет что-то вроде «кря». Допустим, у нее есть один способ. Хорошо, сколько способов есть у лебедя? Они вроде как просто шипят. Допустим, тоже один. А если у птиц в целом? Ну не знаю… много. Однако все в целом по-прежнему будет называться словом «поздороваться»:

утка1.поздороваться()
лебедь1.поздороваться()

Действие называется одинаково, но выполнено оно будет по-разному — в зависимости от объекта, который это действие будет выполнять. У объекта утка1 класса Утка и у объекта лебедь1 класса Лебедь это произойдет по-разному. В буквальном переводе греческое слово polýmorphos означает «многообразный». Поняли, да?

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

Первый класс

class Bird:
    print("Я — класс Птица!")

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

Создаем объект (он же экземпляр класса) вот так — в одну строку:

b = Bird()
b2 = Bird()

Имя объекта (здесь создан объект по имени b) пишем с маленькой буквы. В этом случае внутри скобок нет параметров, но позже мы посмотрим, как и зачем их туда вписывать. Для второго объекта — аналогично. И вот такой результат работы этой программы мы получим:

Я — класс Птица!
Я — класс Птица!

Птица, ты кто?

Пока мы толком ничего не можем сказать о созданном объекте b. Все потому, что мы создали для него пустой класс, который просто занимается самолюбованием. Давайте это исправим. Добавим один атрибут name классу Bird.

Атрибуты — это набор данных, характеризующих объект или его состояние. 

Изменим структуру класса, добавив специальную инициализирующую функцию. В ней обычно атрибуты заполняются конкретными значениями, переданными в качестве параметров. В этом случае — параметр name. Тут, правда, есть еще один странный параметр — self. Это специальная переменная, содержащая ссылку на текущий экземпляр класса. Она помогает реализовать механизм ООП в Python.  Поговорим о ней в следующем разделе.

class Bird:
    def __init__(self, name):
        self.name = name

Теперь можно дать каждой птице (объекту класса Bird) имя:

b = Bird("Сережа")
print("Я птица. Меня зовут " + b.name);
b2 = Bird("Жанна")
print("Я птица. Меня зовут " + b2.name);

Запустив программу, получим:

Я птица. Меня зовут Сережа
Я птица. Меня зовут Жанна

Чтобы получить значение атрибута, мы обращаемся к нему через имя объекта с точкой (b.name).

Ключевое слово self

Ранее мы уже пытались говорить про ключевое слово self. Давайте разберемся с ним чуть лучше. Это специальная зарезервированная переменная, содержащая ссылку на текущий объект (экземпляр класса). Когда мы пишем код внутри класса, возникает естественное желание использовать его атрибуты и методы. За пределами класса мы делали это так:

 b.name
b2.name

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

 self.name

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

В этом случае self заменяет имя объекта b или b2 внутри класса Bird

Если внутри Bird использовать объект другого класса, слово self для него не применяется. Например, создадим класс Cird:

class Cird:
    def __init__(self):
        self.message = "Я объект c, класса Cird"

Создадим и используем объект класса Cird внутри класса Bird:

class Bird:
    def __init__(self, name):
        self.name = name
        c = Cird()
        print(c.message)

b = Bird("Я объект b, класса Bird")
print(b.name)

Получая значение атрибута (c.message), мы используем имя конкретного объекта (то есть с), а не слово self.

Результат выполнения кода:

Я объект c, класса Cird
Я объект b, класса Bird

Конструктор и инициализатор

Теперь стало немного понятнее, зачем нужен self в этом странном методе класса:

# для нашего класса Bird
def __init__(self, name):
self.name = name

Это один из его параметров. Он записывает значение второго параметра (name) в соответствующий атрибут текущего объекта. Там может быть и больше параметров. А может быть один только — self. Без него нельзя.

# для класса Cird
def __init__(self):
        self.message = "Я объект c, класса Cird"

Хорошо… но почему это так? И зачем вообще нужен этот метод? 

Это инициализатор. Обычно именно здесь в атрибуты объекта записываются значения. Это могут быть значения по умолчанию (как в классе Cird: self.message = "Я объект c, класса Cird") или значения, полученные с использованием параметров функции. 

А как же создается сам объект? 

В других языках программирования, например, существуют так называемые конструкторы. В Python тоже есть нечто похожее. Это специальный метод, который называется __new__. Только в Python его код мы обычно не видим и не пишем сами. Такой конструктор существует и работает «за кулисами». В качестве единственного параметра он принимает класс, анализирует его структуру (код) и на базе этой структуры создает пустой объект. Инициализировать его — не царское дело. Пусть этим занимается инициализатор, а не конструктор. Просьба не путать их друг с другом.

Важно, что оба эти метода вызываются автоматически, когда мы создаем объект — сначала __new__, потом __init__. Например для b = Bird(“Сережа”) последовательность вызовов будет выглядеть так:

1. __new__(Bird)
2. __init__(b, "Сережа")

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

Вот вы говорите конструктор, инициализатор… А что, если я объявлю и инициализирую атрибут вне метода __init__?

class Bird:
    
    ruClassName = "Птица"
    
    def __init__(self, name):
        self.name = name

И такое тоже практикуют. И такие атрибуты даже имеют свое название и применение:

b = Bird("Я объект b, класса " + Bird.ruClassName)
print(b.name)

Атрибут ruClassName называется статическим. А атрибут name — динамическим. Заметьте, что внутри класса к статическим атрибутам мы не обращаемся через self. Вне класса мы обращаемся к статическим атрибутам не через <имя объекта> с точкой, а через <Имя класса> с точкой. То же самое, кстати, требуется делать со статическим атрибутом и внутри методов класса! Иначе работать не будет.

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

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

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

class Bird:
    
    ruClassName = "Птица"
    objInstancesCount = 0
    
    def __init__(self, name):
        self.name = name
        Bird.objInstancesCount = Bird.objInstancesCount + 1 

b = Bird("объект №1 класса " + Bird.ruClassName)
print(b.name)
b2 = Bird("объект №2 класса " + Bird.ruClassName)
print(b2.name)

print("Количество объектов класса " + Bird.ruClassName + ": " + str(Bird.objInstancesCount))

Результат работы:

объект №1 класса Птица
объект №2 класса Птица

Количество объектов класса Птица: 2

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

Что, если нам дали задачу расширить пример из предыдущего раздела? 

1. Нужно добавить Птице больше атрибутов:

  • идентификационный номер (id);
  • возраст (age).

2. Реализовать возможность выводить атрибут имя (name), а также эти два атрибута для каждого объекта класса Птица.

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

class Bird:
    
    ruClassName = "Птица"
    objInstancesCount = 0
    
    def __init__(self, name, id, age):
        self.name = name
        self.id = id
        self.age = age
        Bird.objInstancesCount = Bird.objInstancesCount + 1 

Вроде бы и вывод тоже можно сделать аналогично:

b = Bird("объект №1 класса " + Bird.ruClassName, 11, 3)
print(b.name)
print("Идентификационный номер: " + str(b.id))
print("Возраст: " + str(b.age))

b2 = Bird("объект №2 класса " + Bird.ruClassName, 10, 4)
print(b2.name)
print("Идентификационный номер: " + str(b2.id))
print("Возраст: " + str(b2.age))

print("Количество объектов класса " + Bird.ruClassName + ": " + str(Bird.objInstancesCount))

Результат работы кода:

объект №1 класса Птица
Идентификационный номер: 11
Возраст: 3
объект №2 класса Птица
Идентификационный номер: 10
Возраст: 4

Количество объектов класса Птица: 2

Но наш тимлид считает, что такой код некрасивый и не очень читабельный. Да и ООП рекомендует помещать код обработки данных объекта внутрь его класса. Попробуем:

class Bird:
    
    ruClassName = "Птица"
    objInstancesCount = 0
    
    def __init__(self, name, id, age):
        self.name = name
        self.id = id
        self.age = age
        Bird.objInstancesCount = Bird.objInstancesCount + 1 
    
    def info(self):
        print(self.name)
        print("Идентификационный номер: " + str(self.id))
        print("Возраст: " + str(self.age))

Функция info() внутри класса Bird называется методом. Теперь у каждого созданного объекта этого класса можно вызвать метод:

b = Bird("объект №1 класса " + Bird.ruClassName, 11, 3)
b.info()
b2 = Bird("объект №2 класса " + Bird.ruClassName, 10, 4)    
b2.info()

print("Количество объектов класса " + Bird.ruClassName + ": " + str(Bird.objInstancesCount))

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

Результат работы такой же, как в предыдущем примере:

объект №1 класса Птица
Идентификационный номер: 11
Возраст: 3
объект №2 класса Птица
Идентификационный номер: 10
Возраст: 4

Количество объектов класса Птица: 2

Уровни доступа атрибута и метода

Реализация концепции ООП должна предусмотреть возможность запретить прямой доступ к атрибуту (или к методу) со стороны внешнего кода. То есть сделать его скрытым, приватным (термин из англоязычного ООП — private). Прямая модификация некоторых особо важных атрибутов может привести к дефектам в программе. Часто это нужно для того, чтобы оставлять доступ открытым («публичным», public) только к тем атрибутам и методам, которые будут взаимодействовать с внешним кодом. Остальные атрибуты и функции, которые, например, просто (или сложно) обслуживают свой класс, не должны быть доступны извне. 

В Python это реализовано с помощью добавления к имени атрибута или метода одинарного или двойного подчеркивания. Первое используется в расчете на более сознательных и внимательных граждан. Второе — более жесткий вариант для всех остальных. Что это означает? Перейдем к конкретике и возьмем из нашего примера атрибут name:

  1. Если мы заменим name на _name, он станет скрытым атрибутом. Но к такому атрибуту еще можно получить доступ как обычно, через <имя объекта>._name. Это делать не рекомендуется, но в исключительных случаях те самые сознательные и внимательные граждане умеют без печальных последствий обращаться к скрытым атрибутам таким образом. Ну а исключения подтверждают правила.
  2. Если мы заменим name на __name, он станет по-настоящему закрытым атрибутом. И к нему уже нельзя будет получить доступ обычным способом. Интерпретатор языка Python выдаст ошибку AttributeError и не выполнит этот код.


По заветам ООП, и в первом, и во втором случае нужно узнать, написаны ли специальные методы для получения значения (getter) и/или модификации (setter) интересующего вас скрытого атрибута. Если да, то используйте их.

Эти методы обычно выглядят примерно так:

# getter
def get_name(self):
        return self._name
# setter
def set_name(self, n):
        self._name = n

Перепишем весь класс:

class Bird:
    
    ruClassName = "Птица"
    objInstancesCount = 0
    
    def __init__(self, name, id, age):
        self._name = name
        self.id = id
        self.age = age
        Bird.objInstancesCount = Bird.objInstancesCount + 1 
    
    # getter
    def get_name(self):
            return self._name
    # setter
    def set_name(self, n):
            self._name = n
        
    def info(self):
        print(self._name)
        print("Идентификационный номер: " + str(self.id))
        print("Возраст: " + str(self.age))

Создадим объект и покажем правильный доступ к атрибуту _name (для __name все будет аналогично), который теперь обозначен как скрытый:

b = Bird("Сережа", 23, 2)
print("Это " + b.get_name())
b.set_name("Иван")
print("А, нет... Это " + b.get_name())

Результат работы программы:

Это Сережа
А, нет... Это Иван

Оказывается, мы можем переименовать созданный объект. Здорово. Но что, если нам запрещено называть птиц Иванами? 

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

def set_name(self, n):
            if(n != "Иван"):
                self._name = n

Тогда программа будет работать по-другому:

Это Сережа
А, нет... Это Сережа

Свойства

Есть способ упростить внешний код, который обращается вместо атрибута к его методам getter и setter. Начнем опять обращаться к атрибуту по его имени, только без подчеркивания. И пусть методы getter и setter вызываются автоматически, когда это необходимо. Правда, ради такого комфорта в код класса придется внести изменения. Для этого в Python существует специальная конструкция — декоратор @property:

# getter
    @property
    def name(self):
            return self._name

    # setter
    @name.setter
    def name(self, n):
            if(n != "Иван"):
                self._name = n

В остальном это обычные методы.

Код вызова извне тоже изменится:

b = Bird("Сережа", 23, 2)
print("Это " + b.name)
b.name = "Иван"
print("А, нет... Это " + b.name)

Результат работы программы останется прежним:

Это Сережа
А, нет... Это Сережа

Начать и закончить: наследование и полиморфизм

Про наследование и полиморфизм уже шла речь в начале статьи. Но тогда нельзя было приводить полноценные примеры на Python. Теперь, когда мы подтянули матчасть, написали и много раз переписали свой первый класс… как говорится, примеры в студию! Поэтому, если вам требуется время, чтобы вернуться в начало статьи и повторить эту тему — не волнуйтесь, я подожду. 

Повторили? Тогда полетели дальше. Если что-то все равно не понятно и остались вопросы – милости просим с вопросами к практикующему ментору.

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

Оттолкнемся от нашего класса Bird. Вспомним, как он выглядит:

class Bird:
    
    objInstancesCount = 0
    
    def __init__(self, name, id, age):
        self._name = name
        self._id = id
        self._age = age
        Bird.objInstancesCount = Bird.objInstancesCount + 1 
        
    # getter
    @property
    def name(self):
            return self._name
    # setter
    @name.setter
    def name(self, n):
            if(n != "Иван"):
                self._name = n
    
    # getter
    @property
    def age(self):
            return self._age
    # setter
    @age.setter
    def name(self, age):
            if(age >= 0):
                self._age = age
            else:
                self._age = 0
    
    # getter
    @property
    def id(self):
            return self._id
        
    def info(self):
        print("Имя: " + self._name)
        print("Идентификационный номер: " + str(self._id))
        print("Возраст: " + str(self._age))

Создадим новый класс Duck (утка), который максимально использует код класса Bird и допишем в него немного своего кода. То есть Duck будет наследником Bird, который является родителем, базовым классом. Это в принципе ожидаемо: чтобы некая сферическая птица в вакууме стала уткой, достаточно добавить пару опознавательных знаков (потому что для примера мы придумали простую модель). Итак, пусть у класса Duck будут такие динамические атрибуты:

  • скорость полета (fly_speed);
  • высота полета (fly_height).

А также статический атрибут «вид» (species). Он всегда имеет значение, равное «Утка»

Атрибуты «скорость полета» и «высота полета» бессмысленно помещать в базовый класс, потому что не все птицы умеют летать:

class Duck (Bird):
    
    species = "Утка"
    
    def __init__ (self, name, id, age, fly_speed, fly_height):
        super().__init__(name, id, age)
        self.__fly_speed = fly_speed
        self.__fly_height = fly_height
        
    @property
    def fly_speed(self):
        return self.__fly_speed
        
    @fly_speed.setter
    def fly_speed(self, fly_speed):
        self.__fly_speed = fly_speed
    
    @property
    def fly_height(self):
        return self.__fly_height
        
    @fly_height.setter
    def fly_height(self, fly_height):
        self.__fly_height = fly_height    
        
    def info(self):
        super().info()
        print("Вид: " + Duck.species)
        print("Скорость полета: " + str(self.__fly_speed))
        print("Высота полета: " + str(self.__fly_height))

В строке class Duck (Bird) мы в скобках указываем имя класса, от которого хотим делать наследование.

В методе def __init__ (self, name, id, age, fly_speed, fly_height) мы, помимо заполнения значений атрибутов текущего объекта класса Duck, вызываем инициализатор объекта базового класса (Bird):

 super().__init__(name, id, age)

Только заметьте, что слово Bird мы заменяем на специальное слово super(). Оно обозначает базовый класс. Если не углубляться в дебри, то просто примите, что super() используется для вызова инициализатора и методов родительского класса внутри класса-наследника.

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

d = Duck("duck1", 22, 2, 110, 5)
d.info()

Отметим, что внутри метода info() у утки (см. класс Duck) мы, по аналогии с инициализатором и чисто для полноты информации — вызываем свой метод info() у базового класса (то есть у Bird): 

super().info()

Ну вот, у нас получилась утка, которая может рассказать о себе и при этом помнит, что она Птица — не забывает свои корни. Жирным выделены атрибуты базового класса:

Имя: duck1

Идентификационный номер: 22

Возраст: 2

Вид: Утка

Скорость полета: 110

Высота полета: 5

Отлично, с уткой все получилось удачно. Но означает ли это, что можно создавать еще много аналогичных классов-наследников, используя один и тот же код базового класса? А еще интереснее узнать вот что:

  • можно ли в других наследниках создавать свой публичный метод с тем же названием info(), как у базового класса (Bird)?
  • если для метода info() уже написан код внутри базового класса, будет ли этот код замещен собственным новым кодом при вызове метода info() у каждого из наследников?

Давайте проверим.

Возьмем, например, галапагосского баклана, который не умеет летать. Этим он кардинально отличается от утки. Однако из-за этого он не перестает быть птицей. Галапагосский баклан — редкий вид, за ним ведется пристальное наблюдение. Поэтому в качестве его единственного динамического атрибута выберем количество особей (population). А также статический атрибут «вид» (species):

class GalaBacklane (Bird):
    
    species = "Галапагосский баклан"
    
    def __init__ (self, name, id, age, population):
        super().__init__(name, id, age)
        self.__population = population
        
    @property
    def population(self):
        return self.__population
        
    @population.setter
    def population(self, population):
        self.__population = population
    
    def info(self):
        super().info()
        print("Вид: " + GalaBacklane.species)
        print("Количество особей: " + str(self.__population))

Заметьте, что в строке def __init__ (self, name, id, age, population) присутствуют те же параметры id и age, которые были в инициализаторе внутри класса Duck. И затем также следует вызов:

super().__init__(name, id, age)

Создадим объект:

g = GalaBacklane("galaBacklane1", 22, 2, 60)
g.info()

Теперь проверим, как это сработает на этот раз. Жирным выделены атрибуты, относящиеся к базовому классу:

Имя: galaBacklane1

Идентификационный номер: 22

Возраст: 2

Вид: Галапагосский баклан

Количество особей: 60

Теперь сведем воедино работу объектов всех трех классов — родителя и наследников

g = GalaBacklane("galaBacklane1", 22, 2, 60)
g.info()


b = Bird("bird1", 9, 4)
b.info()


d = Duck("duck1", 77, 9, 110, 5)
d.info()

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

Эти объекты никак не связаны друг с другом. Связаны родственными отношениями только их классы.

Жирным выделены атрибуты, относящиеся к базовому классу:

Имя: galaBacklane1

Идентификационный номер: 22

Возраст: 2

Вид: Галапагосский баклан

Количество особей: 60

 

Имя: bird1

Идентификационный номер: 9

Возраст: 4

 

Имя: duck1

Идентификационный номер: 77

Возраст: 9

Вид: Утка

Скорость полета: 110

Высота полета: 5

Мы показали, что метод info() действительно работает по-разному — в зависимости от класса того объекта, который обращается к этому методу.

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

Надеюсь, тот, кто дочитал это до конца, получил ответ на свой вопрос!

Останні статті

Обучение Power BI – какие онлайн курсы аналитики выбрать

Сегодня мы поговорим о том, как выбрать лучшие курсы Power BI в Украине, особенно для…

13.01.2024

Work.ua назвал самые конкурентные вакансии в IТ за 2023 год

В 2023 году во всех крупнейших регионах конкуренция за вакансию выросла на 5–12%. Не исключением…

08.12.2023

Украинская IT-рекрутерка создала бесплатный трекер поиска работы

Unicorn Hunter/Talent Manager Лина Калиш создала бесплатный трекер поиска работы в Notion, систематизирующий все этапы…

07.12.2023

Mate academy отправит работников в 10-дневный оплачиваемый отпуск

Edtech-стартап Mate academy принял решение отправить своих работников в десятидневный отпуск – с 25 декабря…

07.12.2023

Переписки, фото, история браузера: киевский программист зарабатывал на шпионаже

Служба безопасности Украины задержала в Киеве 46-летнего программиста, который за деньги устанавливал шпионские программы и…

07.12.2023

Как вырасти до сеньйора? Девелопер создал популярную подборку на Github

IT-специалист Джордан Катлер создал и выложил на Github подборку разнообразных ресурсов, которые помогут достичь уровня…

07.12.2023