Интересности и полезности python. Часть 3

от автора

В предыдущих частях мы рассмотрели срезы, распаковку\упаковку коллекций и некоторые особенности булевых операций и типов.

В комментариях упоминалась возможность умножения коллекций на скаляр:

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 


ссылка на оригинал статьи https://habr.com/post/422951/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *