Основные вопросы на собеседование Python Junior Developer
- О языке Python
- Типы данных и работа с ними
- ООП основные принципы: абстракция, инкапсуляция, наследование и полиморфизм.
- Алгоритмы (Big 0, список, сортировки)
- Парадигмы программирования
- Принципы написания кода: DRY, KISS, SoC, SOLID
- Декораторы, менеджеры контекста, итераторы и генераторы
В целом Python можно описать одним предложением:
Высокоуровневый, интерпретируемый, объектно-ориентированный, императивный, строго типизированный язык общего назначения, который имеет динамическую типизацию.
Высокоуровневый
Языки программирования делятся на высокоуровневые и низкоуровневые. Низкоуровневые языки — языки, близкие к машинному коду или его конструкциям (например, байт-кодам). Классикой таких языков являются C, Assembler, Forth.
Высокоуровневые языки — соответственно разрабатываются для удобства использования и скорости написания программы. В них применяются определённые абстракции — структуры данных, набор вспомогательных функций и так далее. Это такие языки как Python, JS, PHP, Go,
Интерпретируемый
Языки делятся на интерпретируемые(Python, JS, PHP, R, Ruby) и компилируемые (С, С++, Pascal). В первом случае программа выполняется специальной программой — интерпретатором, во втором программа сначала преобразуется в понятные компьютеру исполняемые файлы.
Объектно-ориентированный
Все языки также разделяются на процедурные, функциональные и объектно-ориентированные в зависимости от того, с помощью каких конструкций создаётся программа и как происходит её выполнение.
В объектно-ориентированных языках основа это классы и экземпляры классов это равносильно типу и объекту этого типа. Выполнение условных задач или же просто работа программы строится на взаимодействии различных классов.
Python хоть и является объектно-ориентированным языком но также поддерживает и процедурное программирование это значит, что программу можно написать без единого класса.
В основе функциональных языков лежит отличная от предыдущих вычислительная система, называемая лямбда-исчисление, которая, тем не менее, эквивалентна машине Тьюринга, о чём доказана соответствующая теорема (спасибо моему преподавателю за этот абзац).
Императивный
Языки программирования также могут быть разделены на импертивные и декларативные. В императивном языке программист будет указывать последовательность команд для выполнения (это все языки программирования которые мы считаем «языками программирования» простите за тавтологию).
Декларативные же языки в свою очередь ожидают от нас описания результата, который мы хотим получить в ходе выполнения запроса. Яркий пример декларативного языка это SQL (Stucted Query Language) или же структурированный язык запросов. Именно в нём мы описываем конкретный результат выполнения программы а не последовательность команд. Декларативными языками еще называют HTML, CSS, SVG, VRML, SQL, lex/VACC.
Строго (сильно) типизированный
Что касается типизации. В сильно типизированном языке интерпретатор, при выполнении команд, не станет неявно приводить типы в отличии от слабо типизированных языков, в котором приведения типа могут происходить неявно.
Динамическая типизация
Динамическая типизация предполагает, что в процессе выполнения команды переменная может содержать объекты различных типов. То есть мы объявляем переменную не указывая явно, какой тип данных в ней будет содержаться, и в процессе выполнения программы в одной переменной может побывать как текст так и число, а может и булево значение.
Статическая типизация предполагает, что при задании переменной сразу указывается тип данных, который она может содержать.
Типы данных
Разница между атомарными и структурными типами данных:
Вкратце: Атомарные объекты, при их присваивании, передаются по значению, а ссылочные — по ссылке.
# пример присваивания атомарного объекта atom = 3 btom = atom atom = 2 print(atom) > 2 print(btom) > 3
Из результатов видно, что переменной btom было присвоено именно значение, содержащееся в atom, а не ссылка, указывающая на область памяти.
Числовые типы
- int (целое число)
- float (число с плавающей точкой)
- bool (логический тип данных)
- complex (комплексное число)
В Python комплексные числа задаются с помощью функции complex():
# пример комплексного числа z = complex(1, 2) print(z) > (1+2j) # вещественная часть print(z.real) > 1.0 # мнимая часть print(z.imag) > 2.0 # сопряженное комплексное число print(z.conjugate()) > (1-2j)
Последовательности
Списки в Python - упорядоченные изменяемые коллекции объектов произвольных типов
Кортеж (от англ. tuple) – это неизменяемая упорядоченная коллекция объектов произвольного типа
- Неизменяемость — именно это свойство кортежей, порой, может выгодно отличать их от списков.
- Скорость — кортежи быстрее работают. По причине неизменяемости кортежи хранятся в памяти особым образом, поэтому операции с их элементами выполняются заведомо быстрее, чем с компонентами списка.
- Безопасность — неизменяемость также позволяет им быть идеальными кандидатами на роль констант. Константы, заданные кортежами, позволяют сделать код более читаемым и безопасным.
- Использование tuple в других структурах данных — кортежи применимы в отдельных структурах данных, от которых python требует неизменяемых значений. Например ключи словарей (dicts) должны состоять исключительно из данных immutable-типа.
Словари в Python - неупорядоченные коллекции произвольных объектов с доступом по ключу.
Множества в Python – это структура данных, которые содержат неупорядоченные элементы. Элементы также не является индексированным. Как и list, множество позволяет внесение и удаление элементов. Однако, есть ряд особенных характеристик, которые определяют и отделяют множество от других структур данных:
- Множество не содержит дубликаты элементов;
- Элементы множества являются неизменными (их нельзя менять), однако само по себе множество является изменяемым, и его можно менять;
- Так как элементы не индексируются, множества не поддерживают никаких операций среза и индексирования.
Файл
Чтобы начать работу с файлами, нужно вызвать функцию open() и передать ей в качестве аргументов имя файла из внешнего источника и строку, описывающую режим работы функции:
f = open('filename.txt', 'w')Операции с файлами могут быть разными, а, следовательно, разными могут быть и режимы работы с ними:
- r — выбирается по умолчанию, означает открытие файла для чтения;
w — файл открывается для записи (если не существует, то создаётся новый);
x — файл открывается для записи (если не существует, то генерируется исключение);
a — режим записи, при котором информация добавляется в конец файла, а не затирает уже имеющуюся;
b — открытие файла в двоичном режиме;
t — ещё одно значение по умолчанию, означающее открытие файла в текстовом режиме;
+ — читаем и записываем.
range object (a type of iterable)
В Python есть встроенная функции range(), которая способна генерировать непрерывную последовательность целых чисел
None
None — специальный объект внутри Питона. Он означает пустоту, всегда считается "Ложью" и может рассматриваться в качестве аналога
NULL для языка C/С++. Помимо этого, None возвращается функциями, как объект по умолчанию.
ООП в Python
Язык программирования Python является объектно-ориентированным. Это означает, что каждая сущность (переменная, функция и так далее) в этом языке является объектом определенного класса.
ООП строится вокруг четырёх основных принципов: абстракция, инкапсуляция, наследование и полиморфизм.
Всё объектно-ориентированное программирование строится на четырёх понятиях: инкапсуляции, наследовании, полиморфизме и абстракциях. Поэтому давайте объявим наш класс «Кошка» и будем объяснять ООП на нём:
class Cat():
def __init__(self, breed, color, age):
self.breed = breed
self.color = color
self.age = age
def meow(self):
print('Мяу!')
def purr(self):
print('Мрррр')Метод __init__ — инициализатор класса. Он вызывается сразу после создания объекта, чтобы присваивать значения динамическим атрибутам. self — ссылка на текущий объект, она даёт доступ к атрибутам и методам, с которыми вы работаете. Её аналог в других языках программирования — this.
Примечание 1. Слово self общепринятое, но не обязательное, вместо него можно использовать любое другое. Однако это может запутать тех, кто будет читать ваш код.
Примечание 2. Названия классов принято писать с прописной буквы, а объектов — со строчной.
Итак, мы создали класс Cat, в котором объявили три атрибута: порода — breed, цвет — color и возраст — age. А ещё добавили два метода, чтобы наша кошка умела мяукать — meow() и мурчать — purr().
Давайте создадим пару объектов нашего класса:
cat1 = Cat('Абиссинская', 'Рыжая', 4)
cat2 = Cat('Британская', 'Серая', 2)Отлично, теперь, когда у нас есть основа, приступим к изучению принципов ООП.
Инкапсуляция
Доступ к данным объекта должен контролироваться, чтобы пользователь не мог изменить их в произвольном порядке и что-то поломать. Поэтому для работы с данными программисты пишут методы, которые можно будет использовать вне класса и которые ничего не сломают внутри.
Вернёмся к нашим кошечкам. Мы можем разрешить изменять атрибут «возраст», но только в большую сторону, а атрибуты «порода» и «цвет» лучше открыть только для чтения — ведь порода кошки не меняется, а цвет если и меняется, то не по её инициативе.
В нашем классе «Кошка» мы сделали все атрибуты открытыми, поэтому давайте это исправим:
class Cat():
def __init__(self, breed, color, age):
self._breed = breed
self._color = color
self._age = age
@property
def breed(self):
return self._breed
@property
def color(self):
return self._color
@property
def age(self):
return self._age
@age.setter
def age(self, new_age):
if new_age > self._age:
self._age = new_age
return self._ageКод стал выглядеть немного сложнее, но мы сейчас всё объясним. Сначала мы сделали все атрибуты закрытыми с помощью символа _. Он говорит интерпретатору, что эта переменная будет доступна только внутри методов класса.
Нам всё ещё нужно получать доступ к атрибутам, поэтому мы предоставляем его через @property и объявляем для каждого атрибута свой метод — breed, color, age. В каждом из этих методов мы возвращаем значение нашего закрытого атрибута. Это делает его доступным только для чтения.
И последнее — мы должны позволить пользователям увеличивать возраст кота. Для этого воспользуемся @age.setter и ещё раз объявим метод age, а внутри него напишем простое условие и вернём значение атрибута.
Теперь создадим экземпляр класса:
cat = Cat('Абиссинская', 'Рыжая', 4)print(cat.breed) # Абиссинская print(cat.color) # Рыжая print(cat.age) # 4
И попробуем изменить атрибут age:
cat.age = 5 print(cat.age) # 5
Всё успешно. А теперь сделаем это с другим атрибутом:
cat.breed = 'Сиамская' print(cat.breed) # AttributeError: can't set attribute on line 34 in main.py
Мы получили ошибку, потому что запретили изменять этот атрибут.
Наследование
Классы могут передавать свои атрибуты и методы классам-потомкам. Например, мы хотим создать новый класс «Домашняя кошка». Он практически идентичен классу «Кошка», но у него появляются новые атрибуты «хозяин» и «кличка», а также метод «клянчить вкусняшку».
Достаточно объявить «Домашнюю кошку» наследником «Кошки» и прописать новые атрибуты и методы — вся остальная функциональность перейдёт от родителя к потомку.
class HomeCat(Cat):
def __init__(self, breed, color, age, owner, name):
super().__init__(breed, color, age)
self._owner = owner
self._name = name
@property
def owner(self):
return self._owner
@property
def name(self):
return self._name
def getTreat(self):
print('Мяу-мяу')В первой строке мы как раз наследуем все методы и атрибуты класса Cat. А чтобы всё создалось корректно, мы должны вызвать метод super() в методе __init__() и через него заполнить атрибуты класса-родителя. Поэтому мы и передаём в этот метод «породу», «окрас» и «возраст».
Кроме атрибутов для класса-родителя у класса-потомка есть и собственные атрибуты: «хозяин» — owner и «кличка» — name. Их мы будем использовать только в этом классе, поэтому они будут недоступны для класса-родителя.
Мы сразу сделали атрибуты класса-потомка закрытыми и объявили для них собственные методы. А также добавили метод getTreat(), которого нет в классе-родителе.
Давайте создадим объект класса:
my_cat = HomeCat('Сиамская', 'Белая', 3, 'Иван', 'Роза')
print(my_cat.owner)
print(my_cat.breed)
my_cat.getTreat() # Мяу-мяу
my_cat.purr() # МррррКак видим, у нас работают и новые методы, и старые. Наследование прошло успешно.
Полиморфизм
Этот принцип позволяет применять одни и те же команды к объектам разных классов, даже если они выполняются по-разному. Например, помимо класса «Кошка», у нас есть никак не связанный с ним класс «Попугай» — и у обоих есть метод «спать». Несмотря на то что кошки и попугаи спят по-разному (кошка сворачивается клубком, а попугай сидит на жёрдочке), для этих действий можно использовать одну команду.
Допустим у нас есть два класса — «Кошка» и «Попугай»:
class Cat:
def sleep(self):
print('Свернулся в клубок и сладко спит.')
class Parrot:
def sleep(self):
print('Сел на жёрдочку и уснул.')А теперь пусть у нас есть метод, который ожидает, что ему на вход придёт объект, у которого будет метод sleep:
def homeSleep(animal): animal.sleep()
Посмотрим, как это будет работать:
cat = Cat() parrot = Parrot() homeSleep(cat) # Свернулся в клубок и сладко спит. homeSleep(parrot) # Сел на жёрдочку и уснул.
Хотя классы разные, их одноимённые методы работают похожим образом. Это и есть полиморфизм.
Абстракция
При создании класса мы упрощаем его до тех атрибутов и методов, которые нужны именно в этом коде, не пытаясь описать его целиком и отбрасывая всё второстепенное. Например, у всех хищников есть метод «охотиться», поэтому все животные, которые являются хищниками, автоматически будут уметь охотиться.
class Predator:
def hunt(self):
print('Охотится...')Этот класс будет общим для всех животных, которые являются хищниками, — например, кошек:
class Cat(Predator):
def __init__(self, name, color):
super().__init__()
self._name = name
self._color = color
@property
def name(self):
return self._name
@property
def color(self):
return self._colorУ кошки есть свои атрибуты: «имя» — name и «окрас» — color. Но при этом она потомок хищников, а значит, умеет охотиться:
cat = Cat('Даниэла', 'Чёрный')
cat.hunt() # Охотится…Уровни доступа в Python
Чтобы регулировать вмешательство во внутреннюю работу объекта, в ООП есть несколько уровней доступа: публичный (public), защищённый (protected) и приватный (private). Защищённые атрибуты и методы можно вызывать только внутри класса и его классов-наследников. Приватные — только внутри класса: даже наследники не имеют доступа к ним.
В Python это реализовано следующим образом: перед защищёнными атрибутами и методами пишут одинарное нижнее подчёркивание (_example), перед приватными — двойное (__example). Именно это мы сделали в методе _is_enough. Одинарным нижним подчёркиванием мы объявили его защищённым.
При этом в Python само по себе объявление атрибутов и методов защищёнными и приватными не ограничивает доступ к ним извне.
Мы всё ещё можем вызвать метод _is_enough из любого места программы:
# Вызываем метод _is_enough и спрашиваем его, # осталось ли хотя бы 10 мл напитка. coffee._is_enough(10) >>> True
Атрибуты и методы, объявленные приватными, вызвать напрямую уже нельзя, но есть обходной путь:
# Создаём класс Drink с приватным атрибутом __volume.
class Drink:
__volume = 200
# Создаём экземпляр класса Drink.
coffee = Drink()
# Используем обходной путь, чтобы обратиться к приватному атрибуту.
coffee._Drink__volume
>>> 200Примечание. Возможность игнорировать уровни доступа — нарушение важного для ООП принципа инкапсуляции. Поэтому, несмотря на наличие технической возможности, программисты, пишущие на Python, договорились не обращаться к защищённым и приватным методам откуда-то извне.
Алгоритмы (Big 0, список, сортировки)
https://skillbox.ru/media/code/big-o-notation-chto-eto-takoe-i-kak-eye-poschitat/
Парадигмы программирования
Парадигма программирования – это совокупность понятий, идей и правил, которые определяют подход к написанию кода. Она нужна для того, чтобы сделать текст программы структурированным и понятным как для других разработчиков, так и для компьютера. Пример: функциональный подход основан на использовании чистых функций, тогда как объектно-ориентированный базируется на объектах, классах и их взаимодействии друг с другом.
Существует три основных парадигмы: структурное, объектно-ориентированное и функциональное. Интересно, что сначала было открыто функциональное, потом объектно-ориентированное, и только потом структурное программирование, но применяться повсеместно на практике они стали в обратном порядке.
Принципы написания кода: DRY, KISS, SoC, SOLID
DRY (Don't repeat yourself )
Каждая часть знаний должна иметь единственное, недвусмысленное, авторитетное представление в системе.
Это один из самых простых принципов кодирования. Его единственное правило заключается в том, что код не должен дублироваться. Вместо дублирования строк найдите алгоритм, использующий итерацию. Код DRY легко поддерживать. Вы можете еще больше развить этот принцип с помощью абстракции модель/данные.
Минусы принципа DRY заключаются в том, что вы можете получить слишком много абстракций, создания внешних зависимостей и сложного кода. DRY также может вызвать осложнения, если вы попытаетесь изменить большую часть своей кодовой базы. Вот почему вам следует избегать DRYing вашего кода слишком рано. Всегда лучше иметь несколько повторяющихся участков кода, чем неправильные абстракции.
KISS (Будь проще, глупый)
Большинство систем работают лучше всего, если они остаются простыми, а не усложняются.
Принцип KISS гласит, что большинство систем работают лучше всего, если их упрощают, а не усложняют. Простота должна быть ключевой целью в дизайне, и следует избегать ненужной сложности.
SoC (разделение задач)
SoC — это принцип проектирования для разделения компьютерной программы на отдельные разделы, так что каждый раздел касается отдельной задачи. Проблема представляет собой набор информации, которая влияет на код компьютерной программы.
Хорошим примером SoC является MVC (модель — представление — контроллер).
Если вы решите использовать этот подход, будьте осторожны, чтобы не разбить приложение на слишком много модулей. Вы должны создавать новый модуль только тогда, когда это имеет смысл. Больше модулей — больше проблем.
SOLID
SOLID — это мнемоническая аббревиатура пяти принципов проектирования, предназначенных для того, чтобы сделать дизайн программного обеспечения более понятным, гибким и удобным в сопровождении.
SOLID чрезвычайно полезен при написании кода ООП. В нем говорится о разделении вашего класса на несколько подклассов, наследовании, абстракции, интерфейсах и многом другом.
Он состоит из следующих пяти понятий:
- Принцип единой ответственности : «У класса должна быть одна и только одна причина для изменения».
- Принцип « открытое -закрытое »: «Сущности должны быть открыты для расширения, но закрыты для модификации».
- Принцип подстановки Лескова : «Функции, использующие указатели или ссылки на базовые классы, должны иметь возможность использовать объекты производных классов, не зная об этом».
- Принцип разделения интерфейсов : «Клиента не следует заставлять реализовывать интерфейс, который он не использует».
- Принцип инверсии зависимостей : «Зависит от абстракций, а не от конкретики».
Декораторы, менеджеры контекста, итераторы и генераторы
Декораторы
Декораторы — чрезвычайно мощный инструмент в Python, который позволяет нам добавлять к функции некоторые пользовательские функции. По своей сути, это просто функции, вызываемые внутри функций. Используя их, мы используем принцип SoC (разделение задач) и делаем наш код более модульным. Изучите их, и вы будете на пути к коду Pythonic!
Допустим, у нас есть сервер, который защищен паролем. Мы могли бы либо запрашивать пароль в каждом методе сервера, либо создать декоратор и защитить наши методы сервера следующим образом:
def ask_for_passcode(func):
def inner():
print('What is the passcode?')
passcode = input()
if passcode != '1234':
print('Wrong passcode.')
else:
print('Access granted.')
func()
return inner
@ask_for_passcode
def start():
print("Server has been started.")
@ask_for_passcode
def end():
print("Server has been stopped.")
start() # decorator will ask for password
end() # decorator will ask for password
Наш сервер теперь будет запрашивать пароль каждый раз start()или при end()вызове.
Менеджеры контекста
Контекстные менеджеры упрощают наше взаимодействие с внешними ресурсами, такими как файлы и базы данных. Чаще всего используется withзаявление. Их преимущество в том, что они автоматически освобождают память за пределами своего блока.
with open('wisdom.txt', 'w') as opened_file:
opened_file.write('Python is cool.')
# opened_file has been closed.
Без менеджера контекста наш код выглядел бы так:
file = open('wisdom.txt', 'w')
try:
file.write('Python is cool.')
finally:
file.close()
Итераторы
Итератор — это объект, который содержит счетное количество значений. Итераторы позволяют повторять объект, что означает, что вы можете пройти через все значения.
Допустим, у нас есть список имен, и мы хотим просмотреть его. Мы можем пройти через него, используя next(names):
names = ["Mike", "John", "Steve"]
names_iterator = iter(names)
for i in range(len(names)):
print(next(names_iterator))
Или используйте расширенный цикл:
names = ["Mike", "John", "Steve"]
for name in names:
print(name)
Внутри расширенных циклов избегайте использования имен переменных, таких какitemилиvalue, потому что это затрудняет определение того, что хранится в переменной, особенно во вложенных расширенных циклах.
Генераторы
Генератор — это функция в Python, которая возвращает объект итератора вместо одного значения. Основное различие между обычными функциями и генераторами заключается в том, что генераторы используют yieldключевое слово вместо return. Каждое следующее значение в итераторе извлекается с помощью next(generator).
Допустим, мы хотим сгенерировать первые nкратные x. Наш генератор будет выглядеть примерно так:
def multiple_generator(x, n):
for i in range(1, n + 1):
yield x * i
multiples_of_5 = multiple_generator(5, 3)
print(next(multiples_of_5)) # 5
print(next(multiples_of_5)) # 10
print(next(multiples_of_5)) # 15Ключевое отличие генератора от классического итератора заключается в том, что итератор выдаёт уже существующие в каком-то контейнере значения, а генератор вычисляет новые значения на лету. Это позволяет экономить ресурсы системы, если для дальнейших вычислений не требуются, чтобы все значения где-то хранились в одном месте.