В предыдущих частях мы рассмотрели срезы, распаковку\упаковку коллекций и некоторые особенности булевых операций и типов.
В комментариях упоминалась возможность умножения коллекций на скаляр:
Более-менее опытный разработчик на языке python знает, что в нём отсутствует механизм копирования при записи
Что же тогда выведет следующий код?
Python в данном случае работает по принципу наименьшего удивления: в переменной a у нас хранится одна единица, то есть b можно было объявить и как
Поведение в данном случае будет такое же:
Но не всё так просто, python копирует содержимое списка столько раз, на сколько вы его домножили и конструирует новый список. А в списках хранятся ссылки на значения, и если, при копировании таким образом ссылок на неизменяемые типы всё хорошо, то вот с изменяемыми вылезает эффект отсутствия копирования при записи.
Генераторы списков и numpy вам в помощь в данном случае.
Списки можно складывать и даже инкрементировать, при этом справа может находиться любой итератор:
Вопрос с подвохом (для собеседования): в python параметры передаются по ссылке или по значению?
Как мы видим, изменения значения в теле функции не изменили значение объекта вне её, из этого можно сделать вывод, что параметры передаются по значению и написать следующий код:
В таких языках как C++ есть переменные, хранящиеся на стеке и в динамической памяти. При вызове ф-ции мы помещаем все аргументы на стек, после чего передаём управление функции. Она знает размеры и смещения переменных на стеке, соответственно может их правильно интерпретировать.
При этом у нас есть два варианта: скопировать на стек память переменной или положить ссылку на объект в динамической памяти (или на более высоких уровнях стека).
Очевидно, что при изменении значений на стеке функции, значения в динамической памяти не поменяются, а при изменении области памяти по ссылке, мы модифицируем общую память, соответственно все ссылки на эту же область памяти «увидят» новое значение.
В python отказались от подобного механизма, заменой служит механизм связывания(assignment) имени переменной с объектом, например при создании переменной:
Интерпретатор создаёт объект «john» и «имя» var, а затем связывает объект с данным именем.
При вызове функции, новых объектов не создаётся, вместо этого в её области видимости создаётся имя, которое связывается с существующим объектом.
Но в python есть изменяемые и неизменяемые типы. Ко вторым, например, относятся числа: при арифметических операциях существующие объекты не меняются, а создаётся новый объект, с которым потом связывается существующее имя. Если же со старым объектом после этого не связано ни одного имени, оно будет удалено с помощью механизма подсчёта ссылок.
Если же имя связано с переменной изменяемого типа, то при операциях с ней изменяется память объекта, соответственно все имена, связанные с данной областью памяти «увидят» изменения.
Можно почитать про это в документации, более подробно изложено здесь.
Ещё один пример:
Но что, если мы решили поменять переменную вне функции? В данном случае нам поможет модификатор global:
Замечание: не надо так делать (нет, серьёзно, не используйте глобальные переменные в своих программах, и тем более не в своих). Лучше просто вернуть несколько значений из функции:
Однако, в python присутствует и другая область видимости и соответствующее ключевое слово:
В данном примере, мы создали переменную, которую можно изменить (и чьё значение получить) только через методы, можно использовать подобный механизм и в классах:
Но, пожалуй, лучше будет ограничиться свойствами или определением __getattr__, __setattr__.
Можете даже определить __delattr__.
Ещё одной особенностью python является наличие двух методов для получения атрибута: __getattr__ и __getattribute__.
В чём между ними разница? Первый вызывается лишь, если атрибут в классе не был найден, а второй безусловно. Если в классе объявлены оба, то __getattr__ вызовется, лишь, если явно его вызвать в __getattribute__ или, если __getattribute__ сгенерировал AttributeError.
И на последок пример того, как python вольно обращается с переменными и областями видимости:
Это, кстати, пожалуй единственный пример, когда второй python лучше третьего, потому что он выводит:
В комментариях упоминалась возможность умножения коллекций на скаляр:
a = [0] * 3 s = 'a' * 2 print(a, s) # -> [0, 0, 0], 'aa'
Более-менее опытный разработчик на языке python знает, что в нём отсутствует механизм копирования при записи
a = [0] b = a b[0] = 1 print(a, b) # -> [1], [1]
Что же тогда выведет следующий код?
b = a * 2 b[0] = 2 print(a, b)
Python в данном случае работает по принципу наименьшего удивления: в переменной a у нас хранится одна единица, то есть b можно было объявить и как
b = [1] * 2
Поведение в данном случае будет такое же:
b = a * 2 b[0] = 2 print(a, b) # -> [1], [2, 1]
Но не всё так просто, python копирует содержимое списка столько раз, на сколько вы его домножили и конструирует новый список. А в списках хранятся ссылки на значения, и если, при копировании таким образом ссылок на неизменяемые типы всё хорошо, то вот с изменяемыми вылезает эффект отсутствия копирования при записи.
row = [0] * 2 matrix = [row] * 2 print(matrix) # -> [[0, 0], [0, 0]] matrix[0][0] = 1 print(matrix) # -> [[1, 0], [1, 0]]
Генераторы списков и numpy вам в помощь в данном случае.
Списки можно складывать и даже инкрементировать, при этом справа может находиться любой итератор:
a = [0] a += (1,) a += {2} a += "ab" a += {1: 2} print(a) # -> [0, 1, 2, 'a', 'b', 1] Заметьте, что строка вставилась посимвольно # ведь именно так работает строковый итератор
Вопрос с подвохом (для собеседования): в python параметры передаются по ссылке или по значению?
def inc(a): a += 1 return a a = 5 print(inc(a)) print(a) # -> 5
Как мы видим, изменения значения в теле функции не изменили значение объекта вне её, из этого можно сделать вывод, что параметры передаются по значению и написать следующий код:
def appended(a): a += [1] return a a = [5] print(appended(a)) # -> [5, 1] print(a) # -> [5, 1]
В таких языках как C++ есть переменные, хранящиеся на стеке и в динамической памяти. При вызове ф-ции мы помещаем все аргументы на стек, после чего передаём управление функции. Она знает размеры и смещения переменных на стеке, соответственно может их правильно интерпретировать.
При этом у нас есть два варианта: скопировать на стек память переменной или положить ссылку на объект в динамической памяти (или на более высоких уровнях стека).
Очевидно, что при изменении значений на стеке функции, значения в динамической памяти не поменяются, а при изменении области памяти по ссылке, мы модифицируем общую память, соответственно все ссылки на эту же область памяти «увидят» новое значение.
В python отказались от подобного механизма, заменой служит механизм связывания(assignment) имени переменной с объектом, например при создании переменной:
var = "john"
Интерпретатор создаёт объект «john» и «имя» var, а затем связывает объект с данным именем.
При вызове функции, новых объектов не создаётся, вместо этого в её области видимости создаётся имя, которое связывается с существующим объектом.
Но в python есть изменяемые и неизменяемые типы. Ко вторым, например, относятся числа: при арифметических операциях существующие объекты не меняются, а создаётся новый объект, с которым потом связывается существующее имя. Если же со старым объектом после этого не связано ни одного имени, оно будет удалено с помощью механизма подсчёта ссылок.
Если же имя связано с переменной изменяемого типа, то при операциях с ней изменяется память объекта, соответственно все имена, связанные с данной областью памяти «увидят» изменения.
Можно почитать про это в документации, более подробно изложено здесь.
Ещё один пример:
a = [1, 2, 3, 4, 5] def rev(l): l.reverse() return l l = a print(a, l) # -> [1, 2, 3, 4, 5], [1, 2, 3, 4, 5] l = rev(l) print(a, l) # -> [5, 4, 3, 2, 1], [5, 4, 3, 2, 1]
Но что, если мы решили поменять переменную вне функции? В данном случае нам поможет модификатор global:
def change(): global a a += 1 a = 5 change() print(a)
Замечание: не надо так делать (нет, серьёзно, не используйте глобальные переменные в своих программах, и тем более не в своих). Лучше просто вернуть несколько значений из функции:
def func(a, b): return a + 1, b + 1
Однако, в python присутствует и другая область видимости и соответствующее ключевое слово:
def private(value=None): def getter(): return value def setter(v): nonlocal value value = v return getter, setter vget, vset = private(42) print(vget()) # -> 42 vset(0) print(vget()) # -> 0
В данном примере, мы создали переменную, которую можно изменить (и чьё значение получить) только через методы, можно использовать подобный механизм и в классах:
def private(value=None): def getter(): return value def setter(v): nonlocal value value = v return getter, setter class Person: def __init__(self, name): self.getid, self.setid = private(name) adam = Person("adam") print(adam.getid()) print(adam.setid("john")) print(adam.getid()) print(dir(adam))
Но, пожалуй, лучше будет ограничиться свойствами или определением __getattr__, __setattr__.
Можете даже определить __delattr__.
Ещё одной особенностью python является наличие двух методов для получения атрибута: __getattr__ и __getattribute__.
В чём между ними разница? Первый вызывается лишь, если атрибут в классе не был найден, а второй безусловно. Если в классе объявлены оба, то __getattr__ вызовется, лишь, если явно его вызвать в __getattribute__ или, если __getattribute__ сгенерировал AttributeError.
class Person(): def __getattr__(self, item): print("__getattr__") if item == "name": return "john" raise AttributeError def __getattribute__(self, item): print("__getattribute__") raise AttributeError person = Person() print(person.name) # -> __getattribute__ # -> __getattr__ # -> john
И на последок пример того, как python вольно обращается с переменными и областями видимости:
e = 42 try: 1 / 0 except Exception as e: pass print(e) # -> NameError: name 'e' is not defined
Это, кстати, пожалуй единственный пример, когда второй python лучше третьего, потому что он выводит:
... print(e) # -> float division by zero
