Передача по ссылке в Python: общие сведения и рекомендации

Оглавление

Смотрите сейчас, к этому уроку прилагается соответствующий видеокурс, созданный командой Real Python. Просмотрите его вместе с письменным руководством, чтобы углубить свое понимание: Переход по ссылке в Python: Лучшие практики

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

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

В этом уроке вы узнаете:

  • Что значит передавать по ссылке и почему вы хотели бы это сделать
  • Чем передача по ссылке отличается от передачи по значению и Уникального подхода Python
  • Как аргументы функции ведут себя в Python
  • Как вы можете использовать определенные изменяемые типы для передачи по ссылке в Python
  • Каковы рекомендации по репликации передачи по ссылке в Python

Бесплатный бонус: 5 Размышления о мастерстве владения Python - бесплатный курс для разработчиков Python, который показывает вам план действий и мышление, с которым вы будете работать. вам нужно поднять свои навыки работы с Python на новый уровень.

Определение передачи по ссылке

Прежде чем вы углубитесь в технические детали передачи по ссылке, полезно более подробно рассмотреть сам термин, разбив его на составляющие:

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

Поскольку вы даете функции ссылку на существующую переменную, все операции, выполняемые с этой ссылкой, будут напрямую влиять на переменную, на которую она ссылается. Давайте рассмотрим несколько примеров того, как это работает на практике.

Ниже вы увидите, как передавать переменные по ссылке в C#. Обратите внимание на использование ключевого слова ref в выделенных строках:

using System;

// Source:
// https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/passing-parameters
class Program
{
    static void Main(string[] args)
    {
        int arg;

        // Passing by reference.
        // The value of arg in Main is changed.
        arg = 4;
        squareRef(ref arg);
        Console.WriteLine(arg);
        // Output: 16
    }

    static void squareRef(ref int refParameter)
    {
        refParameter *= refParameter;
    }
}


Как вы можете видеть, refParameter из squareRef() должно быть объявлено с помощью ключевого слова ref, и вы также должны использовать это ключевое слово при вызове функции. Тогда аргумент будет передан по ссылке и может быть изменен на месте.

В Python нет ключевого слова ref или чего-либо эквивалентного ему. Если вы попытаетесь максимально точно воспроизвести приведенный выше пример в Python, то увидите другие результаты:

>>> def main():
...     arg = 4
...     square(arg)
...     print(arg)
...
>>> def square(n):
...     n *= n
...
>>> main()
4


В этом случае переменная arg является , а не измененной на месте. Похоже, что Python рассматривает предоставленный вами аргумент как отдельное значение, а не как ссылку на существующую переменную. Означает ли это, что Python передает аргументы по значению, а не по ссылке?

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

Противопоставление передачи по ссылке и передачи по значению

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

Давайте вернемся к примеру на C#, на этот раз без использования ключевого слова ref. Это приведет к тому, что программа будет использовать стандартное поведение передачи по значению:

using System;

// Source:
// https://docs.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/passing-parameters
class Program
{
    static void Main(string[] args)
    {
        int arg;

        // Passing by value.
        // The value of arg in Main is not changed.
        arg = 4;
        squareVal(arg);
        Console.WriteLine(arg);
        // Output: 4
    }

    static void squareVal(int valParameter)
    {
        valParameter *= valParameter;
    }
}


Здесь вы можете видеть, что squareVal() не изменяет исходную переменную. Скорее, valParameter является независимой копией исходной переменной arg. Хотя это соответствует поведению, которое вы могли бы наблюдать в Python, помните, что Python не передает данные точно по значению. Давайте докажем это.

Встроенный в Python id() возвращает целое число, представляющее адрес памяти нужного объекта. Используя id(), вы можете проверить следующие утверждения:

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

В приведенном ниже примере обратите внимание, что адрес x изначально совпадает с адресом n, но изменяется после переназначения, в то время как адрес n никогда не меняется:

>>> def main():
...     n = 9001
...     print(f"Initial address of n: {id(n)}")
...     increment(n)
...     print(f"  Final address of n: {id(n)}")
...
>>> def increment(x):
...     print(f"Initial address of x: {id(x)}")
...     x += 1
...     print(f"  Final address of x: {id(x)}")
...
>>> main()
Initial address of n: 140562586057840
Initial address of x: 140562586057840
  Final address of x: 140562586057968
  Final address of n: 140562586057840


Тот факт, что начальные адреса n и x совпадают при вызове increment(), доказывает, что аргумент x передается не по значению. В противном случае n и x имели бы разные адреса памяти.

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

Использование конструкций передачи по ссылкам

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

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

Как избежать дублирования объектов

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

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

Возвращает несколько значений

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

using System;

class Program
{
    static void Main(string[] args)
    {
        int counter = 0;

        // Passing by reference.
        // The value of counter in Main is changed.
        Console.WriteLine(greet("Alice", ref counter));
        Console.WriteLine("Counter is {0}", counter);
        Console.WriteLine(greet("Bob", ref counter));
        Console.WriteLine("Counter is {0}", counter);
        // Output:
        // Hi, Alice!
        // Counter is 1
        // Hi, Bob!
        // Counter is 2
    }

    static string greet(string name, ref int counter)
    {
        string greeting = "Hi, " + name + "!";
        counter++;
        return greeting;
    }
}


В приведенном выше примере greet() возвращает строку приветствия , а также изменяет значение counter. Теперь попытайтесь воспроизвести это как можно точнее на Python:

>>> def main():
...     counter = 0
...     print(greet("Alice", counter))
...     print(f"Counter is {counter}")
...     print(greet("Bob", counter))
...     print(f"Counter is {counter}")
...
>>> def greet(name, counter):
...     counter += 1
...     return f"Hi, {name}!"
...
>>> main()
Hi, Alice!
Counter is 0
Hi, Bob!
Counter is 0


counter в приведенном выше примере значение не увеличивается, потому что, как вы уже знали, в Python нет возможности передавать значения по ссылке. Итак, как вы можете достичь того же результата, что и с C#?

По сути, ссылочные параметры в C# позволяют функции не только возвращать значение, но и оперировать дополнительными параметрами. Это эквивалентно возврату нескольких значений!

К счастью, Python уже поддерживает возврат нескольких значений. Строго говоря, функция Python, которая возвращает несколько значений, на самом деле возвращает кортеж, содержащий каждое значение:

>>> def multiple_return():
...     return 1, 2
...
>>> t = multiple_return()
>>> t  # A tuple
(1, 2)

>>> # You can unpack the tuple into two variables:
>>> x, y = multiple_return()
>>> x
1
>>> y
2


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

Вооружившись этим приемом, вы можете изменить оператор return в greet() из вашего предыдущего кода на Python, чтобы он возвращал как приветствие, так и счетчик:

>>> def main():
...     counter = 0
...     print(greet("Alice", counter))
...     print(f"Counter is {counter}")
...     print(greet("Bob", counter))
...     print(f"Counter is {counter}")
...
>>> def greet(name, counter):
...     return f"Hi, {name}!", counter + 1
...
>>> main()
('Hi, Alice!', 1)
Counter is 0
('Hi, Bob!', 1)
Counter is 0


Это по-прежнему выглядит неправильно. Хотя greet() теперь возвращает несколько значений, они выводятся как tuple, что не входит в ваши намерения. Кроме того, исходная переменная counter остается на прежнем уровне. 0.

Чтобы очистить ваши выходные данные и получить желаемые результаты, вам придется переназначать вашу переменную counter при каждом вызове greet():

>>> def main():
...     counter = 0
...     greeting, counter = greet("Alice", counter)
...     print(f"{greeting}\nCounter is {counter}")
...     greeting, counter = greet("Bob", counter)
...     print(f"{greeting}\nCounter is {counter}")
...
>>> def greet(name, counter):
...     return f"Hi, {name}!", counter + 1
...
>>> main()
Hi, Alice!
Counter is 1
Hi, Bob!
Counter is 2


Теперь, после переназначения каждой переменной с помощью вызова greet(), вы можете увидеть желаемые результаты!

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

Создание условных функций с множественным возвратом

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

Рассмотрим стандарт Int32.Функция TryParse в C#, которая возвращает логическое значение и одновременно оперирует ссылкой на целочисленный аргумент:

public static bool TryParse (string s, out int result);


Эта функция пытается преобразовать string в 32-разрядное целое число со знаком, используя ключевое слово out. Есть два возможных результата:

  1. Если синтаксический анализ завершится успешно, то выходному параметру будет присвоено результирующее целое число, и функция вернет true.
  2. Если синтаксический анализ завершится неудачей, то выходному параметру будет присвоено значение 0, и функция вернет значение false.

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

using System;

// Source:
// https://docs.microsoft.com/en-us/dotnet/api/system.int32.tryparse?view=netcore-3.1#System_Int32_TryParse_System_String_System_Int32__
public class Example {
    public static void Main() {
        String[] values = { null, "160519", "9432.0", "16,667",
                            "   -322   ", "+4302", "(100);", "01FA" };
        foreach (var value in values) {
            int number;

            if (Int32.TryParse(value, out number)) {
                Console.WriteLine("Converted '{0}' to {1}.", value, number);
            }
            else {
                Console.WriteLine("Attempted conversion of '{0}' failed.",
                                   value ?? "<null>");
            }
        }
    }
}


Приведенный выше код, который пытается преобразовать строки другого формата в целые числа с помощью TryParse(), выдает следующее:

Attempted conversion of '<null>' failed.
Converted '160519' to 160519.
Attempted conversion of '9432.0' failed.
Attempted conversion of '16,667' failed.
Converted '   -322   ' to -322.
Converted '+4302' to 4302.
Attempted conversion of '(100);' failed.
Attempted conversion of '01FA' failed.


Чтобы реализовать аналогичную функцию в Python, вы могли бы использовать несколько возвращаемых значений, как вы видели ранее:

def tryparse(string, base=10):
    try:
        return True, int(string, base=base)
    except ValueError:
        return False, None


Это tryparse() возвращает два значения. Первое значение указывает, было ли преобразование успешным, а второе содержит результат (или None, в случае сбоя).

Однако использование этой функции немного затруднительно, поскольку вам нужно распаковывать возвращаемые значения при каждом вызове. Это означает, что вы не можете использовать функции внутри if заявление:

>>> success, result = tryparse("123")
>>> success
True
>>> result
123

>>> # We can make the check work
>>> # by accessing the first element of the returned tuple,
>>> # but there's no way to reassign the second element to `result`:
>>> if tryparse("456")[0]:
...     print(result)
...
123


Несмотря на то, что обычно это работает, возвращая несколько значений, tryparse() нельзя использовать при проверке условий. Это означает, что вам предстоит еще кое-что сделать.

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

def tryparse(string, base=10):
    try:
        return int(string, base=base)
    except ValueError:
        return None


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

Используя преимущества гибкости Python в отношении типов объектов, а также новые выражения присваивания в Python 3.8, вы можете вызвать эту упрощенную функцию с помощью условного оператора if и получают возвращаемое значение, если проверка прошла успешно:

>>> if (n := tryparse("123")) is not None:
...     print(n)
...
123
>>> if (n := tryparse("abc")) is None:
...     print(n)
...
None

>>> # You can even do arithmetic!
>>> 10 * tryparse("10")
100

>>> # All the functionality of int() is available:
>>> 10 * tryparse("0a", base=16)
100

>>> # You can also embed the check within the arithmetic expression!
>>> 10 * (n if (n := tryparse("123")) is not None else 1)
1230
>>> 10 * (n if (n := tryparse("abc")) is not None else 1)
10


Ничего себе! Эта версия Python для tryparse() еще более мощная, чем версия C#, что позволяет использовать ее в условных операторах и арифметических выражениях.

Проявив немного изобретательности, вы воспроизвели конкретный и полезный шаблон передачи по ссылке, фактически не передавая аргументы по ссылке. Фактически, вы снова присваиваете возвращаемые значения при использовании оператора выражения присваивания(:=) и используете возвращаемое значение непосредственно в выражениях Python.

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

Передача аргументов в Python

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

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

Понимание присваивания в Python

Справочник по языку Python для операторов присваивания содержит следующие сведения:

  • Если целью присвоения является идентификатор или имя переменной, то это имя привязывается к объекту. Например, в x = 2, x - это имя, а 2 - объект.
  • Если имя уже привязано к отдельному объекту, то оно повторно привязывается к новому объекту. Например, если x уже равно 2 и вы вводите x = 3, то имя переменной x будет изменено на 3.

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

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

Документация по Python содержит дополнительные сведения о количестве ссылок .

Давайте придерживаться примера x = 2 и рассмотрим, что происходит, когда вы присваиваете значение новой переменной:

  1. Если объект, представляющий значение 2, уже существует, то он извлекается. В противном случае он создается.
  2. Счетчик ссылок этого объекта увеличивается.
  3. В текущее пространство имен добавлена запись, чтобы привязать идентификатор x к объекту, представляющему 2. Эта запись на самом деле представляет собой пару ключ-значение, хранящуюся в словаре! Представление этого словаря возвращается с помощью locals() или globals().

Теперь вот что произойдет, если вы присвоите x другому значению:

  1. Счетчик ссылок объекта, представляющего 2, уменьшается.
  2. Счетчик ссылок объекта, представляющего новое значение, увеличивается.
  3. Словарь для текущего пространства имен обновляется, чтобы связать x с объектом, представляющим новое значение.

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

from sys import getrefcount

print("--- Before  assignment ---")
print(f"References to value_1: {getrefcount('value_1')}")
print(f"References to value_2: {getrefcount('value_2')}")
x = "value_1"
print("--- After   assignment ---")
print(f"References to value_1: {getrefcount('value_1')}")
print(f"References to value_2: {getrefcount('value_2')}")
x = "value_2"
print("--- After reassignment ---")
print(f"References to value_1: {getrefcount('value_1')}")
print(f"References to value_2: {getrefcount('value_2')}")


Этот скрипт покажет количество ссылок для каждого значения до присвоения, после присвоения и после переназначения:

--- Before  assignment ---
References to value_1: 3
References to value_2: 3
--- After   assignment ---
References to value_1: 4
References to value_2: 3
--- After reassignment ---
References to value_1: 3
References to value_2: 4


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

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

Изучение аргументов функции

Аргументами функции в Python являются локальные переменные. Что это значит? Локальная - это одна из областей Python. Эти области представлены словарями пространств имен, упомянутыми в предыдущем разделе. Вы можете использовать locals() и globals() для получения локального и глобального словарей пространств имен соответственно.

При выполнении каждая функция имеет свое собственное локальное пространство имен:

>>> def show_locals():
...     my_local = True
...     print(locals())
...
>>> show_locals()
{'my_local': True}


Используя locals(), вы можете продемонстрировать, что аргументы функции становятся обычными переменными в локальном пространстве имен функции. Давайте добавим к функции аргумент my_arg:

>>> def show_locals(my_arg):
...     my_local = True
...     print(locals())
...
>>> show_locals("arg_value")
{'my_arg': 'arg_value', 'my_local': True}


Вы также можете использовать sys.getrefcount(), чтобы показать, как аргументы функции увеличивают счетчик ссылок для объекта:

>>> from sys import getrefcount

>>> def show_refcount(my_arg):
...     return getrefcount(my_arg)
...
>>> getrefcount("my_value")
3
>>> show_refcount("my_value")
5


Приведенный выше скрипт выводит количество ссылок для "my_value" сначала снаружи, затем внутри show_refcount(), показывая увеличение количества ссылок не на одну, а на две!

Это потому, что в дополнение к самому show_refcount() вызов sys.getrefcount() внутри show_refcount() также получает my_arg в качестве аргумента. Это помещает my_arg в локальное пространство имен для sys.getrefcount(), добавляя дополнительную ссылку на "my_value".

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

Теперь вы можете видеть, как Python передает аргументы путем присваивания!

Реплицирующий проход по ссылке с помощью Python

Изучив пространства имен в предыдущем разделе, вы, возможно, спросите, почему global не упоминается как один из способов изменения переменных, как если бы они передавались по ссылке:

>>> def square():
...     # Not recommended!
...     global n
...     n *= n
...
>>> n = 4
>>> square()
>>> n
16


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

  • Свободные переменные, казалось бы, ни с чем не связанные
  • Функции без явных аргументов для указанных переменных
  • Функции, которые нельзя использовать в общем случае с другими переменными или аргументами, поскольку они полагаются на одну глобальную переменную
  • Отсутствие потокобезопасности при использовании глобальных переменных

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

>>> def square(n):
...     return n * n
...
>>> square(4)
16


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

Несмотря на то, что Python не является ни языком с передачей ссылок, ни языком с передачей значений, у него нет недостатков в этом отношении. Его гибкость более чем удовлетворяет поставленной задаче.

Наилучшая практика: Возврат и переназначение

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

Цель - написать одноцелевые функции, которые возвращают одно значение, а затем (повторно) присваивают это значение переменным, как в следующем примере:

def square(n):
    # Accept an argument, return a value.
    return n * n

x = 4
...
# Later, reassign the return value:
x = square(x)


Возвращаемые и присваиваемые значения также делают ваши намерения более явными, а ваш код более понятным и тестируемым.

Для функций, которые работают с несколькими значениями, вы уже видели, что Python способен возвращать кортеж значений. Вы превзошли даже элегантность Int32.TryParse() в C# благодаря гибкости Python!

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

def greet(name, counter):
    # Return multiple values
    return f"Hi, {name}!", counter + 1

counter = 0
...
# Later, reassign each return value by unpacking.
greeting, counter = greet("Alice", counter)


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

Рекомендуемая практика: Используйте атрибуты объектов

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

Напишите функции, которые принимают объекты с атрибутами, а затем работают непосредственно с этими атрибутами, как в следующем примере:

>>> # For the purpose of this example, let's use SimpleNamespace.
>>> from types import SimpleNamespace

>>> # SimpleNamespace allows us to set arbitrary attributes.
>>> # It is an explicit, handy replacement for "class X: pass".
>>> ns = SimpleNamespace()

>>> # Define a function to operate on an object's attribute.
>>> def square(instance):
...     instance.n *= instance.n
...
>>> ns.n = 4
>>> square(ns)
>>> ns.n
16


Обратите внимание, что square() необходимо записать для работы непосредственно с атрибутом, который будет изменен без необходимости переназначать возвращаемое значение.

Стоит повторить, что вы должны убедиться, что атрибут поддерживает присвоение! Вот тот же пример с namedtuple, атрибуты которого доступны только для чтения:

>>> from collections import namedtuple
>>> NS = namedtuple("NS", "n")
>>> def square(instance):
...     instance.n *= instance.n
...
>>> ns = NS(4)
>>> ns.n
4
>>> square(ns)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in square
AttributeError: can't set attribute


Попытки изменить атрибуты, которые не допускают изменения, приводят к AttributeError.

Кроме того, вам следует помнить о атрибутах класса. Они останутся неизменными, а атрибут экземпляра будет создан и изменен:

>>> class NS:
...     n = 4
...
>>> ns = NS()
>>> def square(instance):
...     instance.n *= instance.n
...
>>> ns.n
4
>>> square(ns)
>>> # Instance attribute is modified.
>>> ns.n
16
>>> # Class attribute remains unchanged.
>>> NS.n
4


Поскольку атрибуты класса остаются неизменными при изменении с помощью экземпляра класса, вам нужно помнить о необходимости ссылки на атрибут экземпляра.

Рекомендуемая практика: Используйте словари и списки

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

A сопоставление объектов с хэшируемыми значениями произвольным объектам. Сопоставления - это изменяемые объекты. В настоящее время существует только один стандартный тип отображения - словарь. (Источник)

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

>>> # Dictionaries are mapping types.
>>> mt = {"n": 4}
>>> # Define a function to operate on a key:
>>> def square(num_dict):
...     num_dict["n"] *= num_dict["n"]
...
>>> square(mt)
>>> mt
{'n': 16}


Поскольку вы переназначаете значение ключу словаря, работа с элементами словаря по-прежнему является формой присвоения. Со словарями вы получаете дополнительную практичность доступа к измененному значению через тот же объект словаря.

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

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

>>> # Lists are both subscriptable and mutable.
>>> sm = [4]
>>> # Define a function to operate on an index:
>>> def square(num_list):
...     num_list[0] *= num_list[0]
...
>>> square(sm)
>>> sm
[16]


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

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

>>> subscriptable = [0, 1, 2]  # A list
>>> subscriptable[0]
0
>>> subscriptable = (0, 1, 2)  # A tuple
>>> subscriptable[0]
0
>>> subscriptable = "012"  # A string
>>> subscriptable[0]
'0'
>>> not_subscriptable = {0, 1, 2}  # A set
>>> not_subscriptable[0]
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'set' object is not subscriptable


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

Изменчивость - это более широкая тема, требующая дополнительного изучения и ссылки на документацию. Короче говоря, объект является изменяемым, если его структура может быть изменена на месте, а не требует переназначения:

>>> mutable = [0, 1, 2]  # A list
>>> mutable[0] = "x"
>>> mutable
['x', 1, 2]

>>> not_mutable = (0, 1, 2)  # A tuple
>>> not_mutable[0] = "x"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'tuple' object does not support item assignment

>>> not_mutable = "012"  # A string
>>> not_mutable[0] = "x"
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: 'str' object does not support item assignment

>>> mutable = {0, 1, 2}  # A set
>>> mutable.remove(0)
>>> mutable.add("x")
>>> mutable
{1, 2, 'x'}


Списки и наборы могут изменяться, как и словари и другие типы сопоставлений. Строки и кортежи не могут изменяться. Попытка изменить элемент неизменяемого объекта приведет к возникновению ошибки. TypeError.

Заключение

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

В этом уроке вы узнали:

  • Как Python обрабатывает присвоение значений переменным
  • Как аргументы функции передаются путем присваивания в Python
  • Почему возвращаемые значения лучше всего воспроизводить по ссылке
  • Как использовать атрибуты, словари и списки в качестве альтернативы рекомендации

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

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

Оставайтесь любопытными, и увидимся в следующий раз!

<статус завершения article-slug="python-pass-by-reference" class="btn-group mb-0" data-api-article-bookmark-url="/api/v1/статьи/python-pass-by-reference/bookmark/" data-api-url-адрес статуса завершения статьи="/api/v1/articles/python-pass-by-reference/completion_status/"> <кнопка поделиться bluesky-text="Интересная статья на #Python от @realpython.com :" email-body="Ознакомьтесь с этой статьей о Python:%0A%0APass по ссылке в Python: Справочная информация и рекомендации" email-subject="Статья о Python для вас" twitter-text="Интересная #Статья о Python от @realpython:" url="https://realpython.com/python-pass-by-reference /" url-title="Передача по ссылке в Python: общие сведения и рекомендации">

Смотрите сейчас, к этому уроку прилагается соответствующий видеокурс, созданный командой Real Python. Просмотрите его вместе с письменным руководством, чтобы углубить свое понимание: Переход по ссылке в Python: Лучшие практики

Back to Top