Как стать автором
Обновить

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

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

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

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
Теги:
Хабы:
Всего голосов 15: ↑14 и ↓1+13
Комментарии44

Публикации

Истории

Работа

Python разработчик
121 вакансия
Data Scientist
78 вакансий

Ближайшие события

15 – 16 ноября
IT-конференция Merge Skolkovo
Москва
22 – 24 ноября
Хакатон «AgroCode Hack Genetics'24»
Онлайн
28 ноября
Конференция «TechRec: ITHR CAMPUS»
МоскваОнлайн
25 – 26 апреля
IT-конференция Merge Tatarstan 2025
Казань