Собеседование на должность Junior QA Automation Engineer. Python
t.me/pythonl
1. Проектирование
- Почему глобальные переменные это плохо? 1. Нарушают инкапсуляцию (к ним открыт доступ из любой части программы), добавляют лишние зависимости между компонентами. 2. Ухудшают масштабируемость 3. Способствуют возникновению трудноуловимых ошибок.
- Что такое инверсия управления? Явление, при котором роль главной программы в координации и последовательности действий приложения выполняет фреймворк (а не код пользователя). В этом основное отличие фреймворка и библиотеки. Библиотека - это набор функций, которые вызываются кодом пользователя, а после окончания выполнения возвращают управление пользователю. В случае с фреймворком он сам координирует и вызывает код пользователя.
- Закон Деметры. Каждый модуль должен обладать минимальной информированностью о других модулях. "Не разговаривай с незнакомцами!"
- Принцип подстановки Барбары Лисков. Наследующий класс должен дополнять, а не замещать поведение базового класса. Если класс Б унаследован от А, то мы можем заменить в программе все использования класса А на Б и при этом в работе программы ничего не изменится.
- Dependency hell (Ад зависимостей). Разрастание графа зависимостей библиотек. Чем опасно? Например, несколько или даже один программный продукт может косвенно потребовать разные версии одной и той же библиотеки.
- Хорошо спроектированное ПО должно обладать сильным сцеплением и слабой связностью. Что это значит? Сцепление - сила зависимостей внутри модуля. Связность - сила зависимостей между разными модулями. Итог: внутри модуля должны быть сильные зависимости, а снаружи - нет.
- Нужны ли комментарии в коде? Нужно стараться писать код так, чтобы комментарии были не нужны.
- Шаблоны. Singleton (Одиночка). Гарантирует существование только одного объекта класса.
Python. Singleton pattern example.
class Singleton:
__instance = None
# Single call check
def __init__(self):
print('Constructor called!')
@staticmethod
def instance():
if Singleton.__instance is None:
Singleton.__instance = Singleton()
return Singleton.__instance
6. Шаблоны. Observer (Наблюдатель). Наблюдаем за списком объектов. При возникновении события оповещаем каждый их них.
Python. Observer pattern example.
class CameraSystemManager:
def __init__(self):
self.__observers = list()
def attach(self, observer):
self.__observers.append(observer)
def detach(self, observer):
self.__observers.remove(observer)
def notify(self):
for observer in self.__observers:
observer.take_photo()
class AbstractObserver(ABC):
@abstractmethod
def take_photo(self):
pass
class Camera(AbstractObserver):
def __init__(self, name):
self.name = name
def take_photo(self):
print(f'{self.name}: Photo is done')
camera1 = Camera('Camera 1')
camera2 = Camera('Camera 2')
manager = CameraSystemManager()
manager.attach(camera1)
manager.attach(camera2)
manager.notify()
7. Шаблоны. Abstract Factory (Абстрактная фабрика). В приведенном примере метод create_form_with_buttons(factory) создает объекты классов Form и Button на основе переданной фабрики (LinuxFactory или WindowsFactory). Класс AbstractFactory определяет фабричные методы create_form() и create_button() и передает их по наследству классам LinuxFactory и WindowsFactory.
Python. Abstract Factory pattern example.
class AbstractFactory:
@classmethod
def create_form(cls, name):
return cls.Form(name)
@classmethod
def create_button(cls, name):
return cls.Button(name)
class LinuxFactory(AbstractFactory):
class Form:
def __init__(self, name):
self.name = f'LinuxFactory: {name}'
self.button = []
def add_button(self, btn):
self.button.append(btn)
class Button:
def __init__(self, name):
self.name = f'LinuxFactory: {name}'
class WindowsFactory(AbstractFactory):
class Form:
def __init__(self, name):
self.name = f'WindowsFactory: {name}'
self.button = []
def add_button(self, btn):
self.button.append(btn)
class Button:
def __init__(self, name):
self.name = f'WindowsFactory: {name}'
def create_form_with_buttons(factory):
form = factory.create_form('Form 1')
button = factory.create_button('Button 1')
form.add_button(button)
return form
linux_form = create_form_with_buttons(LinuxFactory)
windows_form = create_form_with_buttons(WindowsFactory)
2. Языки программирования
- Императивные (процедурные и объектно-ориентированные) ЯП. При таком подходе программа представляет собой совокупность инструкций, которые изменяют состояние данных. Примеры: С++, Java, Ruby, Python.
- Функциональные ЯП. Обходимся вычислением результатов функций от исходных данных и результатов других функций, и не предполагаем явное хранение состояния. Примеры: Haksell, Erlang.
- Компилятор. Транслирует программу на языке высокого уровня в программу на низкоуровневом языке, близком машинному коду. На выходе - исполняемый файл. Пример: C++
- Интерпретатор. Построчно выполняет инструкции кода на высокоуровневом языке. Пример: Python
- Высокоуровневые ЯП. Легко читаются людьми. Не нужно знать, на каком оборудовании будет запускаться программа. Пример: Java, Python.
- Низкоуровневые ЯП. Учитывают требования архитектуры железа. Более быстрые и эффективные, но сложные для работы.
- Статическая / динамическая типизация. Статическая - типы данных выясняются на этапе компиляции (С++, Java). Динамическая - на этапе выполнения программы (Python, Ruby).
- Явная / неявная типизация. Явная - тип данных задает программист в коде (C++). Неявная - тип данных определяется компилятором / интерпретатором (Python).
- Структуры данных. Массив, Стек, Очередь, Связный список, Дерево, Граф, Хэш-таблица.
- Стек. Последний вошел (push), первый вышел (pop).
- Очередь. Первый вошел(append), первый вышел(pop).
- Связный список. Каждый узел списка - данные + указатель на следующий узел.
- Граф. Множество узлов, соединенных ребрами. Ребро может иметь вес.
- Дерево. Это граф, в котором нет циклов.
- Бинарное дерево поиска. Каждый узел может иметь 0, 1 или 2 потомка. Значение кладется в дерево так: последовательно идем от вершины дерева и сравниваем каждый узел в новым значением: если оно меньше узла, но кладем слева, если больше - то справа.
- Хэш-таблица. Можно представить как массив, в котором индекс элемента вычисляет как хэш-функция(свертка).
- Замыкание. Это функция, которая «запоминает» окружение, в котором она была создана.
Python. Closure example.
def counter():
cnt = 0
def current():
nonlocal cnt
cnt += 1
print(cnt)
return current
closure_function = counter()
closure_function() # 1
closure_function() # 2
18. Лямбда функция. Функция, которая 1) не имеет имени 2) возвращает значение одного выражения и 3) используется в коде единожды.
Python. Lambda function example.
def add(value):
return lambda param: param + value
add_to_100_function = add(100)
a = add_to_100_function(5) # 105
b = add_to_100_function(50)
19. Функция высокого порядка. Это функция, которая принимает или возвращает другие функции.
20. Циклы. for, while, do while.
3. ООП
- Определение ООП. Методология, в которой программа - совокупность объектов, каждый из которых - экземпляр класса, а классы образуют иерархию наследования.
- Класс. Шаблон для создания объектов, обеспечивающий начальные значения состояний (инициализация полей и реализация методов).
- Объект. Экземпляр класса, имеющий определенные поля (атрибуты) и операции над ними (методы).
- Поля, методы. Поле - свойство объекта, метод - функция.
- Абстрактный класс. Класс, для которого не реализован ОДИН или БОЛЬШЕ методов. Особенности: 1. Это класс, для которого нельзя создать объект. 2. Может содержать как обычные, так и абстрактные поля и методы. 3. Не допускает множественное наследование.
- Абстрактный метод (виртуальный метод). Метод класса, реализация для которого отсутствует.
- Интерфейс. Это абстрактный класс, у которого НИ ОДИН метод не реализован, все они публичные и нет переменных класса. Любой интерфейс - это абстрактный класс, но не наоборот.
- Абстракция. 1. Выделяет главные свойства предмета. 2. Отбрасывает второстепенные характеристики.
- Инкапсуляция. Прячет внутреннюю реализацию объекта, все взаимодействия - через интерфейс.
- Наследование. Создаем класс на основе существующего. Потомок наследует поля и методы родителя + добавляет свои. Выражает отношение "Является" (например, Mercedes является машиной).
Python. Inheritance example.
class Human:
def __init__(self, name):
self.name = name
def speak(self, phrase):
print(f'{self.name} сказал: \'{phrase}\'')
class Doctor(Human):
def __init__(self, name, specialization):
super().__init__(name)
self.specialization = specialization
def diagnose(self):
super().speak(f'Ваш диагноз - ОРВИ')
if __name__ == "__main__":
doctor_alexander = Doctor('Александр', 'Терапевт')
doctor_alexander.diagnose()
11. Композиция. Класс, известный как составной, содержит объект другого класса, известный как компонент. Выражает отношение "Имеет" (Например, машина имеет двигатель).
Python. Composition example.
class Hobby:
def __init__(self, title):
self.title = title
class Human:
def __init__(self, name, hobby_title):
self.name = name
self.hobby = Hobby(hobby_title)
if __name__ == '__main__':
human = Human('Павел', 'Шахматы')

ООП. Композиция и наследование.
12. Чем отличаются Наследование и Композиция? Общее: позволяют повторно использовать существующий код. Отличия:
- Наследование требует расширения наследуемого класса.
- Во многих языках запрещено множественное Наследование. А значит нельзя переиспользовать функционал нескольких разных классов. В Композиции- можно.
- При Композиции легче писать юнит тесты - делаем заглушки. При Наследовании это сделать сложнее - не получится заменить заглушкой родительский класс.
- При наследовании класс-потомок зависит от функционала класса-родителя. Ломается родитель - ломается и потомок.
Итог: когда нужно использовать класс как таковой без каких-либо изменений, рекомендуется Композиция, а когда нужно изменить поведение метода в другом классе, рекомендуется Наследование.

ООП. Наследование и Композиция. Код.
13. Полиморфизм. Поддержка нескольких реализаций на основе общего интерфейса. Т.е. позволяет перегружать одноименные методы родительского класса в классах-потомках.
Читайте также
[ Часть 3 ] Собеседование на должность QA Automation Engineer. Web, SQL, Linux, Git, сети.
4. Алгоритмы, задачи
1. Сформируйте последовательность Фибоначчи.
Python. Fibonacci sequence.
LENGTH = 7
def create_fib_sequence(length = LENGTH):
lst = [0, 1]
for i in range(1, length-1):
lst.append(lst[i-1] + lst[i])
return lst
res = create_fib_sequence()
2. Определить, является ли строка палиндромом.
Python. Palindrome function.
def is_palindrome(str): reversed_str = str[::-1] return str == reversed_str
3. Сортировка. Пузырьком. Проходимся по элементам массива и попарно сравниваем. Если левый больше правого - меняем местами.
Python. Bubble Sort.
for i in range(n-1):
for j in range(n-i-1):
if a[j] > a[j+1]:
a[j], a[j+1] = a[j+1], a[j]
4. Сортировка. Вставка. Делим массив на две части (левую и правую). Левую часть считаем отсортированной. Изначально первый элемент массива оставляем в левой части, все остальное относим к правой (не отсортированной). Начинаем перемещаться по не отсортированной части. Берем первый элемент, и попарно сравнивая с соседними, ищем ему место в отсортированной части. Например, имеем массив [ 4 6 2 1 ]. Выполняем сортировку:
- Делим на 2 части: [ 4 | 6 2 1 ].
- Берем элемент 6 и ставим его на подходящее место в отсортированной части: [ 4 6 | 2 1 ].
- Ставим на свое место элемент 2: [ 2 4 6 | 1 ].
- Ставим на свое место элемент 1: [ 1 2 4 6 ].
5. Сортировка. QuickSort. Основывается на выборе опорного элемента и дальнейшей сортировке элементов на группы: меньше / равны / большего опорного. В качестве опорного элемента эффективно выбирать медианное значение. Медианное значение - значение, которое находится в середине отсортированного списка. Алгоритм:
- Выбираем опорный элемент.
- Перераспределяем элементы относительно опорного - слева меньше, справа больше.
- Рекурсивно выполняем п 1 и п 2 на полученных подмассивах.
- Рекурсия не применяется, если в подмаслила остался 1 элемент или вообще ни одного.
5. Python
- Какая типизация используется в Python? Динамическая
- Какие типы данных вы знаете? None, bool, int, float, complex, list, tuple, str, bytes, bytearray, memoryview, set, frozenset, dict.
- Чем отличаются изменяемые и неизменяемые данные? Изменяемый тип — сложный тип данных в объектно-ориентированном программировании, значения которого (как правило — объекты) после своего создания допускают изменение своих свойств. К неизменяемым относятся целые числа (int), числа с плавающей запятой (float), булевы значения (bool), строки (str), кортежи (tuple). К изменяемым — списки (list), множества (set), байтовые массивы (byte arrays) и словари (dict).
- Дан кортеж: tpl = (1, 2, 3, [1,2,3], 5). Как все знают, кортеж - это неизменяемый тип данных. Изменится ли его содержимое после выполнения команды tpl[3].append(4)? Да, изменится, так как кортеж содержит только ссылки на объекты и поэтому не может защитить объект внутри себя от изменения.
- Чем отличаются генераторы от итераторов? Итератор — механизм поэлементного обхода данных, а генератор позволяет отложено создавать результат при итерации. Генератор может создавать результат на основе алгоритма или брать элементы из источника данных и изменять их. Любой генератор в Python - итератор, но не наоборот. Примеры генераторов: range() (генерирует арифметическую прогрессию), enumerate() (генерирует двухэлементные кортежи: индекс + элемент итерируемого объекта)
- Как пройтись по всем парам ключ-значение в словаре? for key, value in dct.items():
- Что такое lambda в Python? Анонимная функция, то есть функция, которая имеет функционал (в одну строку), но не имеет имени. Как правило, используется как параметр функции (например filter(), sorted()) или как возвращаемое значение функции. Примеры:
Python. Lambda.
# Пример 1
def multiply(num):
return lambda x: x + num
multiply_by_10 = multiply(10)
res = multiply_by_10(5)
# Пример 2
filtered_lst = list(filter(lambda x: x % 2 == 0, lst))
filtered_dict = dict(filter(lambda item: 'O' in item[1], dct.items()))
# Пример 3
generated_dict = {item[0]: item[1] * 2 for item in dct.items()}
# Пример 4
mapped_lst = list(map(lambda x: x * 10, lst))
8. Что такое декоратор? Декоратор - это обёртка над функцией (или другим объектом), которая изменяет ее поведение. Это удобно, т.к. не нужно менять исходный код, который не обязательно будет вашим. Использование: оценка времени работы функции, кеширование, запуск только в определенное время, задание параметров подключения к базе данных и т.д.
Python. Decorator.
# Простой декоратор
def my_decorator(my_func):
def inner(value):
value *= 2
my_func(value)
return inner
# Декоратор для функции с параметрами
def my_decorator_with_params(my_func):
def inner(*args, **kwargs):
print('Decorator starts...')
my_func(*args, **kwargs)
print('Decorator finishes...')
return inner
@my_decorator
def print_hi():
print('Hi!')
@my_decorator_with_params
def adder(**nums):
print(sum(nums.values()))
# Вызов декорированных функций
print_hi()
adder(a=1, b=2)
9. Знаете ли какие-нибудь встроенные в Python декораторы? 1. @staticmethod, @classmethod. 2. @lru_cache из модуля functools. 3. @dataclass из модуля dataclass
10. Что такое контекстный менеджер? Для чего он нужен? Приведите пример. Контекстные менеджеры позволяют задать поведение при работе с конструкцией with: при входе и выходе из блока. Это упрощает работу с ресурсами в части их захвата и освобождения.
Python. Context manager.
import io
with open('1.txt', 'w') as f:
f.write('Hello!')
11. Приведите пример обработки исключения.
Python. Exception.
import io
class MyPersonalException(Exception):
pass
# Example 1
try:
f.write('Hello')
except io.UnsupportedOperation as e:
print('UnsupportedOperation!')
finally:
print('Releasing resources')
f.close()
# Example 2
try:
raise MyPersonalException('This is my personal exception!')
except MyPersonalException as e:
if 'my personal' not in str(e):
raise e
print('My personal exception was caught')
12. Назовите основные функции по работе с json в питоне. Запись: dump(), dumps(). Чтение: load(), loads().
Python. JSON.
import json
st = 'String'
lst = [1, 2, 3]
dct = {1: 'One', 2: 'Two'}
# Дампим словарь в json и записываем в файл
with open('1.txt', 'w') as f:
json.dump(dct, f, indent=4)
# Формируем json на основе словаря и сохраняем в переменную
my_json = json.dumps(dct, indent=4)
# Парсим строку в словарь или список
my_str = '{"1": "One", "2": "Two"}'
my_json = json.loads(my_str)
my_str = '[1, 2, 3]'
my_json = json.loads(my_str)
13. Какой вывод ожидается после третьего вызова функции? Почему?
Python
def my_func(a, lst=[]):
lst.append(a)
return lst
print(my_func(10))
print(my_func(100))
print(my_func(1000))
В Python аргументы со значением по умолчанию вычисляются единожды в момент объявления функции. В нашем примере таким аргументом является список lst. И каждый раз при вызове функции - его содержимое будет меняться.
Вывод
[10] [10, 100] [10, 100, 1000]