Masz wrażenie, że nikt nie czyta Twojego CV? Nie dostajesz zaproszeń na rozmowy rekrutacyjne?
Sprawdź swoje umiejętności techniczne a MockIT pomoże Ci znaleźć pracę w IT!
Python Developer - zaawansowane pytania i zadania rekrutacyjne
W tym artykule przyjrzymy się pytaniom których możesz się spodziewać podczas rozmowy rekrutacyjnej na stanowiska, które wymagają już pewnego doświadczenia w Pythonie.
Intro
Python to bardzo popularny język programowania, znany ze swojej prostoty, czytelności i szerokiego zastosowania. Niezależnie od tego, czy jesteś początkującym programistą, który dopiero zaczyna swoją przygodę z kodowaniem, czy doświadczonym deweloperem szukającym odpowiedzi na specyficzne pytania techniczne - w tym artykule znajdziesz coś dla siebie.
Odpowiem w nim na dziewięć technicznych pytań pojawiających się na rozmowach rekrutacyjnych na stanowisko Python Developera. Rekruterzy uwielbiają zadawać podchwytliwe pytania. Dlatego, zapoznanie się z tymi pytaniami pomoże Ci przygotować się do rozmów rekrutacyjnych. Co więcej, odpowiedzi na nie powinien znać każdy obecny i przyszły programista języka Python.
Jakie są różnice między list comprehension a generator expressions?
List comprehensions i generator expressions to dwie konstrukcje w Pythonie do tworzenia sekwencji danych. Chociaż mają bardzo podobną składnię, różnią się pod względem wydajności, szybkości i zachowania.
Tworzenie Obiektów
List Comprehensions: Tworzą pełną listę w pamięci. Oznacza to, że wszystkie elementy są generowane od razu i przechowywane w pamięci.
Generator Expressions: Tworzą generator, który produkuje elementy na bieżąco, jeden po drugim, gdy są potrzebne. Nie tworzy pełnej listy w pamięci.
Zarządzanie Pamięcią
List Comprehensions: Może być mniej wydajne, jeśli lista jest duża, ponieważ cała lista jest przechowywana w pamięci.
Generator Expressions: Bardziej efektywne pamięciowo, ponieważ elementy są generowane 'leniwie' (lazy evaluation) i nie są przechowywane wszystkie naraz w pamięci.
Czas Wykonania
List Comprehensions: Mają tendencję do bycia szybszymi, jeśli potrzebujesz przetworzyć wszystkie elementy, ponieważ cała lista jest dostępna od razu.
Generator Expressions: Mogą być wolniejsze w przetwarzaniu wszystkich elementów, ponieważ elementy są generowane na żądanie, co dodaje narzut czasowy dla każdej operacji 'next'.
Kiedy wykorzystywać
List Comprehensions: Są idealne, gdy potrzebujesz dostępu do wszystkich elementów naraz lub gdy wynikowa lista ma być użyta wielokrotnie.
Generator Expressions: Są lepsze, gdy pracujesz z dużymi zbiorami danych lub strumieniami danych, których nie można załadować w całości do pamięci lub gdy chcesz przetworzyć elementy tylko raz.
# List comprehension
list_comp = [x * 2 for x in range(10)]
print(list_comp)
# [0, 2, 4, 6, 8, 10, 12, 14, 16, 18]
# Iterating over List comprehension
for x in list_comp:
print(x)
# 0, 2, 4, 6, 8, 10, 12, 14, 16, 18 (one by one)
# Generator expression
gen_exp = (x * 2 for x in range(10))
print(gen_exp)
# <generator object <genexpr> at 0x...>
# Iterating over generator
for x in gen_exp:
print(x)
# 0, 2, 4, 6, 8, 10, 12, 14, 16, 18 (one by one)
Jak działa mechanizm zarządzania pamięcią w Pythonie. Jak działa garbage collection (GC) i kiedy może być wywołana ręcznie?
Mechanizm zarządzania pamięcią w Pythonie odpowiada za przydzielanie i zwalnianie pamięci w aplikacjach napisanych w tym języku. Oto, jak działa w prostych słowach.
Przydzielanie pamięci
Kiedy tworzysz nowy obiekt (np. liczbę, listę, słownik), Python automatycznie przydziela dla niego pamięć. Nie musisz się martwić o zarządzanie tą pamięcią ręcznie.
Zwalnianie pamięci
Gdy obiekt nie jest już potrzebny (czyli nie ma żadnej zmiennej, która by się do niego odwoływała), pamięć, którą zajmował, powinna być zwolniona, aby mogła być użyta przez inne obiekty.
Garbage Collection
Zbieranie Śmieci Garbage collection to mechanizm, który automatycznie zwalnia pamięć zajmowaną przez obiekty, które nie są już używane. W Pythonie działa to na dwa główne sposoby
Zliczanie referencji
Każdy obiekt ma licznik, który mówi, ile zmiennych (referencji) się do niego odwołuje. Kiedy licznik spada do zera, obiekt jest usuwany, a pamięć zwalniana.
Cykliczne zależności
Python ma mechanizm wykrywania i usuwania cyklicznych zależności (np. dwa obiekty, które odwołują się do siebie nawzajem, ale nie są używane w kodzie). Do tego używa tzw. generacyjnego garbage collectora, który okresowo sprawdza obiekty w poszukiwaniu cykli.
Ręczne wywołanie garbage collection
Choć Python automatycznie zarządza pamięcią, możliwe jest wywołanie go ręcznie, można to zrobić, używając modułu gc.
import gc
# Function to create a cycle of references
def create_cycle():
list_a = []
list_b = [list_a]
list_a.append(list_b)
# After the end of the function, list_a and list_b still exist in memory as a cycle of references
create_cycle()
# Before running GC
print(f"Objects in memory: {len(gc.get_objects())}")
# Running GC manually
gc.collect()
# After running GC
print(f"Objects in memory: {len(gc.get_objects())}")
Jak implementowane są dekoratory w Pythonie i w jakich przypadkach są szczególnie użyteczne?
Dekoratory w Pythonie są specjalnymi funkcjami, które modyfikują zachowanie innych funkcji lub metod. Pozwalają one na 'opakowanie' jednej funkcji przez inną, co daje możliwość dodania dodatkowej funkcjonalności przed lub po wykonaniu oryginalnej funkcji, bez zmiany jej kodu źródłowego.
Dekoratory są szczególnie użyteczne w następujących przypadkach.
Logowanie i monitorowanie
Umożliwiają śledzenie wywołań funkcji, co jest przydatne przy debugowaniu i monitorowaniu działania aplikacji.
def simple_decorator(func):
def wrapper():
print("Before running func")
func()
print("After running func")
return wrapper
@simple_decorator
def say_hello():
print("Hello!")
say_hello()
Autoryzacja i uwierzytelnianie
Mogą sprawdzić, czy użytkownik ma odpowiednie uprawnienia przed wykonaniem określonej funkcji.
def requires_auth(func):
def wrapper(user, *args, **kwargs):
if not user.get('is_authenticated'):
raise PermissionError("User is not authenticated")
return func(user, *args, **kwargs)
return wrapper
@requires_auth
def access_secure_data(user):
return "Secure Data"
user = {'is_authenticated': True}
print(access_secure_data(user))
user = {'is_authenticated': False}
try:
print(access_secure_data(user))
except PermissionError as e:
print(e)
Modyfikacja wejścia/wyjścia
Pozwalają na modyfikowanie argumentów przekazywanych do funkcji oraz wyników zwracanych przez funkcję.
def uppercase_output(func):
def wrapper(*args, **kwargs):
result = func(*args, **kwargs)
return result.upper()
return wrapper
@uppercase_output
def greet(name):
return f"Hello, {name}"
print(greet("world"))
Cachowanie wyników
Przyspieszają działanie funkcji przez zapamiętywanie wyników dla już obliczonych argumentów.
def cache(func):
storage = {}
def wrapper(*args):
if args in storage:
return storage[args]
result = func(*args)
storage[args] = result
return result
return wrapper
@cache
def fibonacci(n):
if n < 2:
return n
return fibonacci(n-1) + fibonacci(n-2)
print(fibonacci(10))
Kontrola dostępu
Mogą ograniczać dostęp do funkcji, np. ze względu na czas (limity wywołań).
import time
def rate_limit(func):
last_called = [0] # We use a list to store a variable in a closure
def wrapper(*args, **kwargs):
now = time.time()
if now - last_called[0] < 1: # Limits calls to 1 per second
raise RuntimeError("Function called too frequently")
last_called[0] = now
return func(*args, **kwargs)
return wrapper
@rate_limit
def do_something():
print("Function executed")
try:
do_something()
time.sleep(0.5)
do_something()
except RuntimeError as e:
print(e)
time.sleep(1)
do_something()
Jakie techniki stosujemy do profilowania kodu Pythona w celu zidentyfikowania wąskich gardeł?
Profilowanie kodu Pythona jest kluczowe do identyfikowania wąskich gardeł wydajnościowych i optymalizacji aplikacji. Na rynku dostępnych jest wiele rozwiazań, najprostzrym sposobem jest wykonanie 'stopera' dzięki timeit, nadaje się on doskonale do mikrobenchmarków, czyli do mierzenia czasu wykonywania małych fragmentów kodu.
import timeit
def test_func():
sum = 0
for i in range(10000):
sum += i
return sum
time = timeit.timeit('test_func()', globals=globals(), number=1000)
print(f"Execution time: {time:.5f} seconds")
cProfile to wbudowane narzędzie w Pythonie do profilowania. Można go użyć do mierzenia czasu wykonywania funkcji i identyfikowania najbardziej kosztownych fragmentów kodu.
import cProfile
import pstats
def profiling_func():
# Example profiling function
sum = 0
for i in range(10000):
sum += i
return sum
cProfile.run('profiling_func()', 'profil_output')
# Displaying results in readable form
p = pstats.Stats('profil_output')
p.sort_stats('cumulative').print_stats(10)
Poza tym można skorzystać z bardziej zaawansowanych narzędzi.
line_profiler
To narzędzie do profilowania kodu linia po linii. Jest to szczególnie przydatne narzędzie do dokładnej analizy czasu wykonania poszczególnych linii kodu. Dzięki niemu możesz zidentyfikować konkretne linie kodu, które są najbardziej kosztowne pod względem czasu wykonania.
memory_profiler
Pozwala dokładnie monitorować zużycie pamięci w trakcie wykonywania programu, co jest niezbędne do identyfikacji problemów związanych z zarządzaniem pamięcią i optymalizacją wydajności.
Py-Spy
Umożliwia monitorowanie zużycia procesora przez aplikację oraz identyfikowanie gorących punktów, czyli fragmentów kodu, które zużywają najwięcej zasobów procesora. Jest to przydatne narzędzie do diagnostyki wydajnościowej i identyfikacji wąskich gardeł w aplikacji.
Opisz, jak działa getitem, setitem i delitem w kontekście tworzenia niestandardowych typów kontenerów.
__getitem__, __setitem__ i __delitem__ to specjalne metody w Pythonie, które pozwalają na dostęp, ustawienie i usunięcie elementów z niestandardowych typów kontenerów, takich jak klasy, które zachowują się jak listy, słowniki lub inne kontenery.
__getitem__(self, key)
Metoda ta jest wywoływana, gdy próbujemy uzyskać dostęp do elementu kontenera za pomocą operatora indeksowania []. Akceptuje ona jeden argument, którym jest klucz lub indeks, za pomocą którego chcemy uzyskać dostęp do elementu.
class MyList:
def __init__(self, data):
self.data = data
def __getitem__(self, index):
return self.data[index]
my_list = MyList([1, 2, 3, 4, 5])
print(my_list[2]) # Displays: 3
__setitem__(self, key, value)
Metoda ta jest wywoływana, gdy próbujemy przypisać wartość do elementu kontenera za pomocą operatora indeksowania []. Akceptuje dwa argumenty: klucz lub indeks oraz wartość, którą chcemy przypisać.
class MyList:
def __init__(self, data):
self.data = data
def __setitem__(self, index, value):
self.data[index] = value
my_list = MyList([1, 2, 3, 4, 5])
my_list[2] = 10
print(my_list.data) # Displays: [1, 2, 10, 4, 5]
__delitem__(self, key)
Metoda ta jest wywoływana, gdy próbujemy usunąć element z kontenera za pomocą operatora del i indeksowania []. Akceptuje tylko jeden argument: klucz lub indeks elementu, który chcemy usunąć.
class MyList:
def __init__(self, data):
self.data = data
def __delitem__(self, index):
del self.data[index]
my_list = MyList([1, 2, 3, 4, 5])
del my_list[2]
print(my_list.data) # Displays: [1, 2, 4, 5]
Dzięki tym specjalnym metodom możliwe jest tworzenie niestandardowych typów kontenerów w Pythonie, które mogą zachowywać się jak wbudowane typy, takie jak listy czy słowniki. Pozwalają one na dostosowanie zachowania kontenera do indywidualnych potrzeb, na przykład poprzez niestandardowe operacje podczas uzyskiwania, ustawiania lub usuwania elementów.
Jak działają wyrażenia regularne w Pythonie? Jakie są najczęściej używane funkcje w module re?
Wyrażenia regularne w Pythonie są narzędziem do manipulowania i przetwarzania tekstów za pomocą wzorców. Moduł re w Pythonie dostarcza funkcji i obiektów do obsługi wyrażeń regularnych.
Jak działają wyrażenia regularne?
Wyrażenia regularne opisują wzorce znaków, które są używane do wyszukiwania i manipulowania tekstami. Mogą zawierać specjalne znaki oraz zwykłe znaki, które definiują, jakie wzorce są dopasowywane w tekście.
Najczęściej używane funkcje w module re
re.search(pattern, string, flags=0): Szuka pierwszego dopasowania wzorca w całym tekście.
import re
result = re.search(r'is', 'This is a test string.')
print(result)
# <re.Match object; span=(2, 4), match='is'>
re.match(pattern, string, flags=0): Sprawdza, czy wzorzec pasuje do początku tekstu.
import re
result = re.match(r'This', 'This is a test string.')
print(result)
# <re.Match object; span=(0, 4), match='This'>
re.findall(pattern, string, flags=0): Znajduje wszystkie dopasowania wzorca w tekście i zwraca listę wyników.
import re
result = re.findall(r'd+', 'There are 10 apples and 20 oranges.')
print(result) # ['10', '20']
re.sub(pattern, repl, string, count=0, flags=0): Zastępuje wszystkie wystąpienia wzorca w tekście danym łańcuchem.
import re
result = re.sub(r's+', '_', 'This is a test string.')
print(result) # This_is_a_test_string.
Te funkcje są podstawowymi narzędziami w pracy z wyrażeniami regularnymi w Pythonie. Pozwalają one na przeszukiwanie, dopasowywanie, podział i zamianę tekstu zgodnie z określonymi wzorcami.
Opisz różnice między podejściem proceduralnym, obiektowym i funkcyjnym w programowaniu w Pythonie.
Podejście proceduralne
W podejściu proceduralnym program jest strukturalnie zorganizowany wokół procedur (funkcji), które wykonują konkretne zadania na danych. Kod jest sekwencyjny i wykonywany od góry do dołu. Główne cechy podejścia proceduralnego to:
Procedury: Podstawowymi elementami są procedury (funkcje), które wykonują konkretne zadania na danych.
Zmienne globalne: Dane są zwykle przechowywane jako zmienne globalne, do których procedury mają dostęp.
Proste struktury danych: Używane są proste struktury danych, takie jak listy, krotki, słowniki itp.
Prostota: Podejście, które jest łatwe do zrozumienia i stosowania w małych projektach.
def count_sum(a, b):
return a + b
a = 10
b = 20
sum = count_sum(a, b)
print(sum)
Podejście obiektowe
W podejściu obiektowym program jest zorganizowany wokół obiektów, które są instancjami klas. Obiekty te przechowują dane (atrybuty) i metody (funkcje), które mogą operować na tych danych. Główne cechy podejścia obiektowego to:
Klasy i obiekty: Kod jest zorganizowany wokół klas, które definiują strukturę i zachowanie obiektów.
Hermetyzacja: Klasy mogą ukrywać swoje wewnętrzne implementacje, a dostęp do danych może być kontrolowany przez metody dostępowe (getter i setter).
Dziedziczenie: Klasy mogą dziedziczyć cechy i metody po innych klasach.
PolimorfizmObiekty różnych klas mogą być traktowane jednolicie, co umożliwia wykonywanie tych samych operacji na różnych typach danych.
class Calculator:
def __init__(self, a, b):
self.a = a
self.b = b
def count_sum(self):
return self.a + self.b
calculator = Calculator(10, 20)
sum = calculator.count_sum()
print(sum)
Podejście funkcyjne
W podejściu funkcyjnym program jest zorganizowany wokół funkcji, które są traktowane jako pierwszorzędne obiekty. Funkcje te mogą być przekazywane jako argumenty do innych funkcji, zwracane jako wartości z funkcji i przechowywane jako zmienne. Główne cechy podejścia funkcyjnego to:
Funkcje pierwszego rzędu: Funkcje mogą być przypisywane do zmiennych, przekazywane jako argumenty i zwracane jako wartości.
Bezstanowość: Funkcje nie mają stanu wewnętrznego i operują tylko na swoich argumentach.
Unikanie efektów ubocznych: Stara się unikać efektów ubocznych, co oznacza, że funkcje nie powinny modyfikować danych poza swoim zakresem.
Rekurencja: Często używa rekurencji do iteracji i przetwarzania danych.
W Pythonie można łączyć te trzy podejścia w jednym programie w zależności od potrzeb i preferencji projektowych. Jednak zwykle Python zachęca do używania podejścia obiektowego ze względu na jego elastyczność i możliwość ponownego użycia kodu.
Jakie są różnice między @property a bezpośrednim dostępem do atrybutów klasy? Jakie są zalety używania @property?
@property jest dekoratorem w Pythonie, który umożliwia definiowanie metod dostępowych do atrybutów klasy. Pozwala to na kontrolę dostępu do atrybutów oraz na wykonywanie dodatkowych działań podczas odczytu lub zapisu wartości atrybutu. Oto różnice między użyciem @property a bezpośrednim dostępem do atrybutów klasy:
Bezpośredni dostęp do atrybutów klasy
class MyClass:
def __init__(self):
self._attribute = None
my_object = MyClass()
my_object._attribute = "value"
print(my_object._attribute)
Użycie @property
class MyClass:
def __init__(self):
self._attribute = None
@property
def attribute(self):
return self._attribute
my_object = MyClass()
my_object.attribute = "wartość" # Throws error AttributeError
print(my_object.attribute)
Zalety używania @property
Kontrola dostępu: Pozwala na kontrolowanie dostępu do atrybutów klasy, umożliwiając definiowanie niestandardowych działań podczas odczytu i zapisu wartości atrybutu.
Enkapsulacja: Pomaga w enkapsulacji danych, co oznacza, że można ukryć wewnętrzną implementację atrybutów i zapewnić bezpieczny dostęp do nich.
Dostosowanie interfejsu: Umożliwia definiowanie interfejsu klasy w sposób bardziej zrozumiały i przyjazny dla użytkownika.
Łatwa do zrozumienia i utrzymania: Pomaga w tworzeniu bardziej zrozumiałego i elastycznego kodu, który jest łatwiejszy do utrzymania i rozszerzania w przyszłości.
Jak działają itertools i jakie są najczęściej używane funkcje tego modułu?
Itertools jest modułem w Pythonie, który zawiera zestaw narzędzi do tworzenia efektywnych iteratorów. Iteratory są obiektami, które umożliwiają przeglądanie sekwencji danych, jednocześnie zajmując minimalną ilość pamięci. itertools oferuje wiele przydatnych funkcji do manipulowania iteratorami i generowania iterowalnych sekwencji danych.
Kilka najczęściej używanych funkcji z modułu itertools
itertools.chain(*iterables): Funkcja chain łączy wiele iterowalnych obiektów w jeden długi iterator.
import itertools
list1 = [1, 2, 3]
list2 = ['a', 'b', 'c']
chain = itertools.chain(list1, list2)
for element in chain:
print(element)
itertools.cycle(iterable): Funkcja cycle tworzy nieskończony iterator, który cyklicznie powtarza elementy z danego iterowalnego obiektu.
import itertools
cycle = itertools.cycle([1, 2, 3])
for _ in range(5):
print(next(cycle))
itertools.count(start=0, step=1): Funkcja count tworzy nieskończony iterator, który generuje liczby zaczynając od wartości start z określonym krokiem step.
import itertools
counter = itertools.count(start=1, step=2)
for _ in range(5):
print(next(counter))
itertools.product(*iterables, repeat=1): Funkcja product tworzy iterator zawierający iloczyny kartezjańskie elementów z podanych iterowalnych obiektów.
import itertools
products = itertools.product('AB', repeat=2)
for product in products:
print(product)
itertools.permutations(iterable, r=None): Funkcja permutations generuje wszystkie możliwe permutacje elementów z danego iterowalnego obiektu.
import itertools
perms = itertools.permutations([1, 2, 3], 2)
for perm in perms:
print(perm)
itertools.groupby(iterable, key=None): Funkcja groupby grupuje elementy z danego iterowalnego obiektu na podstawie klucza, który jest funkcją określającą klucz grupowania.
import itertools
data = [('a', 1), ('b', 2), ('a', 3), ('b', 4)]
groups = itertools.groupby(data, key=lambda x: x[0])
for key, group in groups:
print(key, list(group))
Podsumowanie
Mam nadzieję, że odpowiedzi na te pytania techniczne związane z programowaniem w Pythonie okazały się pomocne i rozjaśniły niektóre z bardziej skomplikowanych aspektów tego języka. Python, dzięki elastyczności, pozostaje jednym z najważniejszych narzędzi w arsenale programistów na całym świecie.
Pamiętaj, że nauka programowania to proces ciągły, a każda nowa informacja i każde rozwiązane wyzwanie przybliża Cię do stania się bardziej biegłym i efektywnym programistą Python.
Techniczna rozmowa kwalifikacyjna na stanowisko Python Developera w branży IT to kluczowy etap w procesie rekrutacyjnym. Aby zwiększyć swoje szanse na sukces, warto dobrze przygotować się do rozmowy kwalifikacyjnej, szczególnie jeśli aspirujesz na stanowisko Junior Python Developera. Przykładowe pytania rekrutacyjne mogą obejmować jeszcze wiele innych zagadnień związanych z Pythonem, takie jak sortowanie bąbelkowe Python, operacje na plikach czy użycie funkcji anonimowej lambda w Pythonie.