Область действия/переменной (передача по значению или ссылке?)
спросил
Изменено 6 лет назад
Просмотрено 54к раз
Я полностью запутался в области видимости переменных Lua и передаче аргументов функции (значение или ссылка).
См. код ниже:
локальный a = 9 -- так как он определяет локальный, не должен иметь область действия. local t = {4,6} -- так как он определяет локальный, не должен иметь область действия функция мода (а) a = 10 -- создает глобальную переменную? конец функция modt(t) t[1] = 7 -- создать глобальную переменную? т[2] = 8 конец мода (а) модт (т) print(a) -- вывести 9 (функция не изменяет родительскую переменную) print(t[1]..t[2]) -- print 78 (как modt модифицирует родительскую переменную t)
Такое поведение меня совершенно смущает.
- переменные
- область действия
- lua
Вы правильно догадались, табличные переменные передаются по ссылке. Ссылка на справочное руководство Lua 5.1:
В Lua есть восемь основных типов: nil, boolean, число, строка, функция, пользовательские данные, поток и таблица. ….
Таблицы, функции, потоки и (полные) значения пользовательских данных являются объектами: переменные фактически не содержат этих значений, только ссылки на них. Присваивание, передача параметров и возврат функций всегда манипулируют ссылками на такие значения; эти операции не предполагают никакого копирования.
Таким образом, nil, логические значения, числа и строки передаются по значению. Это точно объясняет поведение, которое вы наблюдаете.
2
Lua function
, table
userdata
и thread
(coroutine) типы передаются по ссылке. Остальные типы передаются по значению. Или, как любят выражаться некоторые люди; все типы передаются по значению, но функция
, таблица
, userdata
и thread
являются ссылочными типами. строка
также является своего рода ссылочным типом, но неизменяемым, интернированным и копируемым при записи — он ведет себя как тип значения, но с лучшей производительностью.
Вот что происходит:
местный a = 9 местный t = {4,6} функция мода (а) a = 10 -- устанавливает 'a', который является локальным, представленным в списке параметров конец функция modt(t) t[1] = 7 -- изменяет таблицу, на которую ссылается локальная буква 't', представленная в списке параметров. т[2] = 8 конец
Возможно, это поможет понять, почему все так, а не иначе:
местный a = 9 местный t = {4,6} функция мода() a = 10 -- изменяет значение 'a' конец функция модт() t[1] = 7 -- изменяет таблицу, на которую ссылается повышающее значение 't' т[2] = 8 конец -- 'moda' и 'modt' являются замыканиями, уже содержащими 'a' и 't', -- поэтому нам не нужно передавать какие-либо параметры для изменения этих переменных мода() модт() print(a) -- теперь напечатайте 10 print(t[1]. .t[2]) -- по-прежнему печатать 78
jA_cOp прав, когда говорит, что «все типы передаются по значению, но функция, таблица, пользовательские данные и поток являются ссылочными типами».
Разница между этим и «таблицы передаются по ссылке» очень важна.
В данном случае без разницы,
функция modt_1(x) x.foo = "бар" конец
Результат: как «передача таблицы по ссылке», так и «передача таблицы по значению, но таблица является ссылочным типом» будут выполнять одно и то же: x теперь имеет поле foo, установленное на «bar».
Но для этой функции это имеет огромное значение
функция modt_2(x) х = {} конец
В этом случае передача по ссылке приведет к замене аргумента на пустую таблицу. Однако в «передавать по значению, но это ссылочный тип» новая таблица будет локально привязана к x, а аргумент останется неизменным. Если вы попробуете это в lua, вы обнаружите, что это второе (значения являются ссылками).
1
Я не буду повторять то, что уже было сказано в ответах Баса Боссинка и jA_cOp о ссылочных типах, но:
— так как он определен локально, не должен иметь область действия
Это неверно. Переменные в Lua имеют лексическую область видимости, то есть они определены в блоке кода и во всех его вложенных блоках.
local
, так это создает новую переменную, ограниченную блоком, в котором находится оператор, блоком, являющимся либо телом функции, либо «уровнем отступа», либо файлом.Это означает, что всякий раз, когда вы делаете ссылку на переменную, Lua будет «сканировать вверх», пока не найдет блок кода, в котором эта переменная объявлена локальной, по умолчанию используется глобальная область видимости, если такого объявления нет.
В этом случае a
и t
объявляются локальными, но объявление находится в глобальной области действия, поэтому a
и t
являются глобальными; или, самое большее, они являются локальными для текущего файла.
После этого они не передекларируются локальные
внутри функций, но они объявлены как параметры, что имеет тот же эффект.
На lua-users.org есть учебник Scope Tutorial с некоторыми примерами, которые могут помочь вам больше, чем моя попытка объяснения. Программирование в разделе Lua на эту тему также является хорошим чтением.
Означает ли это, что табличные переменные передаются в функцию по ссылке, а не по значению?
Да.
Как создание глобальной переменной конфликтует с уже определенной локальной переменной?
Это не так. Это может выглядеть так, потому что у вас есть глобальная переменная с именем t
и вы передаете ее функции с аргументом t
, но два t
разные. Если вы переименуете аргумент во что-то другое, например, q
, вывод будет точно таким же.
может изменять глобальную переменную t
только потому, что вы передаете его по ссылке. Например, если вы вызываете modt({})
, глобальное значение t
не изменится.
Почему modt может изменять таблицу, а moda не может изменять переменную a?
Поскольку аргументы являются локальными. Именование вашего аргумента в
аналогично объявлению локальной переменной с local a
, за исключением того, что, очевидно, аргумент получает переданное значение, а обычная локальная переменная — нет. Если ваш аргумент назывался z
(или вообще не присутствовал), то мода
действительно изменил бы глобальный на
.
Зарегистрируйтесь или войдите в систему
Зарегистрируйтесь с помощью GoogleЗарегистрироваться через Facebook
Зарегистрируйтесь, используя адрес электронной почты и пароль
Опубликовать как гость
Электронная почта
Обязательно, но не отображается
Опубликовать как гость
Электронная почта
Требуется, но не отображается
День 13: Область действия и возвращаемые значения из функций
Здравствуйте, и добро пожаловать на 13 день серии 30 Days of Python! Сегодня мы начнем изучать очень важную тему прицел .
Мы можем рассматривать область действия как описание того, где в нашем приложении можно сослаться на заданное имя. Понимание того, как это работает в Python, является жизненно важным шагом в нашем учебном путешествии.
Мы также собираемся расширить возможности использования наших функций. До сих пор все наши функции просто что-то выводили на консоль, но бывают случаи, когда мы хотим получить что-то и от наших функций. О том, как это сделать, мы и поговорим в сегодняшней публикации.
Демонстрация прицела
Как я уже упоминал, область действия — это понятие, описывающее, где в нашем приложении можно сослаться на заданное имя.
На самом деле мы не сталкивались ни с одним случаем, когда определенное нами имя еще не было доступно, поэтому давайте рассмотрим пример.
Во-первых, мы собираемся определить простую функцию с именем Greeting
.
приветствие по умолчанию (имя): приветствие = f"Привет, {имя}!" распечатать (приветствие)
Все приветствие
берет имя и печатает приветствие пользователю. Внутри приветствие
мы определили переменную с именем приветствие
, где мы назначаем нашу форматированную строку, и мы передаем эту строку приветствия
в print
в качестве аргумента.
Вопрос в том, можем ли мы получить доступ к этой строке приветствия
вне функции?
приветствие по умолчанию (имя): приветствие = f"Привет, {имя}!" распечатать (приветствие) распечатать (приветствие)
В этом случае похоже, что мы получаем ошибку:
Трассировка (последний последний вызов): Файл «main.py», строка 6, враспечатать (приветствие) NameError: имя «приветствие» не определено
Но, возможно, это не совсем удачный пример. В конце концов, мы никогда не вызывали
, поэтому код, в котором определено приветствие
, никогда не запускался. Как это могло быть? Мы даже не сказали ему, что означает имя
.
Возможно, это сработает после того, как мы вызовем , приветствуем
.
приветствие по умолчанию (имя): приветствие = f"Привет, {имя}!" распечатать (приветствие) приветствовать("Фил") распечатать (приветствие)
Теперь наш вывод выглядит так:
Привет, Фил! Traceback (последний последний вызов): Файл «main.py», строка 7, враспечатать (приветствие) NameError: имя «приветствие» не определено
Как видим, "Привет, Фил!"
печатается, когда вызывается приветствие
, но все же приветствие
не определено. В чем дело?
Дело в том, что приветствие
«выходит за рамки».
Пространства имен
Чтобы лучше понять, что происходит в приведенном выше примере, нам нужно подумать о том, что происходит, когда мы определяем переменную.
Python фактически хранит записи определенных нами переменных и значений, связанных с этими именами. Мы называем эту запись пространством имен, и вы можете думать о ней как о словаре. Мы можем немного взглянуть на этот словарь, вызвав функцию globals
и распечатав результат.
Во-первых, давайте посмотрим, что происходит, когда мы вызываем глобальную переменную
в пустом файле.
печать(глобальные())
Если вы запустите этот код, вы можете удивиться, обнаружив, что этот словарь не пуст. У нас есть все виды интересных вещей в нем.
{ '__name__': '__main__', '__doc__': Нет, '__package__': нет, '__loader__': объект <_frozen_importlib_external.SourceFileLoader по адресу 0x7f2b30020bb0>, '__spec__': нет, '__аннотации__': {}, '__builtins__': <модуль 'builtins' (встроенный)>, '__file__': 'main.py', '__cached__': нет }
Хотя в этом есть смысл. В конце концов, нам не нужно определять каждое имя, которое мы используем в Python. Мы никогда не определяли print
, input
или len
, например.
Не беспокойтесь сейчас о фактическом содержании этого словаря. Просто обратите внимание на то, что уже существует, чтобы мы могли видеть, что происходит, когда мы определяем некоторые собственные имена.
Давайте изменим наш файл так, чтобы в нем действительно было какое-то содержимое. Я собираюсь определить пару переменных и функцию, чтобы у нас было немного разнообразия:
имён = ["Майк", "Фиона", "Патрик"] х = 53657 определение добавить (а, б): напечатать (а, б) печать (глобальные значения ())
Теперь давайте посмотрим, что у нас есть в нашем словаре globals
. Обратите особое внимание на последние три пункта.
{ '__name__': '__main__', '__doc__': Нет, '__package__': нет, '__loader__': объект <_frozen_importlib_external.SourceFileLoader по адресу 0x7fae511ffbb0>, '__spec__': нет, '__аннотации__': {}, '__builtins__': <модуль 'builtins' (встроенный)>, '__file__': 'main.py', '__cached__': нет, 'имена': ['Майк', 'Фиона', 'Патрик'], х: 53657, 'добавить': <добавить функцию по адресу 0x7fae512021f0> }
Мы видим, что имена, которые мы определили ( имена
, x
и добавить
), были записаны как ключи, и с каждым из наших имен связано значение, которое мы присвоили этому имени.
add
выглядит немного странно, но этот <добавление функции по адресу 0x7fae512021f0>
— просто представление функции, которую мы определили. Мы видим, что это функция, ее имя — добавить
, а последнее число — это адрес памяти для этой функции.
Когда мы используем имя в нашем приложении, Python просто просматривает пространство имен, чтобы убедиться, что оно определено. Если это так, он может просто ссылаться на значение, связанное с этим именем. Если он не может найти запрошенное имя, Python говорит, что переменная не определена.
Функции и пространства имен
Оглядываясь назад на наши выходные данные globals
, можно заметить, что пара имен явно отсутствует: параметры a
и b
, которые мы определили для , добавляют
. Возможно, вы ожидали, что они будут там наряду с другими именами, которые мы определили. Что случилось с теми?
Python на самом деле имеет более одного пространства имен. Здесь мы рассматриваем только глобальное пространство имен , но параметры функции не являются частью этого глобального пространства имен.
Когда мы вызываем функцию, Python создает новое пространство имен. Другими словами, он создает новый словарь для хранения любых имен, которые мы хотим использовать при выполнении этой функции. Как только функция завершит работу, это пространство имен будет уничтожено, так что когда мы запустим функцию в следующий раз, мы будем работать с чистым листом.
Точно так же, как когда мы использовали globals
, мы можем заглянуть в пространство имен этой функции, вызвав locals
. Давайте внесем небольшое изменение в , добавим
, чтобы увидеть это:
по умолчанию добавить (а, б): печать (местные жители ()) напечатать (а, б) добавить(7, 25)
В этом случае мы получаем следующий вывод:
{'а': 7, 'б': 25} 7 25
Первая строка представляет собой словарь locals
и содержит имена, расположенные в пространстве имен для вызова этой функции.
Теперь мы можем лучше объяснить, что происходит в примере приветствия
из предыдущего поста.
приветствие по умолчанию (имя): приветствие = f"Привет, {имя}!" распечатать (приветствие)
Глобальное пространство имен не содержит имя, приветствие
, поэтому, когда мы пытаемся сослаться на него вне функции, Python не может его найти. Однако внутри welcome
мы работаем со вторым локальным пространством имен, которое было создано при вызове функции. Это пространство имен содержит приветствие
, потому что мы добавили его, когда определили приветствие
в теле функции.
Мы можем увидеть это более подробно, выполнив что-то вроде этого:
приветствие по умолчанию (имя): печать (местные жители ()) приветствие = f"Привет, {имя}!" печать (местные жители ()) распечатать (приветствие) приветствовать("Фил")
Если мы запустим этот код, мы увидим следующее:
{'имя': 'Фил'} {'имя': 'Фил', 'приветствие': 'Привет, Фил!'} Привет, Фил!
Мы видим, что когда мы входим в тело функции для этого вызова функции, у нас есть только имя
определено. Затем мы переходим ко второй строке, где приветствие
получает результат нашей интерполяции строк. Когда мы снова напечатаем locals()
в третьей строке тела функции, мы увидим, что приветствие
было добавлено в это локальное пространство имен для вызова функции.
Я думаю, что это интуитивно понятно. У функции есть некоторые закрытые переменные, о которых знает только она, и у нас есть таблица с именами и значениями переменных, которую мы создаем, когда запускаем функцию, чтобы отслеживать, что происходит внутри. Когда мы закончим с функцией, мы очистим таблицу, потому что нам больше не нужны эти значения.
Получение значений из функции
Поскольку все переменные, которые мы определяем в наших функциях, существуют только внутри наших функций, вопрос в том, как нам получить обратно значение из функции? Например, функция ввода
может предоставить нам ответ пользователя на наше приглашение. Мы хотим иметь возможность сделать что-то подобное.
Мы получаем значение из функции с помощью оператора return
.
Оператор return
фактически имеет две роли. Во-первых, он используется для завершения выполнения функции. Когда Python встречает возвращает ключевое слово
, это немедленно завершает работу функции.
Например, если мы напишем что-то вроде этого:
определение my_func(): возвращаться print("Эта строка никогда не будет запущена")
Мы никогда не будем запускать вызов print
во второй строке тела функции. Ключевое слово return
приведет к завершению функции до того, как мы достигнем этой строки с вызовом print
.
Мы также можем поставить выражение сразу после возвращает ключевое слово
, и именно так мы получаем значения из функции. Мы помещаем значения, которые хотим получить, в ту же строку, что и ключевое слово return
.
Например, давайте напишем новую версию нашей функции add
из вчерашних упражнений. Вместо того, чтобы печатать результат, мы собираемся вернуть результат.
по умолчанию добавить (а, б): вернуть а + б
Если мы запустим эту функцию, ничего не произойдет. В конце концов, мы больше ничего не печатаем. Однако теперь мы можем присвоить возвращаемое значение переменной, если захотим.
по умолчанию добавить (а, б): вернуть а + б результат = добавить (5, 12) печать (результат) # 17
Итак, что же здесь происходит?
Помните, что вызов функции — это выражение. Он оценивается как некоторое значение. Какое значение? Вызов функции оценивается как значение, которое было возвращено функцией при ее вызове.
В приведенном выше примере мы вернули результат выражения a + b
. Так как a
был 5
и b
был 12
результатом этого выражения было целое число 17
. Это то, что было возвращено функцией, и это значение, которое оценивается вызовом функции.
Кстати, поскольку add(5, 12)
— это просто выражение, мы могли бы напрямую передать вызов функции print
:
по умолчанию добавить (а, б): вернуть а + б напечатать (добавить (5, 12)) # 17
Но что делать, если у нас нет оператора return
? Если мы не укажем возвращаемое значение, Python неявно возвращает None
для нас.
Например, если мы попытаемся найти значение нашего вызова функции приветствия
, мы получим Нет
:
приветствие по умолчанию (имя): приветствие = f"Привет, {имя}!" распечатать (приветствие) print(приветствовать('Фил'))
На выходе мы получаем:
Привет, Фил! Никто
Сначала мы получаем результат вызова функции, потому что Python должен вызвать функцию, чтобы выяснить значение welcome('Phil')
есть; затем мы печатаем возвращаемое значение функции, которое равно None
.
То же самое произойдет, если вы наберете и вернете
без значения. Python вернет None
.
приветствие по умолчанию (имя): приветствие = f"Привет, {имя}!" распечатать (приветствие) возвращаться print(приветствовать('Фил'))
Наконец, помните, что мы можем использовать возвращаемое функцией значение везде, где мы могли бы использовать простое старое значение. Например, при присвоении переменной или как часть строки:
приветствие по умолчанию (имя): приветствие = f"Привет, {имя}!" распечатать (приветствие) print(f"Значение приветствия('Фил') равно {приветствие('Фил')}.")
Здесь мы получаем строку, говорящую нам о том, что значение welcome('Phil')
равно None
:
Привет, Фил! Значение приветствия('Фил') равно None.
Множественный
возврат
операторов Иногда определение функции может содержать более одного оператора return
. Это абсолютно законно, но полезно только в том случае, если у нас есть какая-то условная логика, которая направляет нас только к одному из 9 вариантов.0041 возвращает операторов. Помните, что функция завершится, как только мы столкнемся с любым оператором return
, поэтому, если у нас есть более одного последовательного оператора, мы никогда не попадем в те, которые идут после первого.
Пример, когда несколько операторов возвращают
имеет смысл, это наша функция разделить
:
по определению разделить (а, б): если б == 0: return "Вы не можете делить на 0!" еще: возврат а/б
Это имеет смысл, поскольку у нас есть несколько return
, наша условная логика направляет нас только к одному оператору return
.
Поскольку возвращает
, вызов функции завершается, мы можем немного изменить приведенный выше код:
по определению разделить (а, б): если б == 0: return "Вы не можете делить на 0!" возврат а/б
Это все еще работает, потому что в любом случае, когда b
равно 0
, мы нажимаем на этот оператор return
и выходим из функции. Поэтому мы никогда не попадаем в точку, где выполняем вычисления. Единственный способ добраться туда, если б
не 0
.
Это очень распространенный шаблон, который люди используют, чтобы сэкономить на написании этого предложения else
. Тем не менее, нет никакого вреда, и не стесняйтесь включать его в свой собственный код. Как вам удобнее.
Упражнения
1) Определите функцию возведения в степень
, которая принимает два числа. Первое — это база, а второе — сила, до которой можно поднять базу. Функция должна вернуть результат этой операции. Помните, что мы можем выполнить возведение в степень, используя **
оператор.
2) Определите функцию process_string
, которая принимает строку и возвращает новую строку, которая была преобразована в нижний регистр и в которой были удалены все лишние пробелы.
3) Напишите функцию, которая принимает кортеж, содержащий информацию об актере, и возвращает эти данные в виде словаря.