Как в библиотеке обеспечить экспорт функций и глобальных переменных

Обновлено: 07.07.2024

Как я могу создать или использовать глобальную переменную в функции?

Если я создаю глобальную переменную в одной функции, как я могу использовать эту глобальную переменную в другой функции? Нужно ли хранить глобальную переменную в локальной переменной функции, которой нужен доступ?

Вы можете использовать глобальную переменную в других функциях, объявив ее как global в каждой функции, которая ей назначается:

Я предполагаю, что причина этого в том, что, поскольку глобальные переменные настолько опасны, Python хочет убедиться, что вы действительно знаете, с чем играете, явно требуя global ключевое слово.

Смотрите другие ответы, если вы хотите поделиться глобальной переменной между модулями.

Я не согласен с тем, что Python требует global ключевое слово потому, что глобальные переменные опасны. Скорее, это потому, что язык не требует, чтобы вы явно объявляли переменные, и автоматически предполагает, что переменная, которую вы назначаете, имеет область действия функции, если вы не укажете иное. global Ключевое слово является средством , которое предоставляется сказать иначе.

@avgvstvs: И если вы реализуете ту же программу без глобальных переменных, у вас все равно будет одинаковое количество путей кода. Вы привели аргумент против глобалов.

@LightnessRacesinOrbit Я не понимаю твою точку зрения. Если вы удаляете глобальную переменную, вы удаляете усложняющий фактор, заключающийся в том, что теперь произвольные функции больше не могут изменять состояние программы в различных точках выполнения - таким образом изменяя выполнение таким образом, который в противном случае будет незаметен для других функций, зависящих от этой переменной. Вам больше не нужно следить за тем, «Изменилось ли f2() состояние, чтобы теперь f3() могло произойти что-то неожиданное? Функции теперь могут работать независимо от состояния внешней программы.

Если я правильно понимаю вашу ситуацию, то, что вы видите, является результатом того, как Python обрабатывает локальные (функциональные) и глобальные (модульные) пространства имен.

Скажем, у вас есть такой модуль:

Вы могли бы ожидать, что это напечатает 42, но вместо этого оно печатает 5. Как уже упоминалось, если вы добавите объявление ' global ' func1() , то вы func2() получите 42.

Здесь происходит то, что Python предполагает, что любое имя, которое присваивается в любом месте функции, является локальным для этой функции, если явно не указано иное. Если это только чтение из имени, а имя не существует локально, оно попытается найти имя в любой содержащей области (например, в глобальной области видимости модуля).

myGlobal Поэтому, когда вы назначаете 42 имени , Python создает локальную переменную, которая скрывает глобальную переменную с тем же именем. Это местный выходит из области видимости и сборки мусора , когда func1() возвращается; Между тем, func2() никогда не вижу ничего, кроме (неизмененного) глобального имени. Обратите внимание, что это решение пространства имен происходит во время компиляции, а не во время выполнения - если бы вы прочитали значение myGlobal inside func1() перед тем, как присваивать ему, вы бы получили UnboundLocalError , потому что Python уже решил, что это должна быть локальная переменная, но это не имеет никакого значения, связанного с этим еще. Но используя global оператор ' ', вы говорите Python, что он должен искать имя в другом месте, а не присваивать ему локально.

(Я полагаю, что такое поведение возникло в основном за счет оптимизации локальных пространств имен - без этого поведения виртуальная машина Python должна была бы выполнять как минимум три поиска имен каждый раз, когда новое имя назначается внутри функции (чтобы гарантировать, что имя не уже существует на уровне модуля / встроенного), что значительно замедлит очень распространенную операцию.)

Вы упомянули, что решение о пространстве имен происходит во время компиляции , я не думаю, что это правда. Из того, что я узнал, компиляция Python проверяет только синтаксическую ошибку, а не ошибку имени, попробуйте этот пример def A (): x + = 1 , если вы его не запускаете, он не выдаст UnboundLocalError , пожалуйста, проверьте спасибо

@watashiSHUN: Решение имен это произойдет во время компиляции. Решение о том, что x локально, отличается от проверки во время выполнения, было ли локальное имя связано со значением до того, как оно использовалось в первый раз.

@Vassilis: Все буквы в верхнем регистре являются общими MY_GLOBAL = 5 . Смотрите руководство по стилю для кода Python .

Вы можете исследовать понятие пространств имен . В Python модуль является естественным местом для глобальных данных:

Каждый модуль имеет свою собственную таблицу частных символов, которая используется в качестве глобальной таблицы символов всеми функциями, определенными в модуле. Таким образом, автор модуля может использовать глобальные переменные в модуле, не беспокоясь о случайных столкновениях с глобальными переменными пользователя. С другой стороны, если вы знаете, что делаете, вы можете прикоснуться к глобальным переменным модуля, используя те же обозначения, что и для его функций modname.itemname .

Конкретное использование global-in-a-module описано здесь - Как мне совместно использовать глобальные переменные между модулями? и для полноты содержания поделитесь здесь:

Канонический способ обмена информацией между модулями в рамках одной программы - создать специальный модуль конфигурации (часто называемый config или cfg ). Просто импортируйте модуль конфигурации во все модули вашего приложения; модуль становится доступным как глобальное имя. Поскольку существует только один экземпляр каждого модуля, любые изменения, внесенные в объект модуля, отражаются повсеместно. Например:

Файл: config.py

Файл: mod.py

Файл: main.py

по причине, по которой мне не нравится, config.x могу ли я от нее избавиться? Я пришел с, x = lambda: config.x а затем у меня есть новое значение в x() . по какой-то причине наличие a = config.x не помогает мне.

Внутренний формат представления динамических библиотек в современных системах аналогичен тому, что используется для исполняемых файлов, что позволяет упростить систему. Библиотеки не предназначены для запуска напрямую. Загружаются в память процесса загрузчиком программ операционной системы либо при создании процесса, либо по запросу уже работающего процесса, то есть динамически.

В UNIX-системах библиотеки имеют расширение so (shared object), в Windows — расширение dll (dynamic link library).

Динамическая загрузка

Динамические библиотеки могут использоваться двумя способами:

  • динамическая компоновка (dynamic linking)
  • динамическая загрузка (dynamic loading)

При динамической загрузке программа сама загружает конкретную библиотеку, указывая путь к ней, затем находит в библиотеке нужную функцию по имени и вызывает её. Это частый паттерн использования в программах, которые поддерживают плагины.

Переадресация (relocation)

Это практически всегда скрыто от C/C++ программиста — очень редко проблемы компоновки вызваны трудностями переадресации.

Технология ASLR создана для усложнения эксплуатации нескольких типов уязвимостей. Например, если при помощи переполнения буфера или другим методом атакующий получит возможность передать управление по произвольному адресу, ему нужно будет угадать, по какому именно адресу расположен стек, куча или другие структуры данных, в которые можно поместить шелл-код. Если не удастся угадать правильный адрес, приложение скорее всего аварийно завершится, тем самым лишив атакующего возможности повторной атаки и привлекая внимание системного администратора.

API и ABI

API: Application Program Interface

Это набор публичных типов, переменных, функций, которые вы делаете видимыми из вашего приложения или библиотеки.

В C и C++ API обычно поставляется в виде заголовочного файла (h) вместе с библиотекой.

С API работают люди, когда пишут код.

ABI: Application Binary Interface

Детали реализации этого интерфейса. Определяет такие вещи, как

  • Способ передачи параметров в функции (регистры, стек).
  • Кто извлекает параметры из стека (вызывающий код или вызываемый, caller/callee).
  • Как происходит возврат значений из функции.
  • Как реализован механизм исключений.
  • Декорирование имён в C++ (mangling).

ABI важно, когда приложение использует внешние библиотеки. Если при обновлении библиотеки ABI не меняется, то менять программу не надо. API может остаться тем же, но поменяется ABI. Две версии библиотеки, имеющие один ABI, называют binary compatible (бинарно совместимыми): старую версию библиотеки можно заменить на новую без проблем.

