Этот урок повторяет две темы блока: урок 36 — словари, продолжение (методы, view objects, вложенные структуры, копирование, dict comprehension) и урок 38 — функции, области видимости (def, аргументы, return, LEGB, global). Примеры идут от простого к сложному, часть из них сознательно смешивает обе темы — так виднее, как словари и функции работают вместе. Весь вывод в комментариях получен запуском кода, а не набран по памяти.
Пример 1. Базовый набор методов словаря: fromkeys, get, setdefault
Три метода закрывают самые частые задачи: создать словарь с заготовкой, безопасно прочитать значение и добавить ключ только если его ещё нет.
data = dict.fromkeys(["x", "y", "z"], 0)
print(data)
user = {"name": "Alice", "age": 30}
print(user.get("age"))
print(user.get("city", "Unknown"))
counters = {}
counters.setdefault("visits", 0)
counters["visits"] += 1
counters.setdefault("visits", 0)
counters["visits"] += 1
print(counters)
# {'x': 0, 'y': 0, 'z': 0}
# 30
# Unknown
# {'visits': 2}
Что происходит: get() не бросает KeyError, если ключа нет, а возвращает второй аргумент. setdefault() на второй вызов уже находит ключ "visits" и не трогает значение — счётчик не сбрасывается.
Пример 2. Ловушка dict.fromkeys(): общая ссылка на изменяемый объект
Топ-3 ошибка урока: если вторым аргументом передать список, все ключи получат ссылку на один и тот же объект, а не на свои копии.
keys = ["x", "y"]
shared_list = []
shared = dict.fromkeys(keys, shared_list)
shared["x"].append(1)
print(shared)
independent = {key: [] for key in keys}
independent["x"].append(1)
print(independent)
# {'x': [1], 'y': [1]}
# {'x': [1], 'y': []}
Что происходит: append(1) к shared["x"] меняет тот же список, на который смотрит и shared["y"] — они оба указывают на shared_list. Dict comprehension {key: [] for key in keys} создаёт новый пустой список на каждой итерации, поэтому independent ведёт себя ожидаемо.
Пример 3. keys(), values(), items() — динамические view, а не снимок
View objects «смотрят» на живой словарь: любое изменение словаря сразу видно через ранее полученный view. Чтобы зафиксировать состояние на момент вызова, нужен list().
data = {"name": "Alice", "age": 30}
live_keys = data.keys()
print(live_keys)
data["city"] = "New York"
print(live_keys)
snapshot = list(data.keys())
data["country"] = "USA"
print(snapshot)
print(list(data.keys()))
# dict_keys(['name', 'age'])
# dict_keys(['name', 'age', 'city'])
# ['name', 'age', 'city']
# ['name', 'age', 'city', 'country']
Что происходит: live_keys — это view, добавление "city" в словарь сразу отражается в нём. snapshot — обычный список, скопированный один раз, поэтому добавление "country" его уже не меняет.
Пример 4. Вложенные словари: доступ по цепочке ключей и обход
Словарь может хранить списки и другие словари. Чтобы добраться до значения, ключи и индексы указываются по очереди, уровень за уровнем.
school = {
"class1": {"students": ["Alice", "Bob"], "teacher": "Mrs. Smith"},
"class2": {"students": ["Carol"], "teacher": "Mr. Lee"},
}
print(school["class1"]["students"][0])
for class_name, info in school.items():
print(class_name, "-", info["teacher"], "-", len(info["students"]), "students")
# Alice
# class1 - Mrs. Smith - 2 students
# class2 - Mr. Lee - 1 students
Что происходит: school["class1"]["students"][0] читается слева направо: сначала ключ класса, потом ключ поля, потом индекс элемента. Обход через items() даёт доступ и к ключу верхнего уровня, и к вложенному словарю сразу.
Пример 5. copy() против copy.deepcopy()
copy() создаёт новый внешний словарь, но вложенные объекты (списки, другие словари) остаются общими с оригиналом. deepcopy() копирует и их.
import copy
original = {"name": "Alice", "scores": [90, 85]}
shallow = original.copy()
deep = copy.deepcopy(original)
original["name"] = "Bob"
original["scores"].append(80)
print(shallow)
print(deep)
# {'name': 'Alice', 'scores': [90, 85, 80]}
# {'name': 'Alice', 'scores': [90, 85]}
Что происходит: замена original["name"] не видна нигде, кроме original — строки неизменяемы, присваивание просто переставляет ссылку в словаре original. А вот original["scores"].append(80) меняет тот же список, на который смотрит shallow["scores"], поэтому в shallow появляется 80. У deep свой независимый список.
Пример 6. Dict comprehension: преобразование и фильтрация
Словарь можно построить и отфильтровать в одну читаемую конструкцию — без явного цикла и промежуточного пустого словаря.
words = ["cat", "elephant", "dog", "hippopotamus"]
lengths = {word: len(word) for word in words}
print(lengths)
long_words = {word: length for word, length in lengths.items() if length > 3}
print(long_words)
# {'cat': 3, 'elephant': 8, 'dog': 3, 'hippopotamus': 12}
# {'elephant': 8, 'hippopotamus': 12}
Что происходит: первое выражение преобразует список слов в словарь «слово → длина». Второе строится уже из первого словаря и добавляет условие if — comprehension совмещает map и filter для словарей в одной строке.
Пример 7. def, pass, позиционные и именованные аргументы, значения по умолчанию
Функция объявляется через def, вызывается по имени со скобками. pass временно оставляет обязательное тело пустым. Аргумент со значением по умолчанию можно не указывать при вызове.
def stub():
pass
def greet(name, greeting="Hello"):
return f"{greeting}, {name}!"
print(greet("Alice"))
print(greet("Bob", "Hi"))
print(greet(name="Carol", greeting="Hey"))
# Hello, Alice!
# Hi, Bob!
# Hey, Carol!
Что происходит: в первом вызове greeting не передан — используется значение по умолчанию "Hello". Во втором аргументы позиционные — сопоставляются по порядку. В третьем — именованные, поэтому порядок можно поменять местами.
Пример 8. *args и **kwargs: упаковка лишних аргументов
*args собирает лишние позиционные аргументы в кортеж, **kwargs — лишние именованные в словарь. Это позволяет функции принимать заранее неизвестное число аргументов.
def describe(*args, **kwargs):
print("args:", args)
print("kwargs:", kwargs)
describe(1, 2, 3, name="Alice", age=30)
def total(*numbers):
return sum(numbers)
print(total(1, 2, 3, 4))
# args: (1, 2, 3)
# kwargs: {'name': 'Alice', 'age': 30}
# 10
Что происходит: describe не объявляет ни одного именованного параметра заранее, но принимает любое их количество. total использует *args, чтобы просуммировать сколько угодно чисел через встроенный sum().
Пример 9. return и None, правило LEGB
Функция без return всегда возвращает None. Правило LEGB (Local → Enclosing → Global → Built-in) объясняет, почему локальная переменная внутри функции перекрывает одноимённую глобальную и не влияет на неё снаружи.
def no_return():
print("done")
result = no_return()
print(result)
x = 10
def show_value():
x = 5
return x
print(show_value())
print(x)
# done
# None
# 5
# 10
Что происходит: no_return() печатает строку, но ничего не возвращает — result становится None. Внутри show_value() присваивание x = 5 создаёт новую локальную переменную по правилу LEGB (Local), глобальный x = 10 она не трогает.
Пример 10. global против параметров и return
global позволяет функции изменить переменную из внешней области видимости напрямую. Это работает, но делает поведение функции неочевидным по одной сигнатуре — обычно предсказуемее передавать значение параметром и возвращать новое через return.
counter = 0
def increment_global():
global counter
counter += 1
increment_global()
increment_global()
print(counter)
def increment(value):
return value + 1
counter2 = 0
counter2 = increment(counter2)
counter2 = increment(counter2)
print(counter2)
# 2
# 2
increment(value) результат виден прямо в сигнатуре — «принимает число, возвращает число». У increment_global() нужно читать тело функции, чтобы понять, что она вообще что-то меняет снаружи. Оба примера дали 2, но предсказуемость кода — разная.
Пример 11. Практика: валидация скобок стеком и словарём соответствий
Задача из практической части урока: словарь хранит пары «закрывающая → открывающая» скобка, список работает как стек. Пример совмещает функции (def, return) и словарь (in, доступ по ключу).
def is_valid_brackets(text):
pairs = {")": "(", "]": "[", "}": "{"}
stack = []
for char in text:
if char in pairs.values():
stack.append(char)
elif char in pairs:
if not stack or stack.pop() != pairs[char]:
return False
return not stack
print(is_valid_brackets("([)]"))
print(is_valid_brackets("({[]})"))
# False
# True
Что происходит: каждая открывающая скобка кладётся в стек. При встрече закрывающей скобки функция сверяет её пару с вершиной стека через pairs[char]: если пары не совпали или стек пуст — расстановка неверна. Пустой стек в конце означает, что все скобки закрылись.
Пример 12. Практика: словарь + функция вместе — реверс словаря и группировка
Два примера из практической части показывают, как setdefault() и цепочки if/elif/else внутри функции решают реальные задачи с данными.
def reverse_dictionary(data):
result = {}
for key, value in data.items():
result.setdefault(value, []).append(key)
return result
print(reverse_dictionary({"a": 1, "b": 2, "c": 1, "d": 3}))
def group_students(students):
groups = {"Отличники": {}, "Хорошисты": {}, "Троечники": {}, "Не сдали": {}}
for name, score in students.items():
if score >= 85:
groups["Отличники"][name] = score
elif score >= 70:
groups["Хорошисты"][name] = score
elif score >= 50:
groups["Троечники"][name] = score
else:
groups["Не сдали"][name] = score
return groups
students = {"Аня": 92, "Боря": 76, "Ваня": 65, "Галя": 48}
print(group_students(students))
# {1: ['a', 'c'], 2: ['b'], 3: ['d']}
# {'Отличники': {'Аня': 92}, 'Хорошисты': {'Боря': 76}, 'Троечники': {'Ваня': 65}, 'Не сдали': {'Галя': 48}}
Что происходит: reverse_dictionary использует setdefault(value, []), чтобы не проверять заранее, встречалось ли значение — при первой встрече создаётся пустой список, при повторной он уже есть и просто пополняется. group_students возвращает новую структуру и не трогает исходный словарь students — функция с return предсказуемее, чем изменение данных на месте.
dict.setdefault() и модуля copy сверяйте с dict и copy в официальной документации.
Что делать дальше
Если словарь методов или аргументы функций путаются — вернитесь к уроку 36 и уроку 38. Дальше — задания этого урока и раздел типичные ошибки, если пример не заработал сразу.