Python tips & tricks

Base python tips & tricks

Недавно прочитал книгу Марка Лутца "Learning Python", 5-ое издание. Привожу список самых интересных фишек (по моему мнению) оттуда, что-то вроде конспекта.

  • генерация set'a:

    {x for x in [1,2]}
    set(x for x in [1,2])
    assert set(x for x in [1,2]) == {x for x in [1,2]}
    
  • генерация dict'а:

    {x:x**2 for x in [1,2]}
    dict((x, x**2) for x in [1,2])
    assert {x:x**2 for x in [1,2]} == dict((x, x**2) for x in [1,2])
    
  • деление целых чисел

    В python 3 деление целых чисел возвращает float

    >>> 1 / 2
    0.5
    >>> - 1 / 2
    -0.5
    

    В python 2 деление - округление в меньшую сторону, это не truncate

    >>> 1 / 2    # 0.5 floor округление -> 0
    0
    >>> - 1 / 2  # -0.5 floor округление -> -1 (не 0)
    -1
    

    В python 2 и 3 целочисленное деление

    >>> 1 // 2
    0
    >>> - 1 // 2
    -1
    >>> 13 // 2.0
    6.0
    
  • is - проверяет, что переменные указывают на один и тот же адрес, == проверяет, что объекты имеют одинаковые значения

  • python 3: [1, 'spam'].sort() возбуждает исключение (разные типы)

  • python 3: dict().keys() возвращает итератор (view объект, зависимый от dict). Это set-like объект, к нему можно применять set операции (union и пр.)

    >>> dict(a=1, b=2).keys()
    dict_keys(['b', 'a'])
    >>> dict(a=1, b=2).keys() | {'c', 'd'}
    {'b', 'd', 'a', 'c'}
    
  • frozenset - immutable set, он является hashable, можно использовать например как ключ в dict

    >>> fz = frozenset([1,2])
    >>> fz.add(3)
    AttributeError: 'frozenset' object has no attribute 'add'
    >>> {fz: 5}
    {frozenset([1, 2]): 5}
    
  • list поддерживает операторы сравнения: ==, <, >, <=, >=. Сравнение аналогично сравнению срок. Для py3 все объекты должны быть одного типа

    >>> [1, 2] == [1, 2]
    True
    >>> [2, 2] > [1, 2]
    True
    >>> [1] > ['sh']  # python2
    False
    >>> [1] > ['sh']  # python3
    TypeError: unorderable types: int() > str()
    
  • сравнение dict'ов

    python 2 and 3

    >>> dict(a=1) == dict(a=1)
    True
    

    python 2 only

    >>> dict(a=3) > dict(a=2)
    True
    >>> dict(a=3) > dict(a=2, b=1)
    False
    
  • нельзя list + string, list + tuple, однако можно list += string

    >>> L = []
    >>> L + 'spam'
    TypeError: can only concatenate list (not "str") to list
    
    >>> L = []
    >>> L += 'spam'
    >>> L
    ['s', 'p', 'a', 'm']
    
  • L += a is faster that L = L + a.

  • L += [1,2] is in place modification! (не создается новый список)

    >>> L = []
    >>> id(L)
    4368997048
    >>> L += [1,2]
    >>> id(L)
    4368997048
    >>> L = L + [1,2]
    >>> id(L)
    4368996976
    
  • 'spam'[0][0][0] можно до бесконечности, каждый раз будет возвращатся односимвольная строка 's'

  • распаковка аргументов в python 3 при присваивании

    >>> a, *b = 'spam'
    >>> a
    's'
    >>> b
    ['p', 'a', 'm']
    
    >>> *a, b = 'spam'
    >>> a
    ['s', 'p', 'a']
    >>> b
    'm'
    
    >>> a, *b, c = 'spam'
    >>> a
    's'
    >>> b
    ['p', 'a']
    >>> c
    'm'
    
  • python 2: True = 0, но не в python 3

    python 2

    >>> True = 0
    >>> True
    0
    

    python 3

    >>> True = 0
    SyntaxError: can't assign to keyword
    
  • sys.stdout = open('temp.txt', 'w') - все print'ы будут идти в файл temp.txt

  • and, or возвращают объект, а не True/False

  • while has else

  • python 3: ... все равно что pass

  • reversed works with lists, not generator

    >>> reversed([1,2,3])
    <list_reverseiterator object at 0x10127c550>
    >>> reversed((x for x in [1,2,3]))
    TypeError: argument to reversed() must be a sequence
    
  • zip итерирует до самой маленькой последовательности

    >>> [x for x in zip([1,2,3], [4,5])]
    [(1, 4), (2, 5)]
    
  • python 2: map(None, s1, s2) тоже самое, что zip, но добавялет None для элементов из более длинной последовательности

    python 2

    >>> map(None, [1,2,3], [4,5])
    [(1, 4), (2, 5), (3, None)]
    >>> map(None, [1,2], [4,5,6])
    [(1, 4), (2, 5), (None, 6)]
    

    python 3

    >>> list(map(None, [1,2,3], [4,5]))
    TypeError: 'NoneType' object is not callable
    
  • map can take more than one iterators (похоже на zip)

    python 2

    >>> map(lambda x, y: (x, y), [1,2], [3,4])
    [(1, 3), (2, 4)]
    >>> map(lambda x, y: (x, y), [1,2], [3,4,5])
    [(1, 3), (2, 4), (None, 5)]
    

    python 3

    >>> list(map(lambda x, y: (x, y), [1,2], [3,4]))
    [(1, 3), (2, 4)]
    >>> list(map(lambda x, y: (x, y), [1,2], [3,4,5]))
    [(1, 3), (2, 4)]
    
  • nested list comprehensions

    >>> [x+y for x in 'abc' for y in 'lmn']
    ['al', 'am', 'an', 'bl', 'bm', 'bn', 'cl', 'cm', 'cn']
    
    # flat list of lists
    >>> csv = [[1, 2, 3], [4, 5, 6], [7, 8, 9]]
    >>> [col for row in csv for col in row]
    [1, 2, 3, 4, 5, 6, 7, 8, 9]
    
  • sorted возвращает список (не генератор) в py2 и py3

    >>> sorted(x for x in [2,1,3])
    [1, 2, 3]
    
  • *args понимает любой итератор, не обязательно list

  • unzip: zip(*zip(a,b))

    >>> zip(*zip([1,2],[3,4]))
    [(1, 2), (3, 4)]
    
  • py3: map возвращает генератор, пройти по нему можно только раз

    >>> m = map(lambda x: x, [1,2,3])
    >>> [x for x in m]
    [1, 2, 3]
    >>> [x for x in m]
    []
    
  • py3: хоть range и генератор (это хитрый генератор), он поддерживает len() и доступ по индексам

    >>> r = range(10)
    >>> r
    range(0, 10)
    >>> len(r)
    10
    >>> r[3]
    3
    
  • generator allows only single scan

  • циклические импорты работают! Но только с import, без from

  • у try есть else, который вызовится, если exception не случилось

  • with similar to finally

  • except (name1, name2) - orders from top to bottom, from left to right

  • except Exception: vs except: - первое не перехватывает системные исключения (KeyboardInterrupt, SystemExit, GeneratorExit например)

  • set().remove(x) - удаляет x или KeyError, set().discard(x) - удаляет x или ничего

  • py3.3+ accept u"", U"" для обратной совместимости с py2

  • default encoding is in sys module sys.getdefaultencoding()

    python 2

    >>> sys.getdefaultencoding()
    'ascii'
    

    python 3

    >>> sys.getdefaultencoding()
    'utf-8'
    
  • [c for c in sorted([1,2,3], key=lambda c: -c)] - тут переменная c конфликтовать не будет

  • в py2 переменная внутри comprehension может изменять внешние переменные а также доступна после, в py3 - нет.

    python 2

    >>> x = 1
    >>> [x for x in range(3)]
    [0, 1, 2]
    >>> x
    2
    # creates new var
    >>> [y for y in range(3)]
    [0, 1, 2]
    >>> y
    2
    

    python 3

    >>> x = 1
    >>> [x for x in range(3)]
    >>> x
    1
    # no new var
    >>> [y for y in range(3)]
    [0, 1, 2]
    >>> y
    NameError: name 'y' is not defined
    
  • py3 имеет инструкцию nonlocal. Используется для ссылки на имя во внешнем def блоке (в py2 к такой переменной нельзя обратиться)

    def f():
        x = 2  # local for f
        def g():
            nonlocal x  # python3 only
            x = 3  # local for g
        g()
        print(x)
    >>> f()  # python3 only
    3
    >>> f()  # with commented nonlocal
    2
    
  • LEGB rule (local, enclosing, global, builtin) или LNGB (N=nonlocal) - порядок поиска переменной в python

  • py3 переменная исключения as name удаляется после выполнения блока (даже если переменная была объявлена до try)

    python 2

    >>> x = 1
    >>> try:
    ...     1/0
    ... except Exception as x:
    ...     pass
    >>> x
    ZeroDivisionError('integer division or modulo by zero',)
    

    python 3

    >>> x = 1
    >>> try:
    ...     1/0
    ... except Exception as x:
    ...     pass
    >>> x
    NameError: name 'x' is not defined
    
  • переопределить builtin и отменить переопределение

    >>> open = 99
    >>> open
    99
    >>> del open
    >>> open
    <built-in function open>
    
  • py2 fun: __builtins__.True = False

  • lambda может принимать аргументы по умолчанию

  • nonlocal можно заменить mutable объектом или аттрибутом функции

    def f():
        x = [1]
        def g():
            print x[0]
            x.append(2)
        g()
        print x
    >>> f()
    1
    [1, 2]
    
    def f():
        x = 1
        def g():
            print g.x
            g.x = 2
        g.x = x
        g()
        print g.x
    >>> f()
    1
    2
    
  • py3 keyword only arguments

    def f(*args, name):
        print("args", args)
        print("name", name)
    >>> f(1, 2)
    TypeError: f() missing 1 required keyword-only argument: 'name'
    >>> f(1, 2, name=3)
    args (1, 2)
    name 3
    
    def f(*args, name=3):
        print("args", args)
        print("name", name)
    >>> f(1, 2)
    args (1, 2)
    name 3
    
  • py3 есть распаковка при присваивании, она возвращает list, а распаковка при вызове функции возвращает tuple

    python 2 and 3

    def f(a, *b):
        print(b)
    >>> f(1, *[2, 3])
    (2, 3)
    

    python 3

    >>> a, *b = [1, 2, 3]
    >>> print(b)
    [2, 3]
    >>> a, *b = (1, 2, 3)
    >>> print(b)
    [2, 3]
    
  • добавить список в начало существующего: L[:0] = [1, 2, 3]

  • посмотреть и задать максимальный уровень рекурсии

    >>> sys.getrecursionlimit()  # 1000
    >>> sys.setrecursionlimit(10000)
    >>> help(sys.setrecursionlimit)
    
  • аргументы функций

    >>> def f(a):
    ...     b = 1
    ... 
    >>> f.__name__
    'f'
    >>> f.__code__.co_varnames
    ('a', 'b')
    >>> f.__code__.co_argcount
    1
    
  • py3 к аргументам функции можно добавить аннотации. Эти данные доступны в func.__annotations__. Автоматически ничего с этими аннотациями не происходит, но с ними можно работать вручную по ситуации (например, для проверки типа или диапазона с помощью своего декоратора)

    >>> def func(a: 'spam', b: (1, 10), c: float):
    ...     return a + b + c
    >>> func.__annotations__
    {'b': (1, 10), 'c': <class 'float'>, 'a': 'spam'}
    
    # default values
    >>> def func(a: 'spam'=4, b: (1, 10)=5, c: float=0.1):
    ...     return a + b + c
    
  • внутри lambda нельзя присвоить, но можно setattr, __dict__

  • operator module in std lib

    import operator as op
    reduce(op.add, [2, 4, 6])
    # same as
    reduce(lambda x, y: x+y, [2, 4, 6])
    
  • KISS: Keep It Simple [Sir/Stupid]

  • comprehension vs map в общем случае (лучше проверить на вашей системе)

    map(lambda x: x ..) slower than [x for x ..]

    [ord(x) for x ..] slower than map(ord for x ..)

    map(lambda x: L.append(x+10), range(10)) even slower than for x in range(10): L.append(x+10)

  • распаковка в lambda отличатся для py2 и py3

    python 2

    >>> map(lambda (a, b, c): a, [(0,1,2), (3,4,5)])
    [0, 3]
    

    python 3

    >>> list(map(lambda (a, b, c): a, [(0,1,2), (3,4,5)]))
    SyntaxError: invalid syntax
    >>> list(map(lambda a, b, c: a, [(0,1,2), (3,4,5)]))
    TypeError: <lambda>() missing 2 required positional arguments: 'b' and 'c'
    >>> list(map(lambda row: row[0], [(0,1,2), (3,4,5)]))
    [0, 3]
    
  • многие встроенные функции могут принимать генераторы, тогда не нужны дополнительные скобки

    >>> "".join(str(x) for x in [1, 2])
    '12'
    >>> sorted(str(x) for x in [1, 2])
    ['1', '2']
    

    but for args () is needed

    >>> sorted(str(x) for x in [1, 2], reverse=True)
    SyntaxError: Generator expression must be parenthesized if not sole argument
    >>> sorted((str(x) for x in [1, 2]), reverse=True)
    ['2', '1']
    
  • py3: yield from iterator (приведенные ниже функции идентичны)

    def f():
        for i in range(5):
            yield i
    
    def g():
        yield from range(5)
    
  • поместить первый элемент в конец списка

    L = L[1:] + L[:1]
    
  • zip одного списка

    >>> zip([1,2,3])
    [(1,), (2,), (3,)]
    
  • map и zip похожи

    map(lambda x,y: (x,y), S1, S2) == zip(S1, S2)
    
  • python -m script_name - запускает модуль (модуль - это файл .py, т.е. считай скрипт), который ищется в текущих путях поиска. Модуль может лежать где-нибудь в site-packages, а запустится от main (__name__ == '__main__'). Если это package (директория с __init__.py), то запустится файл __main__.py. Если такого нет, то ошибка. Некоторые модули умные и берут аргументы из командной строки, например timeit: python -m timeit '"-".join(str(n) for n in range(100))'

  • прямой возможности использовать одноименную переменную в одной функции: и глобальную и локальную нет. Можно только играть c __main__.my_global_var

    # OK
    X = 99
    def f():
        print(X)
    >>> f()
    99
    
    # ERROR
    def f():
        print(X)  # <- error
        X = 99
    >>> f()
    UnboundLocalError: local variable 'X' referenced before assignment
    
    # global everywhere
    def f():
        global X
        print(X)
        X = 88
    
    # hack with main
    def f():
        import __main__
        print(__main__.X)
        X = 88
    
  • скорость вычисления корня числа

    math.sqrt(x)  # fastest
    x ** .5  # fast
    pow(x, .5)  # slow
    
  • py3.2+ создает папку __pycache__, чтобы сохранять разные байткоды для разных версий python'а и не пересоздавать их впоследствии. В корне уже нет *.pyc.

  • .pyc для вызывающегося скрипта (__name__ = '__main__') не создается, только для import

  • порядок поиска при импорте (можно посмотреть в sys.path):

    1. home of program (+ in some versions current dir, from where program is launched, i.e. current dir)
    2. PYTHONPATH
    3. std lib dir
    4. content of any .pth file (if exists)
    5. site-packages dir
  • sys.path можно изменять в runtime, это затронет всю программу

  • python -O создает слегка опимизированный байткод .pyo вместо .pyc, он на ~5% быстрее. Также этот флаг убирает все assert'ы из кода. А так же влияет на переменную __debug__

    # main.py
    print __debug__
    assert True == False
    
    # python main.py
    True
    AssertionError
    
    # python -O main.py
    False
    
  • в py2 в функции можно сделать from some_module import *, но будет warning. В py3 - error

    # python 2
    def f():
        from urllib import *
        print('after import')
    >>> f()
    SyntaxWarning: import * only allowed at module level
    after import
    
    # python 3
    >>> f()
    SyntaxError: import * only allowed at module level
    
  • reload не обновляет объекты, загруженные с помощью from: from x import y. y не обновится после reload(x)

  • reload не обновляет c-шные модули

  • py3: в package текущей папки пакета нету в sys.path. Если модуль в пакете хочет импортировать другой модуль из этого же пакета, надо использовать относительный импорт: from . import smth. Однако, если модуль запускается как __main__, то есть.

  • py2: from __future__ import absolute_import делает поведение import в py2 таким же, как в py3. Это позволит импортировать модуль string из стандартной библиотеки в данном случае довольно просто:

    mypkg
    ├── __init__.py
    ├── main.py  # import string from std here?
    └── string.py
    
  • относительный импорт запрещен вне пакета:

    # test.py
    from . import a
    
    # python 2
    python test.py
    ValueError: Attempted relative import in non-package
    
    # python 3
    python test.py
    SystemError: Parent module '' not loaded, cannot perform relative import
    
  • минусы относительного импорта:

    • модуль, в котором используются относительные импорты нельзя использовать как скрипт (__main__). Решение: использовать в модуле абсолютный импорт с именем пакета в начале.
    • как следствие предыдущего пункта, нельзя запустить тесты, которые запускаются при запуске модуля как скрипта
  • в py3.3+ есть namespace packages. Это такие пакеты, в которых нет __init__.py. Два (или более) namespace package с одним и тем же именем могут лежать в разных директориях из sys.path. При этом модули пакетов собираются под этим именем. Если у модулей одинаковые имена - берется тот, который найден раньше в порядке sys.path. namespace пакет всегда имеет меньший приоритет над обычным пакетом (с __init__.py). Как только где-то найден обычный пакет - используется он, все найденные namespace packages отметаются. namespace пакеты медленнее импортятся, чем обычные.

    # collect modules in namespace package
    current_dir
    └── mypkg
        └── mymod1.py
    
    site-packages
    └── mypkg
        └── mymod2.py
    
    >>> import mypkg.mymod1
    >>> import mypkg.mymod2
    
    # redefine module in namespace package
    current_dir
    └── mypkg
        └── mymod1.py
        └── mymod2.py
    
    site-packages
    └── mypkg
        └── mymod2.py
    
    >>> import mypkg.mymod1
    >>> import mypkg.mymod2  # current_dir.mypkg.mymod2
    
    # regular package is used
    current_dir
    └── mypkg
        └── mymod1.py
    
    site-packages
    └── mypkg
        └── mymod2.py
    
    another-packages
    └── mypkg
        └── mymod1.py
    
    >>> import sys
    >>> sys.append('another-packages')
    >>> import mypkg.mymod1  # another-packages.mypkg.mymod1
    >>> import mypkg.mymod2
    ImportError: No module named 'mypkg.mymod2'
    
  • В py3 и в py2 new style classes (отнаследованные от object) магические методы при выполнении оператора ищутся в классе, минуя инстанс (__getattr__, __getattribute__ не вызываются). Но если явно вызвать магический метод - то вызывается от инстанса (__getattr__, __getattribute__ вызываются).

    class A(object):
        def __repr__(self):
            return "class level repr"
        def normal_method(self):
            return "class level normal method"
    
    def instance_repr():
        return "instance level repr"
    def instance_normal_method():
        return "instance level normal method"
    
    a = A()
    print(a)  # class level repr
    print(a.normal_method())  # class level normal method
    
    a.__repr__ = instance_repr
    a.normal_method = instance_normal_method
    
    print(a)  # class level repr
    print(a.normal_method())  # instance level normal method
    
    print(a.__repr__())  # instance level repr
    
  • ZODB - объектно ориентированная база данных для python объектов, поддерживает ACID транзакции (включая savepoints)

  • slice object:

    L[2:4] == L[slice(2,4)]
    
  • iteration context (for, while, ...) will try

    1. __iter__
    2. __getitem__
      class Gen(object):
          def __getitem__(self, index):
              if index > 5:
                  raise StopIteration()
              return index
      
      for x in Gen():
          print x,
      
      # output
      0 1 2 3 4 5
      
  • for вызывает __iter__(). Потом к полученному объекту вызывает __next__() (в py2 .next()), пока не получит StopIteration. В классе можно использовать __iter__(): yield ..., тогда не надо реализовать __next__

  • __call__ вызывается, когда скобки () применяются к инстансу, а не к классу

    class A(object):
        def __call__(self):
            print("call")
    
    a = A()  # nothing
    a()  # print call
    
  • __eq__ = True не подразумевает, что __ne__ = False

  • boolean context:

    • __bool__ (__nonzero__ in py2)
    • __len__
    • True
  • паттерны ООП

    • inheritance - "is a"
    • composition - "has a" (контейнер хранит другие объекты)
    • delegation - вид composition, когда хранится только один объект. Wrapper сохраняет основной интерфейс, добавляя какие-то шаги.
  • аттрибуты и методы класса, которые начинаются с двух подчеркиваний __, но не заканчиваются ими, имеют особое поведение. Они не пересекаются с одноименными аттрибутами и методами унаследованного класса. В __dict__ они попадают под именем _ClassName__attrname.

    class A(object):
        __x = 1
    
        def show_a(self):
            print self.__x
    
    class B(A):
    
        def show_b(self):
            print self.__x
    
    >>> a = A()
    >>> a.show_a()
    1
    >>> b = B()
    >>> b.show_a()
    1
    >>> b.show_b()
    AttributeError: 'B' object has no attribute '_B__x'
    
    class B(A):
        __x = 2
    
        def show_b(self):
            print self.__x
    
    >>> b = B()
    >>> b.show_a()
    1
    >>> b.show_b()
    2
    
  • в py3 можно в методе класса не указывать аргумент self, и использовать его только от имени класса (не инстанса) - он будет работать как static method. В py2 так нельзя.

    class A(object):
        def f():
            print("f")
    
    # python 2
    >>> A.f()
    TypeError: unbound method f() must be called with A instance as first argument (got nothing instead)
    
    # python 3
    >>> A.f()
    f
    >>> a = A()
    >>> a.f()
    TypeError: f() takes 0 positional arguments but 1 was given
    
  • bound function:

    class A(object):
        def f(self):
            pass
    
    a = A()
    print(a.f.__self__)  # вот где хранится self
    
  • поиск аттрибутов в classic (old-style) классах и new-style классах:

    • classic. DFLR: Depth First, Left to Right
    • new-style. Diamond pattern, L-R, D-F; MRO (хитрее, чем просто LRDF)

    MRO исключает класс, от которого унаследованны >= 2 других класса, от поиска аттрибуты дважды. Т.е. класс пападает в поиск только 1 раз.

    # python 2 old-style
    class A: attr = 1
    
    class B(A): pass
    
    class C(A): attr = 2
    
    class D(B,C): pass
    
    >>> x = D()
    >>> print(x.attr)  # x, D, B, A
    1
    
    # python 2 new-style
    class A(object): attr = 1
    
    class B(A): pass
    
    class C(A): attr = 2
    
    class D(B,C): pass
    
    >>> x = D()
    >>> print(x.attr)  # x, D, B, C
    2
    
    # scheme
    A     A
    |     |
    B     C
    \     /
       |
       D
       |
       X
    

    Посмотреть порядок поиска в new-style (по алгоритму mro):

    >>> D.__mro__
    (<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <type 'object'>)
    >>> D.mro()  # все равно что list(D.__mro__)
    [<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <type 'object'>]
    
  • format() конструкция вызывает метод __format__. Если его нет, то в py2 - TypeError.

    python 2

    >>> print('{0}'.format(object))
    <type 'object'>
    >>> print('{0}'.format(object.__reduce__))
    TypeError: Type method_descriptor doesn't define __format__
    # явно вызовим __str__
    >>> print('{0!s}'.format(object.__reduce__))
    <method '__reduce__' of 'object' objects>
    

    python 3.4

    >>> print('{0}'.format(object.__reduce__))
    <method '__reduce__' of 'object' objects>
    

    python 2 & 3

    class A(object):
        def __format__(self, *args):
            return "A.__format__"
    
        def __str__(self):
            return "A.__str__"
    
    >>> a = A()
    >>> "{0}".format(a)
    'A.__format__'
    >>> print(a)
    A.__str__
    >>> '%s' % a
    'A.__str__'
    
  • В __dict__ не попадают "виртуальные" аттрибуты:

    • new-style properties (@property)
    • slots
    • descriptors
    • dynamic attrs computed with tools like __getattr__
  • MRO - method resolution order

  • diamond pattern - разновидность 'multi inheritance', когда 2 или более класса могут быть потомками одного и того же класса (object). Этот паттерн используется в python.

  • прокси объект, который возвращает super(), не работает с операторами:

    # python 3
    class A(list):
        def get_some(self):
            return super()[0]
    
    >>> a = A([1, 2])
    >>> a.get_some()
    TypeError: 'super' object is not subscriptable
    
    class A(list):
        def get_some(self):
            return super().__getitem__(0)
    
    >>> a = A([1,2])
    >>> a.get_some()
    1
    
    # python 2
    class A(list):
        def get_some(self):
            return super(A, self)[0]
    
    >>> a = A([1,2])
    >>> a.get_some()
    TypeError: 'super' object has no attribute '__getitem__'
    
    class A(list):
        def get_some(self):
            return super(A, self).__getitem__(0)
    
    >>> a = A([1,2])
    >>> a.get_some()
    1
    
  • super()

    • Плюсы super():

      • если superclass нужно изменить в runtime, без super не получится: C.__bases__ = (Y, )
      • когда нужно вызвать цепочки унаследованных методов в multi inheritance классе, в порядке MRO.

        Если попытаться вызвать без super, то можем вызвать метод какого-то класса дважды.

        class A(object):
            def __init__(self):
                print("A")
        
        class B(A):
            def __init__(self):
                print("B")
                super(B, self).__init__()
        
        class C(A):
            def __init__(self):
                print("C")
                super(C, self).__init__()
        
        class D(B, C):
            pass
        
        >>> d = D()
        B
        C
        A
        >>> B.mro()
        [<class '__main__.B'>, <class '__main__.A'>, <type 'object'>]
        >>> D.mro()
        [<class '__main__.D'>, <class '__main__.B'>, <class '__main__.C'>, <class '__main__.A'>, <type 'object'>]
        

        Вызов цепочки методов

        class B(object):
            def __init__(self):
                print("B")
                # for B super is C here, by MRO order
                super(B, self).__init__()
        
        class C(object):
            def __init__(self):
                print("C")
                # it is ok here to call super().__init__
                # because object also has __init__
                super(C, self).__init__()
        
        class D(B, C):
            pass
        
        >>> d = D()
        B
        C
        
      • super будет искать метод в иерархии MRO. Он будет искать пока не найдет или пока не упрется. Т.е. допустим иерархия для super: A, С, и в A нет метода, а в C есть, то вызовится C.method без ошибки

    • минусы (или особенности) super():

      • при использовании super все методы в цепочке должны принимать одинаковые аргументы
      • super().m - все классы должны иметь метод m и вызывать super().m, кроме последнего, который вызывать super не должен
  • унаследовать метод от конкретного класса:

    class A(B, C):
        other = C.other  # not B other
    
  • finally вызовится даже если исключение случается в except блоке или else блоке

  • исключение - всегда instance, даже если raise ExceptionClass (без ()), автоматически (без аргументов) создается instance:

    raise Exception  # == raise Exception()
    raise  # возбуждается перехваченное исключение
    
  • py2, посмотреть исключения builtin:

    import exceptions
    help(exceptions)
    
  • минус чтения байтов из файла с последующим ручным decode в том, что если мы будем читать по кускам, то может быть сложный случай, когда один байт одного символа попадает в один кусок, а другой - в другой. Поэтому в py2 лучше использовать codecs.

  • Когда имя файла даем в unicode, python автоматически декодит и кодит в байты. Когда имя файла - байты, то не кодит нечего. encoding для имен файлов (дефолтный):

    >>> sys.getfilesystemencoding()
    'utf-8'
    
  • дескриптор - это класс, который реализует хотя бы один из методов

    • __get__
    • __set__
    • __delete__
  • Если в дескрипторе не реализовать __set__, то это еще не значит, что соответствующий аттрибут будет read-only. Аттрибут просто перезатрется. Надо делать __set__ с исключением.

  • декоратры можно совмещать, тогда они будут применяться в порядке снизу вверх:

    @A
    @B
    @C
    def f(): pass
    
    # все равно что
    
    f = A(B(C(f)))
    
  • декоратор может принимать аргументы. Реализовать можно через вложенные функции

    @dec(a, b)
    def f(): pass
    
    # все равно что
    
    f = dec(a, b)(f)
    
    # реализация:
    
    def dec(a, b):
        def actual_dec(f):
            return f
        return actual_dec
    

    Т.е. декоратор может включать 3 уровня callables:

    • callable to accept decorator args
    • callable to serve as decorator
    • callable to handle calls to the original function
  • при создании класса вызываются два метода класса type:

    type.__new__(type_class, class_name, super_classes, attr_dict)
    type.__init__(class, class_name, super_classes, attr_dict)
    
    # python 3
    class Eggs: pass
    
    class Spam(Eggs):
        data = 1
        def method(self, arg): pass
    
    # все равно, что
    Eggs = type('Eggs', (), ...)  # в () object добавится автоматически
    
    Spam = type('Spam', (Eggs, ), {'data': 1, 'method': method, '__module__': '__main__'})
    
  • Задать метакласс для класса

    python 2

    class Spam(object):
        __metaclass__ = Meta
    

    Наследовать от object не обязательно, но если его нет, а __metaclass__ есть, то результат все равно будет new-style, и в __bases__ будет object. Но лучше явно указать object, т.к. могут быть проблемы, например с наследованием.

    python 3

    class Spam(Eggs, metaclass=Meta):
        pass
    

    аттрибут __metaclass__ просто игнорируется

  • Метакласс сам не обязательно должен быть классом. Просто его вызов должен возвращать класс. Это может быть и функция:

    def meta_func(class_name, bases, attr_dict):
        return type(class_name, bases, attr_dict)
    
    # python 2
    class Spam(object):
        __metaclass__ = meta_func
    
  • У обычных классов тоже есть метод __new__. Но он не создает класс, он вызывается при создании инстанса класса (получает готовый класс в качестве аргумента). Он же и вызывает __init__.

  • Магические методы метакласса и класса:

    class Meta(type): pass
    

    при создании класса Class (class Class(metaclass=Meta): ...) вызываются методы

    Meta.__new__
    Meta.__init__
    

    при создании инстанса класса Class (instance = Class(...)) вызываются методы (внешний вызывает вложенный)

    Meta.__call__
        calls Class.__new__
            calls Class.__init__
    

    при вызове инстанса класса Class (instance()) вызывается метод

    Class.__call__
    
  • Метакласс можно не наследовать от type, а определить метод __new__. Но тогда методы __init__, __call__ нашего метакласса не будут вызываться:

    class MySimpleMetaClass(object):
        def __new__(cls, *args, **kwargs):
            new_class = type.__new__(type, *args, **kwargs)
            return new_class
    
        def __init__(new_class, *args, **kwargs):
            print("__init__ won't be called...")
    
        def __call__(*args, **kwargs):
            print("__call__ won't be called...")
    
  • Метакласс класса вызывается и для всех потомков класса. Когда __new__ метакласса вызывается для родительского класса, в bases будет (<type 'object'>,), а для дочернего класса - класс родителя.

  • Аттрибуты метакласса наследуются классом, но не инстансами класса.

    python 2 (в python 3 небольшие отличия в синтаксисе)

    class MyMetaClass(type):
        attr = 2
    
        def __new__(*args, **kwargs):
            return type.__new__(*args, **kwargs)
    
        def toast(*args, **kwargs):
            print(args, kwargs)
    
    class A(object):
        __metaclass__ = MyMetaClass
    

    Метакласс входит в цепочку поиска аттрибутов класса

    >>> A.toast()
    ((<class '__main__.A'>,), {})
    

    Интересно, что метод от метакласса - bound, хотя вызывается от класса, не от инстанса. На самом деле класс - это инстанс метакласса:

    >>> A.toast
    <bound method MyMetaClass.toast of <class '__main__.A'>>
    

    Но метакласс не входит в цепочку поиска аттрибутов инстанса класса

    >>> a = A()
    >>> a.toast()
    AttributeError: 'A' object has no attribute 'toast'
    

    Если в каком-нибудь super классе объявлен аттрибут с тем же именем, что и в метаклассе, то он имеет преимущество (не важно насколько глубоко super)

    class B(object):
        attr = 1
    
    class C(B):
        __metaclass__ = MyMetaClass
    
    >>> C.attr
    1  # MyMetaClass.attr = 2 is ignored
    

    Аттрибуты инстанса ищутся в его __dict__, дальше в __dict__'ах __class__.__mro__. Аттрибуты класса ищутся еще и в __class__.__mro__, это другой класс, со стороны инстанса это будет __class__.__class__.__mro__.

    >>> inst = C()
    >>> inst.__class__ -> <class '__main__.C'>
    >>> C.__bases__    -> (<class '__main__.B'>,)
    >>> C.__class__    -> <class '__main__.MyMetaClass'>
    

    Инстансы наследуют аттрибуты от всех суперклассов. Классы - от суперклассов и метаклассов. Метаклассы - от супер-метаклассов (и вероятно от мета-метаклассов).

    Data descriptor'ы (те, которые определяют __set__) вносят небольшие поправки в порядок поиска аттрибутов для инстанса. Для инстанса, data descriptor будут иметь преимущество в поиске, даже если они объявлены в супер классах:

    class DataDescriptor(object):
        def __get__(self, instance, owner):
            print("DataDescriptor.__get__")
            return 5
        def __set__(self, instance, value):
            print("DataDescriptor.__set__", value)
    
    class B(object):
        attr = DataDescriptor()
    
    class C(B):
        pass
    
    >>> c = C()
    >>> c.__dict__['attr'] = 88
    >>> c.attr
    DataDescriptor.__get__
    5
    >>> c.attr = 8
    ('DataDescriptor.__set__', 8)
    

    Вызвался дескриптор, несмотря на то, что мы задали аттрибут с тем же именем в c.__dict__. Аттрибут не затер дескриптор суперкласса, сработал дескриптор. Такого поведения не будет с обычным дескриптором (nondata):

    class SimpleDescriptor(object):
        def __get__(self, instance, owner):
            print("SimpleDescriptor.__get__")
            return 5
    
    class B(object):
        attr = SimpleDescriptor()
    
    class C(B):
        pass
    
    >>> c = C()
    >>> c.attr
    SimpleDescriptor.__get__
    5
    >>> c.__dict__['attr'] = 88
    >>> c.attr
    88
    

    Так же, для builtin операторов, которые вызывают магические методы, поиск особый. Он минует instance.__dict__, сразу идет поиск в __dict__ классов из __mro__.

  • магические методы, которые вызываются неявно путем использования builtin операторов для классов ищутся в метаклассе, минуя сами классы (сам класс и всего его суперклассы):

    python 2 (в python 3 небольшие отличия в синтаксисе)

    class MyMetaClass(type):
        def __new__(*args, **kwargs):
            return type.__new__(*args, **kwargs)
        def __str__(cls):
            return "__str__ from meta"
    
    class A(object):
        __metaclass__ = MyMetaClass
        def __str__(self):
            return "__str__ from class A"
    

    Вызывается метод __str__ метакласса, а не класса:

    >>> print A
    __str__ from meta
    

    А тут вызывается метод __str__ от object:

    >>> print MyMetaClass
    <class '__main__.MyMetaClass'>
    
  • Автор Марк Лутц немного беспокоится, что python становится слишком сложным и обрастает дублирующим функционалом, например:

    • str.format и %
    • with и try/finally

    Это противоречит import this

Опубликовано: Апрель 30, 2015
Bookmark and Share
Comments powered by Disqus