В предыдущих частях мы рассмотрели срезы, распаковкуупаковку коллекций и некоторые особенности булевых операций и типов.
В комментариях упоминалась возможность умножения коллекций на скаляр:
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]
В 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)
Замечание: не надо так делать (нет, серьёзно, не используйте глобальные переменные в своих программах, и тем более не в своих).
Однако, в 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
Автор: LinearLeopard