Иногда без изменений ABI не обойтись. Тогда приходится перекомпилировать зависящие программы. Если ABI библиотеки меняется, а API нет, то версии называют source compatible.

Разработчики библиотеки стараются поддерживать ABI стабильным. Новые функции и типы данных могут добавляться, но старые должны сохраняться.

Linux

Полностью статическая сборка

Рассмотрим такой простейший код:

Даже такая программа использует динамические библиотеки. Вы можете просмотреть библиотеки, используемые приложением, через команду ldd.

Обратите внимание, что адреса меняются — это ASLR.

С помощью ключа -static компилятора gcc можно создать статически скомпонованный исполняемый файл.

Динамический загрузчик ld-linux.so

Когда операционная система загружает приложение, скомпонованное динамически, она должна найти и загрузить динамические библиотеки, необходимые для выполнения программы. В ОС Linux эту работу выполняет ld-linux.so.2.

Когда запускается программа ls, ОС передаёт управление в ld-linux.so.2 вместо нормальной точки входа в приложение. В свою очередь ld-linux.so.2 ищет и загружает требуемые библиотеки, затем передаёт управление на точку старта приложения.

Справочная страница (man) к ld-linux.so.2 даёт высокоуровневое описание работы динамического компоновщика. По сути это рантайм-компонент компоновщика (ld), который отыскивает и загружает в память динамические библиотеки, используемые приложением. Обычно динамический компоновщик неявно задаётся в процессе компоновки. Спецификация ELF предоставляет функциональность динамической компоновки. Компилятор GCC включает в исполняемые файлы специальный заголовок (program header) под названием INTERP, он указывает путь к динамическому компоновщику.

Спецификация гласит, то если присутствует заголовок PT_INTERP, то ОС должна создать образ процесса интерпретатора вместо приложения. Управление передаётся интерпретатору, который отвечает за загрузку динамических библиотек. Спецификация закладывает достаточную гибкость.

linux-vdso.so.1

В те времена, когда процессоры с архитектурой x86 только появились, взаимодействие пользовательских приложений со службами операционной системы осуществлялось с помощью прерываний. По мере создания более мощных процессоров эта схема взаимодействия становилась узким местом системы. Во всех процессорах, начиная с Pentium® II, Intel® реализовала механизм быстрых системных вызовов (Fast System Call), в котором вместо прерываний используются инструкции SYSENTER и SYSEXIT, ускоряющие выполнение системных вызовов.

Библиотека linux-vdso.so.1 является виртуальной библиотекой, или виртуальным динамически разделяемым объектом (VDSO), который размещается только в адресном пространстве отдельной программы. В более ранних системах эта библиотека называлась linux-gate.so.1. Эта виртуальная библиотека содержит всю необходимую логику, обеспечивающую для пользовательских приложений наиболее быстрый доступ к системным функциям в зависимости от архитектуры процессора – либо через прерывания, либо (для большинства современных процессоров) через механизм быстрых системных вызовов.

Система нумерации версий

Во всём UNIX-мире принята система нумерации вида major.minor.patchlevel:

  • Мажорная версия библиотеки изменяется всякий раз, когда у неё меняется ABI.
  • Минорная версия изменяется при добавлении в библиотеку новой функциональности без изменения ABI.
  • Patchlevel изменяется при исправлении ошибок без добавления новой функциональности.

Смена мажорной версии библиотеки -- это всегда событие, переход на неё -- это всегда трудозатраты.

Пример

Пример динамической загрузки

Сборка выполняется так:

Решение проблемы перемещения

Скомпилируем объектный файл:

RIP — регистр instruction pointer, указывает на следующую инструкцию.

Адресация относительно RIP была введена в x86-64 в "длинном" режиме и используется по умолчанию. В старом x86 такая адресация применялась только для инструкций перехода call, jmp, . а теперь стала применяться в гораздо большем числе инструкций.

Когда потом объектый файл статически линкуется в исполняемый файл, нули превращаются в ненули:

Но это статическая линковка, а с динамической сложнее.

Есть два подхода:

  • Load-time relocation
  • Position independent code (PIC)

Load-time relocation

На x86-64 метод не применяется.

  • Замедление на стадии загрузки.
  • text-сегмент получается разным в разных копиях библиотеки, то есть не может разделяться между библиотеками, теряется преимущество экономии памяти.
  • text-сегмент доступен на запись (лишняя угроза безопасности)

Position Independent Code

Пример использования внешней библиотеки

LD_PRELOAD

Windows

Несмотря на то что общие принципы разделяемых библиотек примерно одинаковы как на платформах UNIX, так и на Windows, всё же есть несколько существенных различий.

Экспортируемые символы

Самое большое отличие заключается в том, что в библиотеках Windows символы не экспортируются автоматически. В UNIX все символы всех объектных файлов, которые были подлинкованы к разделяемой библиотеке, видны пользователю этой библиотеки. В Windows программист должен явно делать некоторые символы видимыми, т. е. экспортировать их.

Есть три способа экспортировать символ и Windows DLL (и все эти три способа можно перемешивать в одной и той же библиотеке).

Первый способ

В исходном коде объявить символ как __declspec(dllexport), примерно так:

Второй способ

При выполнении команды компоновщика использовать опцию LINK.EXE export:symbol_to_export

Третий способ

Скормить компоновщику файл определения модуля (DEF) (используя опцию /DEF:def_file), включив в этот файл секцию EXPORT, которая содержит символы, подлежащие экспортированию.

Как приходится иметь дело с C++, первая из этих опций становится самой простой, так как в этом случае компилятор берёт на себя обязательства позаботиться о декорировании имён.

.LIB и другие относящиеся к библиотеке файлы

Мы подошли ко второй трудности, связанной с библиотеками Windows: информация об экспортируемых символах, которые компоновщик должен связать с остальными символам, не содержится в самом DLL. Вместо этого данная информация содержится в соответствующем .LIB файле.

.LIB файл, ассоциированный с DLL, описывает какие (экспортируемые) символы находятся в DLL вместе с их расположением. Любой бинарник, который использует DLL, должен обращаться к .LIB файлу, чтобы связать символы корректно.

Чтобы сделать всё ещё более запутанным, расширение .LIB также используется для статических библиотек.

На самом деле существует целый ряд различных файлов, которые могут относиться каким-либо образом к библиотекам Windows. Наряду с .LIB файлом, а также (опциональным) .DEF файлом Вы можете увидеть все нижеперечисленные файлы, ассоциированные с вашей Windows-библиотекой.

Файлы на выходе компоновки

Файлы на входе компоновки

Это является большим отличием по сравнению с UNIX, где почти вся информация, содержащаяся в этих всех дополнительных файлах, просто добавляется в саму библиотеку.

Импортируемые символы

Вместе с требованием к DLL явно объявлять экспортируемые символы, Windows также разрешает бинарникам, которые используют код библиотеки, явно объявлять символы, подлежащие импортированию. Это не является обязательным, но даёт некоторую оптимизацию по скорости, вызванную историческими свойствами 16-битной Windows.

В LIB-файле для фунции FunctionName генерирутеся "заглушка", которая выглядит как

Здесь __imp__FunctionName является записью в таблице импортированных функций. То есть заглушка считывает адрес из таблицы импортированных адресов (IAT) и выполняет переход на тот код.

За счёт двухступенчатого процесса получается лишняя потеря производительности.

Если писать dllimport, компилятор будет генерировать код для непрямого вызова через таблицу IAT прямо по месту. Это уменьшает число индирекций и позволяет компилятору локально (в вызывающей функции) закешировать целевой адрес. Хотя при наличии LTO это уже не актуально.

Объявляем символ как __declspec(dllimport) в исходном коде примерно так:

При этом индивидуальное объявление функций или глобальных переменных в одном заголовочном файле является хорошим тоном программирования на C. Это приводит к некоторому ребусу: код в DLL, содержащий определение функции/переменной, должен экспортировать символ, но любой другой код, использующий DLL, должен импортировать символ.

Стандартный выход из этой ситуации — это использование макросов препроцессора.

Циклические зависимости

Ещё одной трудностью, связанной с использованием DLL, является тот факт, что Windows относится строже к требованию, что каждый символ должен быть разрешён во время компоновки. В UNIX вполне возможно скомпоновать разделяемую библиотеку, которая содержит неразрешённые символы, т. е. символы, определение которых неведомо компоновщику. В этой ситуации любой другой код, использующий эту разделяемую библиотеку, должен будет предоставить определение незразрешённых символов, иначе программа не будет запущена. Windows не допускает такой распущенности.

Для большинства систем — это не проблема. Выполняемые файлы зависят от высокоуровевых библиотек, высокоуровневые библиотеки зависят от библиотек низкого уровня, и всё компонуется в обратном порядке — сначала библиотеки низкого уровня, потом высокого, а затем и выполняемый файл, который зависит от всех остальных

Однако, если имеет место циклическая зависимость между бинарниками, тогда всё немного усложняется. Если X.DLL нуждается в символе из Y.DLL, а Y.DLL нуждается в символе из X.DLL, тогда необходимо решить задачу про курицу и яйцо: какая бы библиотека ни компоновалась бы первой, она не сможет найти разрешение ко всем символам.

Windows предоставил обходной приём примерно следующего содержания. Сначала имитируем компоновку библиотеки X. Запускаем LIB.EXE (не LINK.EXE), чтобы получить файл X.LIB точно такой же, какой был бы получен с LINK.EXE. При этом X.DLL не будет сгенерирован, но вместо него будет получен файл X.EXP. Компонуем библиотеку Y как обычно, используя X.LIB, полученную на предыдущем шаге, и получаем на выходе как Y.DLL, так и Y.LIB. В конце концов компонуем библиотеку X теперь уже полноценно. Это происходит почти как обычно, используя дополнительно файл X.EXP, полученный на первом шаге. Обычное в этом шаге то, что компоновщик использует Y.LIB и производит X.DLL. Необычное — компоновщик пропускает шаг создания X.LIB, так как этот файл был уже создан на первом шаге, чему свидетельствует наличие .EXP файла.

Но несомненно лучше всё же реорганизовать библиотеки таким образом, чтоб избежать любых циклических зависимостей…

Relocation

DLL в Microsoft Windows используют вариант E8 инструкции CALL (относительный, смещение относительно следующей команды). Эти инструкции не нужно изменять при загрузке DLL.

В Windows Vista и более поздних версиях Windows перемещение DLL и исполняемых файлов выполняется диспетчером памяти ядра, который разделяет relocated-библиотеки между несколькими процессами. Образы всегда перемещаются с их предпочтительных базовых адресов, обеспечивая рандомизацию размещения адресного пространства (ASLR).

Версии Windows до Vista требуют, чтобы системные DLL-файлы были предварительно привязаны к неконфликтным фиксированным адресам во время компоновки, чтобы избежать релокации образов во время выполнения. Перемещение времени выполнения в этих более старых версиях Windows выполняется загрузчиком DLL в контексте каждого процесса, и результирующие перемещенные части каждого изображения больше не могут делиться между процессами.

Динамические библиотеки и язык C++

Поддержка бинарной совместимости

Что делать можно

  • Можно добавить новую функцию, новый невиртуальный метод класса, новый конструктор и новые статические поля.
  • Можно добавлять новые перегрузки к существующим перегруженным функциям, создавать новые перегруженные функции или перегружать private-методы.
  • Можно добавлять новые виртуальные функции в конец класса, не имеющего наследников.
  • Можно изменить тело любой непоинлайненной функции (т. е. не определённой в публичном заголовочном файле).
  • Можно добавлять новые классы.
  • Можно произвольным образом изменять, добавлять и удалять friend declarations.
  • Можно переименовывать private-поля классов.
  • Можно делать всё, что угодно, с классами и функциями, не являющимися частью API (размещёнными в cpp-файле и имеющими static или обёрнутыми в анонимный namespace).

Что делать не стоит

  • Можно изменить тело поинлайненной функции или унести её в .cpp. Однако функциональность менять при этом нельзя (даже исправлять ошибки), поскольку собранные с библиотекой бинарники продолжат пользоваться старой версией функции.
  • Можно переопределить виртуальный метод, определённый в базовом классе. Однако функциональность менять при этом, опять же, нельзя, т. к. в ряде мест компилятор может заменять виртуальный вызов прямым (напр. Derived d; d.foo();).
  • Можно удалить невиртуальный закрытый (private) метод класса (или private static поле). Но перед этим нужно убедиться, что метод никогда за время жизни библиотеки не вызывался (или поле не использовалось) ни из одной inline-функции.
  • Можно изменять значения по умолчанию для функций и методов; однако уже собранные бинарники продолжат передавать старые значения по умолчанию.
  • Можно добавлять перегруженные варианты существующих неперегруженных public-методов. Это сохраняет бинарную совместимость, но код, который брал адрес такой функции (auto x = &MyClass::method) перестанет компилироваться.

Чего делать нельзя

  • Нельзя удалять существующие классы, являющиеся частью API.
  • У существующих классов нельзя удалять открытые, защищённые или виртуальные методы.
  • Нельзя как-либо менять существующую иерархию классов (т. е. изменять, добавлять, удалять или переупорядочивать базовые классы).
  • Нельзя как-либо менять список шаблонных параметров для существующих шаблонных классов и функций.
  • Нельзя менять сигнатуру существующих функций и методов.
  • Нельзя добавлять виртуальные методы в середину класса.
  • Нельзя добавлять виртуальные методы в класс, не имеющий виртуальных функций или виртуального деструктора.
  • Нельзя добавлять виртуальные методы в класс, имеющий наследников.
  • Нельзя добавлять, переупорядочивать или удалять поля существующих классов.

Идиома PImpl (pointer to implementation)

Другое, более общее название — opaque pointer (непрозрачный указатель).

  • реализация скрыта,
  • добавление новых полей в в приватную структуру не ломает бинарную совместимость,
  • ускоряется компиляция.

Минусы также очевидны:

  • усложняется код,
  • замедляются все вызовы за счёт лишнего разыменования указателя.

Похожий принцип можно использовать и на чистом C, эмулируя ООП и инкапсуляцию:


В этой статье мы рассмотрим глобальные и нелокальные переменные в Python и как их использовать, чтобы избежать проблем при написании кода.

Мы начнем с краткого руководства по областям видимости переменных, прежде чем мы расскажем, как и почему использовать глобальные и нелокальные переменные в ваших собственных функциях.

Области видимости в Python

Прежде чем мы сможем начать, мы сначала должны коснуться областей. Для тех из вас, кто не знаком, "область видимости" относится к контексту, в котором определяется переменная и как к ней можно получить доступ или изменить или, более конкретно, откуда она может быть получена.

Ссылаясь на Python прямо сейчас, вы можете сделать вывод из контекста, что я имею в виду язык программирования. Однако в другом контексте Python может быть ссылкой на змею или комедийную группу.

Глобальная и локальная области видимости - это то, как ваша программа понимает контекст переменной, на которую вы ссылаетесь.

Как правило, переменные, определенные в функции или классе (как переменная экземпляра), по умолчанию являются локальными, а переменные вне функций и классов по умолчанию являются глобальными.

Локальные переменные в Python

Поняв это, давайте посмотрим на это в действии. Мы начнем с определения функции с ее собственной локальной переменной внутри. В этой функции у нас есть переменная fruit , которую мы инициализируем как список и печатаем:

Как и ожидалось, этот код выведет нам:

Но что происходит, когда мы перемещаем оператор печати за пределы функции?

Мы получаем ошибку:

В частности, NameError , поскольку fruit был определен локально и поэтому остается ограниченным в этом контексте.

Чтобы наша программа могла понимать переменную глобально (вне функции), нам нужно определить ее глобально.

Глобальные переменные в Python

Что, если вместо первоначального определения нашей переменной внутри функции мы переместим ее наружу и инициализируем там?

В этом случае мы можем ссылаться на нее вне функции, и все работает.

Но если мы попытаемся переопределить переменную fruit внутри shopping_list , эти изменения не будут обновлены до исходной глобальной переменной, а будут изолированы локально:

Это потому, что fruit мы изменили в функции shopping_list() создав новую локальную переменную. Мы создали ее, присвоили ей значение и после этого ничего не сделали. Это фактически полностью избыточный код. print() выводит значение глобальной переменной.

Ключевое слово global

Если мы хотим, чтобы эти изменения отражались в нашей глобальной переменной, вместо того, чтобы создавать новую локальную, все, что нам нужно сделать, это добавить ключевое слово global . Это позволяет нам сообщить, что переменная fruit действительно является глобальной переменной:

И, конечно же, глобальная переменная модифицируется новыми значениями, поэтому, когда мы вызываем print(fruit) , новые значения печатаются:

Определив контекст переменной fruit, которую мы называем глобальной, мы можем затем переопределить и изменить его, насколько нам нравится, зная, что изменения, которые мы вносим в функцию, будут перенесены.

Мы также могли бы определить глобальную переменную в нашей функции и иметь возможность ссылаться на нее и получать к ней доступ в любом другом месте.

Мы могли бы даже объявить глобальную переменную в одной функции и получить к ней доступ в другой, не определяя ее как глобальную во второй:

Осторожность при использовании глобальных переменных

Хотя возможность локально изменять глобальную переменную - это небольшой удобный инструмент, к нему нужно относиться с некоторой осторожностью. Чрезмерное переписывание и переопределение области видимости - это рецепт катастрофы, которая заканчивается ошибками и неожиданным поведением.

Всегда важно убедиться, что вы манипулируете переменной только в том контексте, который вам нужен, а в противном случае, оставив его в покое, это основной движущий принцип принципа инкапсуляции.

Мы быстро рассмотрим пример потенциальной проблемы, прежде чем перейти к некоторым из способов, которыми глобальные переменные могут быть полезны в вашем собственном коде:

Запустив приведенный выше код, мы получим следующий вывод:

В этом примере мы ссылаемся на переменную в обеих функциях first_item() и iterate() . Все вроде работает нормально, если вызвать iterate() и потом first_item() .

Если мы изменим этот порядок или попытаемся повторить его позже, мы столкнемся с большой проблемой:

Теперь это выводит:

А именно, теперь fruit это строка, которая будет повторяться. Что еще хуже, эта ошибка не проявляется, пока, по-видимому, не станет слишком поздно. Первый код вроде бы работал нормально.

Теперь эта проблема очевидна намеренно. Мы напрямую изменили глобальную переменную - и вот, она изменилась. Однако в более сложных структурах можно случайно зайти слишком далеко от модификации глобальной переменной и получить неожиданные результаты.

Ключевое слово nonlocal

То, что вам нужно быть осторожным, не означает, что глобальные переменные также не очень полезны. Глобальные переменные могут быть полезны всякий раз, когда вы хотите обновить переменную, не указывая ее в операторе возврата, например счетчик. Они также очень удобны с вложенными функциями.

Для тех из вас, кто использует Python 3+ , вы можете использовать ключевое слово nonlocal , которое действует аналогично global , но в основном действует при вложении в методы. nonlocal по сути образует промежуточное звено между глобальной и локальной областью.

Поскольку в большинстве наших примеров мы использовали списки покупок и фрукты, мы могли бы подумать о функции оформления заказа, которая суммирует сумму покупок:

Запустив приведенный выше код, мы получим результат:

Таким образом, глобальная переменная count по-прежнему является локальной для внешней функции и не влияет (или не существует) на более высоком уровне. Это дает вам некоторую свободу в добавлении модификаторов к вашим функциям.

Вы всегда можете подтвердить это, попробовав распечатать pct_off не с помощью метода подсчета покупок:

Если бы мы использовали ключевое слово global вместо nonlocal , печать привела бы к pct_off :

Заключение

В конце концов, global (nonlocal) ключевые слова - это инструмент, и при правильном использовании они могут открыть множество возможностей для вашего кода. Лично я довольно часто использую оба этих ключевых слова в своем собственном коде, и при достаточной практике вы поймете, насколько мощными и полезными они могут быть.

Группа функций, предназначенных для работы с глобальными переменными.

Не следует путать глобальные переменные клиентского терминала с переменными, объявленными на глобальном уровне MQL4-программы.

Глобальные переменные существуют в клиентском терминале 4 недели с момента последнего обращения, после этого автоматически уничтожаются. Обращением к глобальной переменной считается не только установка нового значения, но и чтение значения глобальной переменной.

Глобальные переменные клиентского терминала доступны одновременно из всех MQL4-программ, запущенных на клиентском терминале.

GlobalVariableCheck

bool GlobalVariableCheck(string name)

Возвращает значение TRUE, если глобальная переменная существует, иначе возвращает FALSE.
Чтобы получить информацию об ошибке, необходимо вызвать функцию GetLastError().

Параметры:
name — Имя глобальной переменной.

Пример:

GlobalVariableDel

bool GlobalVariableDel(string name)

Удаляет глобальную переменную. При успешном удалении функция возвращает TRUE, иначе FALSE. Чтобы получить информацию об ошибке, необходимо вызвать функцию GetLastError().

Параметры:

name — Имя глобальной переменной.

Пример:

GlobalVariableGet

double GlobalVariableGet(string name)

Возвращает значение существующей глобальной переменной или 0 в случае ошибки. Чтобы получить информацию об ошибке, необходимо вызвать функцию GetLastError().

Параметры:

name — Имя глобальной переменной.

Пример:

GlobalVariableName

string GlobalVariableName(int index)

Функция возвращает имя глобальной переменной по порядковому номеру в списке глобальных переменных. Чтобы получить информацию об ошибке необходимо вызвать функцию функцию GetLastError().

Параметры:

index — Порядковый номер в списке глобальных переменных. Должен быть большим или равным 0 и меньшим, чем GlobalVariablesTotal().

Пример:

GlobalVariableSet

datetime GlobalVariableSet(string name, double value)

Устанавливает новое значение глобальной переменной. Если переменная не существует, то система создает новую глобальную переменную. При успешном выполнении функция возвращает время последнего доступа, иначе 0. Чтобы получить информацию об ошибке, необходимо вызвать функцию GetLastError().

Параметры:

name — Имя глобальной переменной.
value — Новое числовое значение.

Пример:

GlobalVariableSetOnCondition

bool GlobalVariableSetOnCondition(string name, double value, double check_value)

Устанавливает новое значение существующей глобальной переменной, если текущее значение переменной равно значению третьего параметра check_value. Если переменной не существует, функция сгенерирует ошибку ERR_GLOBAL_VARIABLE_NOT_FOUND (4058) и вернет FALSE. При успешном выполнении функция возвращает TRUE, иначе FALSE. Для того, чтобы получить информацию об ошибке, необходимо вызвать функцию GetLastError(). Если текущее значение глобальной переменной отличается от check_value, функция вернет FALSE.
Функция обеспечивает атомарный доступ к глобальной переменной, поэтому она может быть использована для организации семафора при взаимодействии нескольких одновременно работающих экспертов в пределах одного клиентского терминала.

Параметры:

name — Имя глобальной переменной.
value — Новое значение.
check_value — Значение для проверки текущего значения глобальной переменной.

Пример:

GlobalVariablesDeleteAll

int GlobalVariablesDeleteAll(string prefix_name=NULL)

Удаляет глобальные переменные. Если префикс для имени не задан, то удаляются все глобальные переменные. В противном случае удаляются только те переменные, имена которых начинаются на указанный префикс. Функция возвращает количество удаленных переменных.

Параметры:

prefix_name — Префикс имени удаляемых глобальных переменных.

Читайте также: