Пользователь
0,0
рейтинг
8 сентября 2013 в 01:24

Разработка → Python. Неочевидное поведение некоторых конструкций

Рассмотрены примеры таких конструкций + некоторые очевидные, но не менее опасные конструкции, которых в коде желательно избегать. Статья рассчитана на python программистов с опытом 0 — 1,5 года. Опытные разработчики могут в коментах покритиковать или дополнить своими примерами.

1. Lambda.
переменная pw в lambda — ссылка на переменную, а не на значение. К моменту вызова функции переменная pw равна 5
Проблемный код
to_pow = {}
for pw in xrange(5): 
    to_pow[pw] = lambda x: x ** pw 
print to_pow[2](10)  # 10000 ??? 

Решение: Передавать все переменные в lambda явно
to_pow = {}                       
for pw in xrange(5):
    to_pow[pw] = lambda x, pw=pw: x ** pw 
print to_pow[2](10)  # 100 



2. Отличный порядок поиска атрибутов при ромбоидальном наследовании в классах классического и нового стиля

class A():
    def field(self):
        return 'a'
  
class B(A):
     pass
  
class C(A):
     def field(self):
        return 'c'
  
class Entity(B, C):
    pass
 
print Entity().field()  # a !!!

class A():
    def field(self):
        return 'a'
  
class B(A):
     pass
  
class C(A, object):  # New style class
     def field(self):
        return 'c'
  
class Entity(B, C):
     pass
 
print Entity().field()  # c !!!



3. Изменяемые объекты в качестве значений по умолчанию
Магия:
def get_data(val=[]):

    val.append(1)
    return val

print get_data()  # [1]
print get_data()  # [1, 1]    ???
print get_data()  # [1, 1, 1]    ???

Решение:
def get_data(val=None): 
    val = val or []
    val.append(1)
    return val 

print get_data()  # [1]  
print get_data()  # [1]


val = val or [] выглядит короче и вполне приемлем, но если на входе в функцию не передаются 0, пустая строка, False и т.д. Тогда надо делать проверку is None, как и описано в gogle-style google-styleguide.googlecode.com/svn/trunk/pyguide.html?showone=Default_Argument_Values#Default_Argument_Values

(Кто не читал этот документ — советую обязательно заглянуть.)

4. Значения по умолчанию инициализируются единожды
import random
def get_random_id(rand_id=random.randint(1, 100)):
    return rand_id
  
print get_random_id()  # 53
print get_random_id()  # 53 ??? 
print get_random_id()  # 53 ???



5. Не учтена иерархия исключений. Если вы не держите в голове подобные списки docs.python.org/2/library/exceptions.html#exception-hierarchy + списки исключений встроенных модулей + иерархию исключений вашего приложения, а также не используете PyLint. То можно написать следующее:

KeyError никогда не отработает

try:
    d = {}
    d['xxx']
except LookupError:
    print '1'
except KeyError:
    print '2'



6. Кэширование интерпретатором коротких строк и чисел

str1 = 'x' * 100
str2 = 'x' * 100
print str1 is str2  # False
  
str1 = 'x' * 10
str2 = 'x' * 10
print str1 is str2  # True ???


7. Неявная конкатенация.
Пропущенная запятая не генерирует исключений а приводит к слиянию смежных строк. Такая ситуация может произойти когда после последнего элемента в кортеже не поставили необязательную запятую, а затем строки в кортеже перегруппировали, отсортировали
tpl = (
    '1_3_dfsf_sdfsf',
    '3_11_sdfd_jfg',
    '7_17_1dsd12asf sa321fs afsfffsdfs'
    '11_19_dfgdfg211123sdg sdgsdgf dsfg',
    '13_7_dsfgs dgfdsgdg',
    '24_12_dasdsfgs dgfdsgdg',
)


8. Природа булевого типа.
True и False это самые настоящие 1 и 0, для которых придумали специальные название для большей выразительности языка.
try:
    print True + 10 / False * 3
except Exception as e:
    print e  # integer division or modulo by zero


>>> type(True).__mro__
(<type 'bool'>, <type 'int'>, <type 'object'>)


docs.python.org/release/2.3.5/whatsnew/section-bool.html
Python's Booleans were added with the primary goal of making code clearer.

To sum up True and False in a sentence: they're alternative ways to spell the integer values 1 and 0, with the single difference that str() and repr() return the strings 'True' and 'False' instead of '1' and '0'.


