Введение
В этом лабораторном занятии вы узнаете о правилах видимости (scoping rules) в Python и изучите продвинутые методы работы с областью видимости (scope). Понимание области видимости в Python является важным аспектом при написании чистого и поддерживаемого кода, а также помогает избежать непредвиденного поведения программы.
Цели этого лабораторного занятия включают детальное изучение правил видимости в Python, освоение практических методов работы с областью видимости при инициализации классов, реализацию гибкой системы инициализации объектов и применение методов инспекции кадров (frame inspection techniques) для упрощения кода. Вы будете работать с файлами structure.py и stock.py.
Понимание проблемы с инициализацией классов
В мире программирования классы являются фундаментальным понятием, которое позволяет создавать пользовательские типы данных. В предыдущих упражнениях вы, возможно, создали класс Structure. Этот класс представляет собой полезный инструмент для простого определения структур данных. Структура данных - это способ организации и хранения данных таким образом, чтобы к ним можно было эффективно обращаться и использовать. Класс Structure в качестве базового класса занимается инициализацией атрибутов на основе предварительно определенного списка имен полей. Атрибуты - это переменные, принадлежащие объекту, а имена полей - это имена, которые мы даем этим атрибутам.
Давайте более подробно рассмотрим текущую реализацию класса Structure. Для этого нам нужно открыть файл structure.py в редакторе кода. Этот файл содержит код класса Structure. Вот команды для перехода в директорию проекта и открытия файла:
cd ~/project
code structure.py
Класс Structure предоставляет базовую структуру для определения простых структур данных. Когда мы создаем подкласс, например класс Stock, мы можем определить конкретные поля, которые нам нужны для этого подкласса. Подкласс наследует свойства и методы своего базового класса, в данном случае класса Structure. Например, в классе Stock мы определяем поля name, shares и price:
class Stock(Structure):
_fields = ('name', 'shares', 'price')
Теперь давайте откроем файл stock.py, чтобы увидеть, как класс Stock реализован в контексте всего кода. Вероятно, этот файл содержит код, который использует класс Stock и взаимодействует с ним. Используйте следующую команду для открытия файла:
code stock.py
Хотя этот подход с использованием класса Structure и его подклассов работает, он имеет несколько ограничений. Чтобы выявить эти проблемы, мы запустим интерпретатор Python и исследуем, как ведет себя класс Stock. Следующая команда импортирует класс Stock и отображает его справочную информацию:
python3 -c "from stock import Stock; help(Stock)"
Когда вы запустите эту команду, вы заметите, что сигнатура, показанная в выходных данных справки, не очень информативна. Вместо отображения реальных имен параметров, таких как name, shares и price, она показывает только *args. Этот недостаток четких имен параметров делает сложным для пользователей понять, как правильно создать экземпляр класса Stock.
Давайте также попробуем создать экземпляр класса Stock с использованием именованных аргументов (keyword arguments). Именованные аргументы позволяют вам указывать значения параметров по их именам, что может сделать код более читаемым. Запустите следующую команду:
python3 -c "from stock import Stock; s = Stock(name='GOOG', shares=100, price=490.1); print(s)"
Вы должны получить сообщение об ошибке, похожее на следующее:
TypeError: __init__() got an unexpected keyword argument 'name'
Эта ошибка возникает потому, что наш текущий метод __init__, который отвечает за инициализацию объектов класса Stock, не обрабатывает именованные аргументы. Он принимает только позиционные аргументы, что означает, что вы должны предоставлять значения в определенном порядке, не используя имена параметров. Это ограничение, которое мы хотим исправить в этом лабораторном занятии.
В этом лабораторном занятии мы рассмотрим различные подходы, чтобы сделать наш класс Structure более гибким и удобным для пользователя. Таким образом, мы сможем улучшить удобство использования класса Stock и других подклассов класса Structure.
Использование функции locals() для доступа к аргументам функции
В Python понимание областей видимости переменных является крайне важным. Область видимости переменной определяет, где в коде можно к ней обращаться. Python предоставляет встроенную функцию locals(), которая очень полезна для начинающих программистов, чтобы понять области видимости. Функция locals() возвращает словарь, содержащий все локальные переменные в текущей области видимости. Это может быть чрезвычайно полезно, когда вы хотите проверить аргументы функции, так как оно дает вам ясное представление о том, какие переменные доступны в определенной части вашего кода.
Давайте проведем простой эксперимент в интерпретаторе Python, чтобы увидеть, как это работает. Сначала нам нужно перейти в директорию проекта и запустить интерпретатор Python. Вы можете сделать это, запустив следующие команды в терминале:
cd ~/project
python3
Как только вы окажетесь в интерактивной оболочке Python, мы определим класс Stock. Класс в Python представляет собой чертеж для создания объектов. В этом классе мы будем использовать специальный метод __init__. Метод __init__ является конструктором в Python, то есть он автоматически вызывается при создании объекта класса. Внутри этого метода __init__ мы будем использовать функцию locals() для вывода всех локальных переменных.
class Stock:
def __init__(self, name, shares, price):
print(locals())
Теперь давайте создадим экземпляр этого класса Stock. Экземпляр - это фактический объект, созданный на основе чертежа класса. Мы передадим некоторые значения для параметров name, shares и price.
s = Stock('GOOG', 100, 490.1)
Когда вы запустите этот код, вы должны увидеть вывод, похожий на следующий:
{'self': <__main__.Stock object at 0x...>, 'name': 'GOOG', 'shares': 100, 'price': 490.1}
Этот вывод показывает, что функция locals() возвращает словарь, содержащий все локальные переменные в методе __init__. Ссылка self является специальной переменной в классах Python, которая ссылается на экземпляр класса. Другие переменные - это значения параметров, которые мы передали при создании объекта Stock.
Мы можем использовать функциональность locals() для автоматической инициализации атрибутов объекта. Атрибуты - это переменные, связанные с объектом. Давайте определим вспомогательную функцию и изменим наш класс Stock.
def _init(locs):
self = locs.pop('self')
for name, val in locs.items():
setattr(self, name, val)
class Stock:
def __init__(self, name, shares, price):
_init(locals())
Функция _init принимает словарь локальных переменных, полученный с помощью locals(). Сначала она удаляет ссылку self из словаря с помощью метода pop. Затем она проходит по оставшимся парам ключ - значение в словаре и использует функцию setattr для установки каждой переменной в качестве атрибута объекта.
Теперь давайте протестируем эту реализацию как с позиционными, так и с именованными аргументами. Позиционные аргументы передаются в том порядке, в котором они определены в сигнатуре функции, в то время как именованные аргументы передаются с указанием имен параметров.
## Test with positional arguments
s1 = Stock('GOOG', 100, 490.1)
print(s1.name, s1.shares, s1.price)
## Test with keyword arguments
s2 = Stock(name='AAPL', shares=50, price=125.3)
print(s2.name, s2.shares, s2.price)
Теперь оба подхода должны работать! Функция _init позволяет нам без проблем обрабатывать как позиционные, так и именованные аргументы. Она также сохраняет имена параметров в сигнатуре функции, что делает вывод функции help() более полезным. Функция help() в Python предоставляет информацию о функциях, классах и модулях, и сохранение имен параметров делает эту информацию более осмысленной.
Когда вы закончите экспериментировать, вы можете выйти из интерпретатора Python, запустив следующую команду:
exit()
Исследование инспекции стека вызовов
Подход _init(locals()), который мы использовали, работает, но имеет недостаток. Каждый раз, когда мы определяем метод __init__, мы должны явно вызывать locals(). Это может стать немного утомительным, особенно при работе с несколькими классами. К счастью, мы можем сделать наш код более чистым и эффективным, используя инспекцию стека вызовов. Эта техника позволяет нам автоматически получить доступ к локальным переменным вызывающего кода без необходимости явного вызова locals().
Давайте начнем изучать эту технику в интерпретаторе Python. Сначала откройте терминал и перейдите в директорию проекта. Затем запустите интерпретатор Python. Вы можете сделать это, запустив следующие команды:
cd ~/project
python3
Теперь, когда мы находимся в интерпретаторе Python, нам нужно импортировать модуль sys. Модуль sys предоставляет доступ к некоторым переменным, используемым или поддерживаемым интерпретатором Python. Мы будем использовать его для доступа к информации о стеке вызовов.
import sys
Далее мы определим улучшенную версию нашей функции _init(). Эта новая версия будет напрямую получать доступ к кадру (frame) вызывающего кода, устраняя необходимость явно передавать locals().
def _init():
## Get the caller's frame (1 level up in the call stack)
frame = sys._getframe(1)
## Get the local variables from that frame
locs = frame.f_locals
## Extract self and set other variables as attributes
self = locs.pop('self')
for name, val in locs.items():
setattr(self, name, val)
В этом коде sys._getframe(1) извлекает объект кадра (frame) вызывающей функции. Аргумент 1 означает, что мы ищем на один уровень выше в стеке вызовов. Как только у нас есть объект кадра, мы можем получить доступ к его локальным переменным с помощью frame.f_locals. Это дает нам словарь всех локальных переменных в области видимости вызывающего кода. Затем мы извлекаем переменную self и устанавливаем оставшиеся переменные в качестве атрибутов объекта self.
Теперь давайте протестируем эту новую функцию _init() с новой версией нашего класса Stock.
class Stock:
def __init__(self, name, shares, price):
_init() ## No need to pass locals() anymore!
## Test it
s = Stock('GOOG', 100, 490.1)
print(s.name, s.shares, s.price)
## Also works with keyword arguments
s = Stock(name='AAPL', shares=50, price=125.3)
print(s.name, s.shares, s.price)
Как вы можете видеть, метод __init__ больше не нуждается в явном передаче locals(). Это делает наш код более чистым и легким для чтения с точки зрения вызывающего кода.
Как работает инспекция стека вызовов
Когда вы вызываете sys._getframe(1), Python возвращает объект кадра (frame), представляющий выполнение вызывающего кода. Аргумент 1 означает "на один уровень выше от текущего кадра" (вызывающей функции).
Объект кадра содержит важную информацию о контексте выполнения. Это включает в себя текущую выполняемую функцию, локальные переменные в этой функции и номер текущей выполняемой строки.
Путем доступа к frame.f_locals мы получаем словарь всех локальных переменных в области видимости вызывающего кода. Это похоже на то, что вернет locals(), если его вызвать напрямую из этой области видимости.
Эта техника очень мощная, но ее следует использовать с осторожностью. Она обычно считается продвинутой возможностью Python и может показаться немного "магической", так как она выходит за обычные границы области видимости Python.
После того, как вы закончите экспериментировать с инспекцией стека вызовов, вы можете выйти из интерпретатора Python, запустив следующую команду:
exit()
Реализация продвинутой инициализации в структуре
Мы только что узнали две мощные техники для доступа к аргументам функции. Теперь мы используем эти техники для обновления нашего класса Structure. Сначала разберемся, почему мы это делаем. Эти техники сделают наш класс более гибким и легким в использовании, особенно при работе с разными типами аргументов.
Откройте файл structure.py в редакторе кода. Вы можете сделать это, запустив следующие команды в терминале. Команда cd изменяет текущую директорию на папку проекта, а команда code открывает файл structure.py в редакторе кода.
cd ~/project
code structure.py
Замените содержимое файла следующим кодом. Этот код определяет класс Structure с несколькими методами. Давайте разберем каждую часть, чтобы понять, что она делает.
import sys
class Structure:
_fields = ()
@staticmethod
def _init():
## Get the caller's frame (the __init__ method that called this)
frame = sys._getframe(1)
## Get the local variables from that frame
locs = frame.f_locals
## Extract self and set other variables as attributes
self = locs.pop('self')
for name, val in locs.items():
setattr(self, name, val)
def __repr__(self):
values = ', '.join(f'{name}={getattr(self, name)!r}' for name in self._fields)
return f'{type(self).__name__}({values})'
def __setattr__(self, name, value):
if name.startswith('_') or name in self._fields:
super().__setattr__(name, value)
else:
raise AttributeError(f'{type(self).__name__!r} has no attribute {name!r}')
Вот что мы сделали в коде:
- Мы удалили старый метод
__init__(). Поскольку подклассы будут определять свои собственные методы__init__, нам больше не нужен старый. - Мы добавили новый статический метод
_init(). Этот метод использует инспекцию стека вызовов (frame inspection), чтобы автоматически захватить и установить все параметры в качестве атрибутов. Инспекция стека вызовов позволяет нам получить доступ к локальным переменным вызывающего метода. - Мы сохранили метод
__repr__(). Этот метод предоставляет удобное строковое представление объекта, которое полезно для отладки и вывода. - Мы добавили метод
__setattr__(). Этот метод обеспечивает валидацию атрибутов, гарантируя, что на объекте можно устанавливать только допустимые атрибуты.
Теперь обновим класс Stock. Откройте файл stock.py с помощью следующей команды:
code stock.py
Замените его содержимое следующим кодом:
from structure import Structure
class Stock(Structure):
_fields = ('name', 'shares', 'price')
def __init__(self, name, shares, price):
self._init() ## This magically captures and sets all parameters!
@property
def cost(self):
return self.shares * self.price
def sell(self, nshares):
self.shares -= nshares
Основное изменение здесь заключается в том, что наш метод __init__ теперь вызывает self._init() вместо ручной установки каждого атрибута. Метод _init() использует инспекцию стека вызовов, чтобы автоматически захватить и установить все параметры в качестве атрибутов. Это делает код более компактным и легким в поддержке.
Протестируем нашу реализацию, запустив модульные тесты. Модульные тесты помогут нам убедиться, что наш код работает как ожидается. Запустите следующие команды в терминале:
cd ~/project
python3 teststock.py
Вы должны увидеть, что все тесты проходят, включая тест для именованных аргументов, который раньше не проходил. Это означает, что наша реализация работает правильно.
Также проверим справочную документацию для нашего класса Stock. Справочная документация предоставляет информацию о классе и его методах. Запустите следующую команду в терминале:
python3 -c "from stock import Stock; help(Stock)"
Теперь вы должны увидеть правильную сигнатуру для метода __init__, показывающую все имена параметров. Это делает проще для других разработчиков понять, как использовать класс.
Наконец, давайте интерактивно проверим, что именованные аргументы работают как ожидается. Запустите следующую команду в терминале:
python3 -c "from stock import Stock; s = Stock(name='GOOG', shares=100, price=490.1); print(s)"
Вы должны увидеть, что объект Stock корректно создан с указанными атрибутами. Это подтверждает, что наша система инициализации классов поддерживает именованные аргументы.
С этой реализацией мы создали гораздо более гибкую и удобную для пользователя систему инициализации классов, которая:
- Сохраняет правильные сигнатуры функций в документации, что делает проще для разработчиков понять, как использовать класс.
- Поддерживает как позиционные, так и именованные аргументы, обеспечивая больше гибкости при создании объектов.
- Требует минимального количества шаблонного кода в подклассах, уменьшая количество кода, которое вам нужно написать.
Резюме
В этом практическом занятии (лабораторной работе) вы узнали о правилах области видимости (scoping rules) в Python и некоторых мощных методах работы с областью видимости. Во - первых, вы изучили, как использовать функцию locals() для доступа ко всем локальным переменным внутри функции. Во - вторых, вы научились инспектировать стек вызовов с помощью sys._getframe() для доступа к локальным переменным вызывающего кода.
Вы также применили эти методы для создания гибкой системы инициализации классов. Эта система автоматически захватывает параметры функции и устанавливает их в качестве атрибутов объекта, сохраняет правильные сигнатуры функций в документации и поддерживает как позиционные, так и именованные аргументы. Эти методы демонстрируют гибкость и возможности интроспекции (introspection) Python. Хотя инспекция стека вызовов является продвинутым методом, который следует использовать с осторожностью, при правильном использовании он может эффективно уменьшить количество шаблонного кода. Понимание правил области видимости и этих продвинутых методов дает вам больше инструментов для написания более чистого и поддерживаемого кода на Python.