Лекция 1. Подпрограммы: функции и область видимости¶
До этого мы программировали в основном «линейно»: пишем команды одну за другой, изредка ветвимся и зацикливаемся. Это нормально для коротких скриптов, но для проектов сложнее «hello world» уже не годится — человеку очень сложно держать в голове сотни строк линейного кода.
Чтобы упростить работу, обособленные или повторяющиеся части программы выделяют в подпрограммы.
Подпрограмма — функционально независимая часть программы. Структура подпрограммы такая же, как у программы в целом: у неё есть имя, входные параметры, тело и (обычно) возвращаемое значение.
Подпрограммы решают три задачи:
- избавляют от необходимости повторять одни и те же куски кода;
- улучшают структуру программы — основная функция становится похожа на «оглавление»;
- упрощают сопровождение: правишь подпрограмму один раз, а изменение видно везде, где она вызывается.
Хорошие поводы выделить код в отдельную подпрограмму:
- вы написали один и тот же кусок кода более одного раза;
- внутри логики «слишком много мелочей», заслоняющих смысл;
- алгоритм сложный и его хочется отладить и протестировать отдельно;
- этот код, скорее всего, понадобится в других программах.
В Python подпрограммы — это функции (def или lambda). В Go — тоже функции (func). И там и там — это объекты первого класса: их можно присваивать переменным, передавать в аргументах, возвращать из других функций.
Функция в Python: базовый синтаксис¶
Определение функции начинается с ключевого слова def:
def add(x: int, y: int) -> int:
"""Сложить два числа и вернуть результат."""
return x + y
print(add(2, 3)) # 5
print(add("ab", "cd")) # "abcd" — Python не проверяет типы в рантайме
def— служебное слово.add— имя функции (snake_case по PEP 8).(x: int, y: int)— список параметров с аннотациями типов.-> int— тип возвращаемого значения."""..."""— строка документации (docstring).return— возврат значения; если его нет, функция возвращаетNone.
Функция в Go:
В Go типы обязательны и проверяются компилятором. Имена с заглавной буквы (Add) — экспортируются из пакета (видны другим пакетам), с маленькой — приватные.
Аргументы функции¶
При определении функции параметры называют формальными. При вызове — переданные значения называются фактическими.
Позиционные и именованные аргументы¶
В Go именованных параметров нет. Если их хочется — заводят структуру:
type PersonOpts struct {
Name string
Age int
}
func Person(opts PersonOpts) {
fmt.Printf("%s is %d years old\n", opts.Name, opts.Age)
}
Person(PersonOpts{Name: "John", Age: 23})
Значения по умолчанию¶
В Python:
def space(planet_name: str, center: str = "Star") -> None:
print(f"{planet_name} is orbiting a {center}")
space("Mars") # Mars is orbiting a Star
space("Mars", "Black Hole") # Mars is orbiting a Black Hole
В Go значений по умолчанию нет. Эмулируют либо перегрузкой через переменное число аргументов, либо через структуру опций (паттерн functional options):
type SpaceOption func(*spaceConfig)
type spaceConfig struct {
center string
}
func WithCenter(c string) SpaceOption {
return func(cfg *spaceConfig) { cfg.center = c }
}
func Space(planet string, opts ...SpaceOption) {
cfg := spaceConfig{center: "Star"}
for _, opt := range opts {
opt(&cfg)
}
fmt.Printf("%s is orbiting a %s\n", planet, cfg.center)
}
Space("Mars")
Space("Mars", WithCenter("Black Hole"))
Переменное число аргументов¶
В Python *args — кортеж позиционных, **kwargs — словарь именованных. В Go вариативный параметр всегда последний, и внутри функции это обычный slice.
Опасная ловушка: изменяемый default¶
# ПЛОХО — список-default создаётся один раз на функцию,
# а не при каждом вызове
def append_to(value, lst=[]):
lst.append(value)
return lst
print(append_to(1)) # [1]
print(append_to(2)) # [1, 2] ← неожиданно!
Правильно:
В Go проблемы нет — значения по умолчанию не поддерживаются.
Аннотации типов¶
В Python со версии 3.5 можно записывать типы аргументов и возвращаемых значений:
from collections.abc import Iterable
def stats(values: Iterable[float]) -> tuple[float, float]:
"""Среднее и медиана списка чисел."""
items = sorted(values)
n = len(items)
mean = sum(items) / n
median = items[n // 2] if n % 2 else (items[n // 2 - 1] + items[n // 2]) / 2
return mean, median
Что важно: Python в рантайме типы не проверяет. Они нужны для:
- читаемости кода (IDE/ревьюеру сразу понятно, что ожидается);
- автокомплита и подсказок в редакторе;
- статической проверки утилитами
mypy,pyright,ruff.
Хороший стиль для современного Python (3.10+):
- встроенные типы вместо
typing.Listиtyping.Dict:list[int],dict[str, int]; - союзы через
|:int | NoneвместоOptional[int]; from collections.abc import Iterable, Mapping, Sequenceдля абстракций.
В Go типы — часть синтаксиса. Альтернативного «опционального» режима нет.
Документирование (docstrings)¶
PEP 257 рекомендует:
- для каждой публичной функции — docstring;
- начинается с краткого предложения в повелительном наклонении;
- грамотный язык, законченные предложения.
def k_nearest_neighbors(dataframe: pd.DataFrame, k: int = 5) -> pd.DataFrame:
"""Найти k ближайших соседей для каждой строки.
Возвращает DataFrame, где для каждой исходной строки добавлены
индексы её k ближайших соседей по евклидовой метрике.
Args:
dataframe: исходный набор данных.
k: количество соседей. По умолчанию 5.
Returns:
Расширенный DataFrame.
"""
В Go комментарий в виде // FunctionName ... сразу над функцией становится её документацией (см. go doc и pkg.go.dev):
// Sum returns the sum of integer values.
// Returns 0 when no arguments are passed.
func Sum(values ...int) int {
...
}
Область видимости (scope)¶
Каждое имя — переменная, функция, импорт — существует в какой-то области видимости. Если обратиться к имени вне его области, будет ошибка (NameError в Python, ошибка компиляции в Go).
Python: LEGB¶
В Python работает правило поиска имён LEGB:
- Local — внутри текущей функции;
- Enclosing — внутри охватывающих функций (для замыканий);
- Global — на уровне модуля;
- Built-in — встроенные имена (
print,len, ...).
name = "Tom" # глобально
def say_hi() -> None:
print("Hello", name) # читает глобальную
def say_bye() -> None:
name = "Bob" # локальная, скрывает глобальную
print("Good bye", name)
say_hi() # Hello Tom
say_bye() # Good bye Bob
print(name) # Tom — глобальная не изменилась
global и nonlocal¶
Если внутри функции нужно изменить глобальную переменную (а не создать локальную), её помечают global:
Для замыкания (изменения переменной из охватывающей функции, не глобальной) — nonlocal:
def make_counter():
count = 0
def inc() -> int:
nonlocal count
count += 1
return count
return inc
c = make_counter()
print(c()) # 1
print(c()) # 2
Неочевидный момент¶
Python видит x += 1 — присваивание внутри функции, значит x считается локальной на всём протяжении функции. К моменту print(x) локальная ещё не определена. Лечится global x или nonlocal x.
Глобальные переменные — это плохо¶
Глобальные переменные:
- усложняют тестирование (тесты влияют друг на друга);
- затрудняют рефакторинг (любая функция может что-то изменить);
- ломают многопоточность (race conditions).
Допустимо хранить в глобальной области только константы:
Общее состояние модулей лучше держать в отдельном config.py и обращаться через config.x.
Scope в Go¶
Go проще: scope определяется блоком { ... }. Внутри блока видны все имена, объявленные раньше; вне блока — нет.
package main
import "fmt"
var counter = 0 // package-level (≈ глобальная)
func inc() {
counter++ // прямо работает
}
func main() {
x := 10
if x > 5 {
y := 20 // видна только в if
fmt.Println(x, y)
}
// fmt.Println(y) // ошибка компиляции: y undefined
}
Имена с заглавной буквы экспортируются из пакета, с маленькой — приватные.
Анонимные функции и замыкания¶
Python: lambda¶
lambda — однострочная анонимная функция. Полезна там, где нужна короткая функция-аргумент:
add = lambda x, y: x + y
print(add(2, 3)) # 5
# Типичный сценарий: ключ сортировки
people = [("Аня", 30), ("Боря", 25)]
people.sort(key=lambda p: p[1])
Ограничения: только одно выражение, без statement'ов. Для всего сложнее — обычный def.
Замыкания¶
def adder(n: int):
def add(x: int) -> int:
return x + n
return add
inc = adder(1)
add5 = adder(5)
print(inc(10)) # 11
print(add5(10)) # 15
add «помнит» внешнее n после возврата adder — это и есть замыкание.
Go: анонимные функции¶
add := func(x, y int) int { return x + y }
fmt.Println(add(2, 3)) // 5
// Замыкание
adder := func(n int) func(int) int {
return func(x int) int { return x + n }
}
inc := adder(1)
fmt.Println(inc(10)) // 11
В Go анонимная функция — обычное выражение, можно сразу вызвать:
Передача по ссылке или по значению¶
В C/Pascal параметры по умолчанию передаются по значению — создаётся копия. Если нужно изменить переменную вызывающего — передают указатель (по ссылке).
Python: всё передаётся по ссылке на объект¶
Точнее — всё передаётся по значению ссылки на объект. Поведение зависит от изменяемости объекта:
- неизменяемые (
int,float,str,tuple,frozenset) — внутри функции «выглядят» как переданные по значению; присваивание создаёт новый объект; - изменяемые (
list,dict,set) — функция видит тот же объект и может его изменить.
def append_value(items: list[int]) -> None:
items.append(99)
a = [1, 2, 3]
append_value(a)
print(a) # [1, 2, 3, 99] ← внешний список изменился
Лечится копией:
new_items = items[:] # копия списка
new_items = list(items) # тоже копия
new_items = items.copy() # тоже
import copy
new_items = copy.deepcopy(items) # глубокая копия
Go: всё передаётся по значению¶
В Go аргументы всегда копируются. Если нужно изменить переменную вызывающего — передают указатель:
func increment(x int) {
x++ // ничего не меняет снаружи
}
func incrementPtr(x *int) {
*x++
}
n := 1
increment(n)
fmt.Println(n) // 1
incrementPtr(&n)
fmt.Println(n) // 2
Слайсы, мапы и каналы в Go — типы со ссылочной семантикой: копируется «дескриптор», но содержимое разделяется:
func appendValue(items []int) {
items = append(items, 99) // ВНИМАНИЕ: может создать новый слайс
}
func mutateFirst(items []int) {
items[0] = 99 // меняет элемент исходного массива
}
Несколько возвращаемых значений¶
В Python — кортеж:
В Go — нативная поддержка multiple return, и это идиоматический способ возвращать ошибки:
func DivMod(a, b int) (int, int) {
return a / b, a % b
}
func ParseConfig(path string) (Config, error) {
...
}
cfg, err := ParseConfig("config.toml")
if err != nil {
return err
}
Что делает функцию «хорошей»¶
«Хорошая» функция (компиляция рекомендаций Боба Мартина и community):
- внятно названа — снаружи понятно, что делает;
- одна ответственность — делает одно дело;
- возвращает значение — даже
True/Falseлучше, чемNone; - короткая — около 50 строк максимум;
- идемпотентная — при одинаковых аргументах возвращает одинаковый результат;
- чистая (если возможно) — без побочных эффектов.
Один принцип ответственности¶
# ПЛОХО — функция делает два дела
def calculate_and_print_stats(values: list[float]) -> None:
total = sum(values)
mean = statistics.mean(values)
median = statistics.median(values)
print(f"SUM: {total}")
print(f"MEAN: {mean}")
print(f"MEDIAN: {median}")
# ХОРОШО — отделили вычисление от вывода
def stats(values: list[float]) -> dict[str, float]:
return {
"sum": sum(values),
"mean": statistics.mean(values),
"median": statistics.median(values),
}
def print_stats(values: dict[str, float]) -> None:
for key, value in values.items():
print(f"{key.upper():<8} {value}")
Слово «and» в имени функции — почти всегда признак нарушения SRP.
Идемпотентность и чистота¶
# Чистая функция — нет побочных эффектов, только вход/выход
def add_three(n: int) -> int:
return n + 3
# Идемпотентная, но не чистая — есть побочный эффект (print)
def add_three_log(n: int) -> int:
print(f"adding 3 to {n}")
return n + 3
# Не идемпотентная — зависит от ввода/состояния
def add_three_input() -> int:
return int(input("число: ")) + 3
Чем больше функций чистые/идемпотентные — тем проще их тестировать (никаких моков, никакой подготовки окружения).
Сравнение Python ↔ Go¶
| Аспект | Python | Go |
|---|---|---|
| Объявление | def name(...) -> Type: |
func Name(...) Type |
| Анонимные | lambda x: x (однострочная) |
func(x int) int { return x } |
| Default-параметры | да | нет (используют functional options) |
*args / **kwargs |
да | variadic ...T, именованных нет |
| Несколько возвращаемых | через tuple | нативно |
| Идиома ошибок | исключения | (value, error) |
| Аннотации типов | опциональны (mypy/pyright) |
обязательны |
| Глобальное состояние | можно, но не нужно | пакет-уровень доступен в пакете |
| Замыкания | поддерживаются | поддерживаются |
Контрольные вопросы¶
- Чем формальные параметры отличаются от фактических?
- Что такое
*argsи**kwargsв Python и какие у Go аналоги? - Почему изменяемый default-аргумент (
def f(x=[]):) — это ловушка? - В чём разница между ключевыми словами
globalиnonlocal? - Почему
print(x); x += 1внутри функции с глобальнойxприводит кUnboundLocalError? - В Python всё передаётся «по ссылке» или «по значению»? Почему ответ — «зависит»?
- Как в Go реализовать functional options и зачем это нужно?
- Что такое идемпотентная и чистая функция? Чем чистая отличается от идемпотентной?
- Какие признаки нарушения принципа единственной ответственности у функции?
- Что произойдёт, если функция в Go не вернёт значение, объявленное в её сигнатуре?