8*. Устаревшая конструкция. Опасна тем что потеря слэша, либо перенос только первой части выражения не приводит в очевидным ошибкам. Как решение — использовать круглые скобки.
x = 1 + 2 + 3 \
+ 4

ps: номер пункта 8* обусловлен тем что изначально в статье присутствовало по ошибке два 7-ых пункта. Чтоб не сбивать нумерацию (так как на неё уже есть ссылки в коментах) пришлось ввести такое обозначение.

9. Операторы сравнения and, or в отличии например от PHP не возвращают True или False, а возвращают один из элементов сравнения, в этом примере current_lang будет присвоен первый положительный элемент
current_lang =  from_GET or from_Session or from_DB or DEFAULT_LANG


Такое выражение как альтернатива тернарному оператору так же будет работать, но стоит обратить внимание, что если первый элемент списка окажется '', 0, False, None, то будет возвращён последний элемент в сравнении, а нет первый элемент списка.
 a = ['one', 'two', 'three']
print a and a[0] or None  # one


10. Перехватить все исключения и при этом никак их не обработать. В этом примере ничего необычного не происходит, но такая конструкция таит в себе опасность и весьма популярна среди начинающих. Так писать не стоит, даже если вы уверены на 100% что исключение можно никак не обрабатывать, так как это не гарантирует что другой разработчик не допишет в блок try-except строку, исключение от которой хотелось бы всё таки зафиксировать. Решение: пишите в лог, конкретизируйте перехватываемый тип исключений, обрамляйте в try-except лишь минимально необходимый кусок кода.

try:
    # Много кода, чем больше тем хуже
except Exception:                                                           
    pass


11. Переопределение объектов из built-in. В данном случае переопределяется объекты list, id, type. Использовать классические id, type в функции и класс list в модуле привычным образом не получится. Как решение — установить PyLint в свою IDE и следить что он подсказывает.

def list(id=DEFAULT_ID, type=TYPES.ANY_TYPE):                                                                       
    """                                                                                        
    W0622 Redefining built-in "id" [pylint]                                                    
    """                                                                                        
                                                                                               
    return Item.objects.filter(id=id, item_type=type)  


Если уж переписать код никак нельзя, то обращаться к built-in функциям придётся так:
def list(id=None):
    print(__builtins__.id(list))


12. Работающий код, без сюрпризов… для вас… А вот другой разработчик, использующий mod2.py весьма удивится, заметив, что один из атрибутов модуля вдруг неожиданно изменился. Как решение стараться избегать таких действий, или хотяб вводить в mod2.py функцию для переопределения атрибута. Тогда изучая mod2.py можно будет хотя б понять, что один из атрибутом модуля может меняться.

    # mod1.py                                                                                  
    import mod2                                                                                
    mod2.any_attr = '123' 


UPD: Полезная ссылка по теме от lega: Hidden features of Python

PS: Критика, замечания, дополнения приветствуются.
Максим @niko83
карма
22,0
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

Подробнее
Реклама

Самое читаемое Разработка

Комментарии (53)

  • +1
    Не соглашусь насчёт True и False: в Python последних версий это, всё-таки, отдельный булев тип:

    >>> type(True)
    <type 'bool'>
    >>> type(1)
    <type 'int'>
    >>> 1 is 1
    True
    >>> True is 1
    False
    

    Внутренняя кухня для булевых типов довольно нетривильна (например, тип bool является подклассом int, для объектов существует понятие truth value, чьё поведение можно переопределить через методы __nonzero__/__bool__ и т.д.), но для простоты можно считать, что любой тип может быть сконвертирован в bool, а bool, в свою очередь, в арифметических операциях может быть сконвертирован в int (конкретно, в 1 или 0).

    А вообще, спасибо за отличное выступление на митапе — многих вещей действительно не знал :)
    • 0
      Рад что понравилось,
      Спасибо.
      Дополнил пункт 8 (True и False) ссылкой на первоисточник и кусочком кода
    • 0
      Насчёт True и False. Обычно ожидаешь, что True будет соответствовать всему, что не является False (0, [], '' и т.д.). Однако если 1 == True даёт True, то 5 == True вернёт False. Есть тут неконсистентность.
      Проблема чисто методологическая, думаю сравнивать между собой переменные разных типов (int и bool, например) – не лучшая идея. Хотя в С уже давно придумали, как правильно приводить переменные к набору (0, 1), но на Python это будет слишком длинно. Вместо !!var нужно писать not not var.
      Это актуально для Python 2.7.
      • +2
        Однако если 1 == True даёт True, то 5 == True вернёт False. Есть тут неконсистентность.

        >>> True is 1
        False
        >>> True is 5
        False


        но на Python это будет слишком длинно. Вместо !!var нужно писать not not var.

        bool(var)
        

        а для условий и этого не надо:
        if var:
        

  • +1
    1. Не знал, спасибо.
    10. Проблема существует, потому что стандартная библиотека плохо документирована по части исключений, которые могут возникать в той или иной функции, не говоря о сторонних библиотеках.
    11. Не вижу в этом большой проблемы.

    PyCharm, как минимум, 3, 4, 6, 7, 10 и 11 выявляет по умолчанию из коробки.
    • 0
      по поводу 11 приведу пример:
      отредактировал кто-то модуль, и ввёл там на уровне модуля функцию bytearray не зная что такая есть в docs.python.org/2/library/functions.html#built-in-functions (при этом в модуле нет __all__), а где-то у кого-то возьми да и используется (ну осталось так в коде с давних пор, ничего не поделать)
      from module import *
      и случится магия, если вдруг у этого кого-то в его модуле и используется родная bytearray из built-in, которая перезатрётся новой реализацией.
      Хорошо если баг всплывёт сразу…
      Неочевидного поведения тут нет, а отхватить проблему, которая непойми где и как вылезет можно.
      • 0
        Там в примере 11 большая разница между list и id / type: функция list создает проблемы пользователю библиотеки (т.к. она перекрывает list в области видимости модуля), а параметры id/type — нет, т.к. они перекрывают id/type только в области видимости функции (эта область видимости пользователю явным образом не доступна). Пользователю нужно называть аргументы функции id/type при вызове, но это не ведет ни к какому перекрытию, т.к. это, по сути, просто создание словаря с ключами 'id' и 'type'.

        Такие названия аргументов могут создать проблемы разработчику функции, но это менее серьезная проблема, т.к. id и type используются очень редко (и часто неправильно, вместо is и isinstance), да и ошибку в короткой функции заметить проще. Но это нужно иметь в виду, да.

        Схожая проблема:

        class Foo(object):
            id = 'foo'
            id_upper = id.upper()
        
            def foo(self, x):
                return id(x)
        

        Тут builtin «id» перекрывается, но только на время, пока создается класс (выполняется его тело); после создания класса (например, после импорта) перекрытия уже нет: id теперь доступен как атрибут Foo, т.е. опять это, по сути, ключ в словаре, который в пользовательском коде «намусорить» не может (ну разве что пользователь locals/globals обновит сам). Строка «return id(x)» во время создания класса не выполняется, так что функция Foo.foo вызывает builtin.
  • +2
    3 нехорошо
    print get_data(0)  # [1]
    
    # лучше в get_data
    val = val if not val is None else []
    


    Вообще 3 и 4 идет из одного корня — инициализация значений по умолчанию при импорте функции
    • 0
      Соглашусь, На случай если ожидается на входе пустая строка или там 0, то лучше так и писать как вы предлагаете, В python google-styleguide имеено проверка на is None.
      google-styleguide.googlecode.com/svn/trunk/pyguide.html?showone=Default_Argument_Values#Default_Argument_Values
      Добавлю в статью.
    • +2
      Python поддерживает и более «естественный» синтакс:
      val is not None
      
      вместо
      val not is None
      
      • 0
        Опечетка: в последней строке читать «not val is None»
      • 0
        ага, но я пишу так, чтоб не путаться как писать — то ли val is not None, то ли val not is None
        • 0
          У них разный приоритет, вроде как.
  • +1
    Спасибо большое за обзор, добавлю в список документов, которые стоит перечитать перед собеседованием:)
    О некоторых вещах знал, о некоторых — нет, но настоящий ужас вызвал лишь 6-й пункт. Бегло загуглив, не нашел объяснения, не могли бы вы подробнее рассказать об этой штуке, чем это обосновано и т.п.?
    • 0
      Ээээ… 6 пункт реально прикольный. Вот так ищешь, ищешь баг в коде… А он вот таким оказывается. В чём проблема?
    • +5
      Да какой там ужас, просто в нормальном коде сравнение двух чисел по is и так встречаться не должно…

      А так — объяснение достаточно простое. В случае чисел, например, с точки зрения интерпретатора Python, экземпляры даже примитивных типов вроде int «забоксены» как экземпляр PyObject. Поэтому в какой-то момент некоторый набор чисел как бы "заинтернили", чтобы, если такое число используется в программе, сэкономить на памяти под него. В некоторых случаях такой intern пытается произойти автоматически и аналогично сэкономить память.

      И, опять про int-ы — как полезный сайд-эффект (и благодаря упомянутому факту, что True и False — это числа), это ещё даёт возможность сравнивать с True и False по is :)
    • +5
      А вы вот еще что попробуйте:

      print int("10") is int("10")
      True
      
      print int("1000") is int("1000")
      False
      
      print 1000 is 1000
      True
      
      print 1000+5 is 1000+5
      False
      


      Так происходит, потому в момент инициализации интерпритатор создает объекты типа int (до 256) и короткие строки. Соответственно, при создании объекта, который уже есть в кеше, создается только указатель на этот объект. А оператор is это сравнение указателей, а не полное сравнение объектов.
      Если же объекта в кеше нет, то интерпритатор создаст два разных объекта в памяти

      print id(1000+5), id(1000+5)
      36179924 36179912
      
      print id(10), id(10)
      30900252 30900252
      


      Вывод: используйте is, только при проверке на None.
      • +3
        С чего бы это? Используйте is для того, для чего он предназначен — сравнения object identity объектов (т.е., ссылаются ли две ссылки на один и тот же объект). Чаще всего это действительно нужно для None, но бывает полезно и во многих других случаях.

        Просто надо понимать, что для immutable типов (к каковым относится и int, и str) этот оператор бессмысленен по определнию.
      • +5
        Если быть точным, то интерпретатор CPython предсоздаёт объекты для чисел от -5 до 256 включительно. Вот небольшой кусок Objects/longobject.c исходников Python 3.2.5:

        #define NSMALLPOSINTS           257
        ...
        #define NSMALLNEGINTS           5
        ...
        /* Small integers are preallocated in this array so that they
           can be shared.
           The integers that are preallocated are those in the range
           -NSMALLNEGINTS (inclusive) to NSMALLPOSINTS (not inclusive).
        */
        static PyLongObject small_ints[NSMALLNEGINTS + NSMALLPOSINTS];
        


        Это лишь всего одна небольшая оптимизация конкретной реализации языка Python. Например, PyPy поступает по-другому.

        Кстати, раз объекты предсоздаются, мы можем использовать модуль ctypes чтобы поменять значения в них.

        Для третьего питона:
        >>> import ctypes
        >>> ctypes.c_long.from_address(id(7)+24).value=140
        >>> print(7+7)
        280
        


        Для второго:
        >>> import ctypes
        >>> ctypes.c_int.from_address(id(7)+16).value=77
        >>> print 7 + 7
        154
        
        
  • +1
    А как вам это сравнение яблок с апельсинами?

    >>> None < 1
    True
    

    В Python 3 поправлено:

    >>> None < 1
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    TypeError: unorderable types: NoneType() < int()
    
    • НЛО прилетело и опубликовало эту надпись здесь
  • +1
    10 еще чревато тем, что в Python SyntaxError — обычное исключение.
  • +5
    Не так давно перечитал спецификацию Питона и по некоторым местам, которые показались мне неочевидными или интересными сделал тест. После каждого ответа выводится верный ответ и ссылка на нужный пункт спецификации.
    • +1
      Укажите в начале, пожалуйста, что тест по третьему питону, т.к. есть некоторые различия.
      • 0
        Перепроверил, в двух вопросах, специфичных для третьего питона подписал что они для третьего питона. Спасибо.
        • 0
          Забыли 11, 12.
          • 0
            Вопросы 11 и 12 — именно те два вопроса, текст которых я поменял. Попробуйте ctrl-f5
  • +1
    Пункт 2 специфичен только для второго питона, в третьем отказались от классического стиля наследования классов. Т.е. в третьем питоне можно не наследоваться явно от object
  • +3
    Первый случай — самое очевидное поведение. К примеру, в Javascript оно идентично.
    Было бы странным, если бы поведение было иным.

    Случаи #3 и #4 рассматривается в любой литературе по изучению питона при изучении функций, поэтому его язык никогда не повернулся бы назвать неочевидным. Неочевидным оно могло быть лишь для того, кто программирует «по примеру» и документацию не читал.

    Случай #5 — обычное поведение во всех языках, где есть исключения.
    Есть правило: используешь исключение — должен знать его иерархию.

    • +1
      А вот такое поведение для большинства языков действительно неочевидно:

      first, *rest, last = '0123'
      
      print(first, rest, last)
      
      • 0
        Интересно, только в 3ем питоне
      • 0
        Вполне логичная распаковка. Это как /^(.)(.*)(.)$/
      • 0
        Строка это iterable, конечно она распаковывается, чтож тут неочевидного. Это не шибко тянет на паттерн-матчинг, потому что вторую звёздочку мы уже не поставим, да и нельзя даже указывать макс. число символов в группу.
        • 0
          Конечно Python в этом нет ничего неочевидного, я имел ввиду сравнение с другими языками:

          std::string first, last = "01";
          std::cout << first << ',' << last  << std::endl; // , 01
          
          • 0
            Ну так в С++ распаковки нет вообще. Синтаксис-то разный.
            • 0
              Распаковка в С++ есть, но она используется иначе (variadic templates).

              Хорошое сравнение можно сделать с ES6:

              let data => [1, 2, 3, 4, 5];
              let [first, ...rest, last] = data();
              

              Хотя так нельзя писать:
              let first, ...rest, last = data();
              


              • 0
                Variadic templates — страшная штука. Аргумент принят, возражений не имею :)
    • 0
      Вот не соглашусь, что первое поведение очевидно.
      Видимость `pw` ограничена телом `for`, так что логично предположить, что на каждой итерации это новая переменная.
      К тому же текущее поведение абсолютно бесполезно и приводит только к ошибкам.

      В scala, например, поведение как раз такое, что переменная каждый раз новая.
      В C# поведение было аналогичным описанному в питоне. Это общепризнанный баг языка и его пофиксили в 5.0.
      • +1
        Видимость pw не ограничена телом for.
        • 0
          Да? Не знал.

          Ну опять таки зря.
          Выцеплять таким образом значение на последней итерации — криптокод, так что использовать все равно не стоит.
      • 0
        В JS и Python область видимости ограничена функцией, а не блоком.
  • 0
    К моменту вызова функции переменная pw равна 5

    Тема с лексическими замыканиями не раскрыта.
    Для полного понимания приведу пример:

    to_pow = [(lambda x: lambda y : y ** x)(x) for x in range(5)]
    
    print(to_pow[2](10))  # 100
    

  • 0
    11) Использовать классические id, type в функции и класс list в модуле привычным образом не получится.

    Привычным — да, но можно сделать так:
    def list(id=None):
        print(__builtins__.id(list))
    

    Так же в __builtins__ можно поместить свои методы и они будут везде доступны.

    Думаю в статью можно добавить эту ссылку: Hidden features of Python
    • 0
      Спасибо, дополнил в статью ваш пример кода и ссылку
    • +1
      не надо так делать

      $ echo 'print __builtins__.id(list)' > a.py
      $ python a.py
      8692896
      
      $ echo 'import a' > b.py
      $ python b.py
      Traceback (most recent call last):
        File "b.py", line 1, in <module>
          import a
        File "/home/megabuz/a.py", line 1, in <module>
          print __builtins__.id(list)
      AttributeError: 'dict' object has no attribute 'id'
      


      Это забавная особенность питона — в импортируемых модулях __builtins__ становится словарем

      пользуйтесь __builtin__ — docs.python.org/2/library/__builtin__.html
      • 0
        Модуль __builtin__ был переименован в builtins
  • +5
    Вот неочевидное поведение:

    import this
    
  • –5
    3.
    def get_data (it = []): 
        it = it[:]
        it.append(1)
        return it
    
    • +2
      Программист не должен выполнять работу обфускатора.
      • +1
        Поведение функции меняется, т.к. она теперь всегда возвращает измененную копию от it. (если мы передали в it список «на вырост»).
        (ПС, чуть промахнулся, ответ для StreetStrider).
  • 0
    11. PEP8 в случае, если избежать использования встроенных имен ну никак не получается, предлагает дописывать к ним trailing underscore (т.е. list -> list_, class -> class_). Мне понравилось, вроде неплохо смотрится, и не нужны всякие монструозные конструкции с __builtins__
  • +1
    3. и 4. имхо одно и тоже.
    Нужно просто знать, что значения по умолчанию «исполняются» и присваиваются/резервируются за переменной только во время декларации функции. Оно даже жирным выделено в доку:
    Default parameter values are evaluated when the function definition is executed.
  • 0
    Есть отличное выступлениеНикиты Лесникова из wargaming, рассказывающее о подноготной некоторых из этих «особенностей»: www.youtube.com/watch?v=zOuxxnUY4lg

Только зарегистрированные пользователи могут оставлять комментарии. Войдите, пожалуйста.