Python interesting and useful. Part 3

    In previous sections, we looked at slices, unpacking / packing collections, and some features of Boolean operations and types.

    The comments mentioned the possibility of multiplying collections by a scalar:

    a = [0] * 3
    s = 'a' * 2
    print(a, s)  # -> [0, 0, 0], 'aa'

    A more or less experienced python developer knows that there is no copying mechanism when writing

    a = [0]
    b = a
    b[0] = 1
    print(a, b)  # -> [1], [1]

    What then will the following code output?

    b = a * 2
    b[0] = 2
    print(a, b)
    

    Python in this case works on the principle of least surprise: in the variable a we have one unit, that is, b could be declared and how

    b = [1] * 2

    The behavior in this case will be the same:

    b = a * 2
    b[0] = 2
    print(a, b)  # -> [1], [2, 1]

    But not everything is so simple, python copies the contents of the list as many times as you multiplied it and constructs a new list. And lists of values ​​are stored in lists, and if, when copying references to immutable types in this way, everything is fine, then the effect of not copying during recording comes up with changeable ones.

    row = [0] * 2
    matrix = [row] * 2
    print(matrix)        # -> [[0, 0], [0, 0]]
    matrix[0][0] = 1
    print(matrix)        # -> [[1, 0], [1, 0]]

    List generators and numpy help you in this case.

    Lists can be added and even incremented, with any iterator to the right:

    a = [0]
    a += (1,)
    a += {2}
    a += "ab"
    a += {1: 2}
    print(a)  # -> [0, 1, 2, 'a', 'b', 1] Заметьте, что строка вставилась посимвольно# ведь именно так работает строковый итератор

    Trick question (for interview): are parameters passed to python by reference or value?

    definc(a):
        a += 1return a
    a = 5
    print(inc(a))
    print(a)         # -> 5

    As we can see, changes in the value in the function body did not change the value of the object outside it, from this we can conclude that the parameters are passed by value and write the following code:

    defappended(a):
        a += [1]
        return a
    a = [5]
    print(appended(a))  # -> [5, 1]
    print(a)            # -> [5, 1]


    In languages ​​like C ++, there are variables stored on the stack and in dynamic memory. When calling the function, we put all the arguments on the stack, and then pass control to the function. She knows the sizes and shifts of variables on the stack, so she can interpret them correctly.
    In this case, we have two options: copy the variable memory onto the stack or put a reference to the object in the dynamic memory (or at higher stack levels).
    Obviously, when the values ​​on the function stack change, the values ​​in the dynamic memory will not change, and when the memory area changes by reference, we modify the shared memory, respectively, all references to the same memory area will “see” the new value.

    In python, they abandoned this mechanism; the replacement is the assignment of the variable name to the object, for example, when creating a variable:
    var = "john"


    The interpreter creates the “john” object and the “name” var, and then associates the object with the given name.
    When a function is called, no new objects are created; instead, a name is created in its scope that is associated with an existing object.
    But in python there are mutable and immutable types. The first, for example, are numbers: for arithmetic operations, existing objects do not change, but a new object is created, which is then associated with an existing name. If no old name is associated with the old object afterwards, it will be removed using the link counting mechanism.
    If the name is associated with a variable of a changeable type, then during operations with it the memory of the object changes, respectively, all the names associated with this memory region will “see” the changes.

    You can read about it indocumentation , more detailed here .

    One more example:

    a = [1, 2, 3, 4, 5]
    defrev(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]

    But what if we decided to change the variable outside the function? In this case, the global modifier will help us:

    defchange():global a
        a += 1
    a = 5
    change()
    print(a)
    

    Note: do not do this (no, seriously, do not use global variables in your programs, and especially not in your own). It is better to simply return several values ​​from the function:
    deffunc(a, b):return a + 1, b + 1


    However, in python, there is another scope and corresponding keyword:

    defprivate(value=None):defgetter():return value
        defsetter(v):nonlocal value
            value = v
        return getter, setter
    vget, vset = private(42)
    print(vget())    # -> 42
    vset(0)
    print(vget())    # ->  0

    In this example, we have created a variable, which can be changed (and whose value is obtained) only through methods; you can use a similar mechanism in the classes:

    defprivate(value=None):defgetter():return value
        defsetter(v):nonlocal value
            value = v
        return getter, setter
    classPerson: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))
    

    But, perhaps, it would be better to limit ourselves to the properties or the definition of __getattr__ , __setattr__ .

    You can even define __delattr__ .

    Another feature of python is the presence of two methods for obtaining an attribute: __getattr__ and __getattribute__ .

    What is the difference between them? The first is called only if the attribute in the class was not found, and the second is unconditional. If both are declared in the class, then __getattr__ will be called only if it is explicitly called in __getattribute__, or if __getattribute__ has generated AttributeError.

    classPerson():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

    And lastly an example of how python freely handles variables and scopes:

        e = 42try:
            1 / 0except Exception as e:
            pass
        print(e)  # -> NameError: name 'e' is not defined

    This, by the way, is perhaps the only example where the second python is better than the third, because it prints:

        ...
        print(e)  # -> float division by zero

    Also popular now: