Метка: баг

Циклы и замыкания Python

Когда вы определяете функцию внутри другой функции и используете локальные переменные внешней функции во вложенной, вы создаете замыкание. Время жизни этих переменных «продляется» в особой области видимости enclosing даже после завершения работы внешней функции. Пример: make_adder возвращает функцию-прибавлятор. Объект из переменной a будет жить и работать даже после выхода из make_adder:

def make_adder(a):
    def adder(x):
        return a + x
    return adder

plus_5 = make_adder(5)
print(plus_5(3))  # 8

Здесь я хочу коснуться одной популярной проблемы. Дело в том, что если мы создадим несколько функций внутри одного контекста, то они будут разделять одну область видимости enclosing. Рассмотрим пример создания трех функций в цикле:

def make_adders():
    adders = []
    for a in range(3):
        def adder(x):
            return a + x
        adders.append(adder)
    return adders

adders = make_adders()
for adder in adders:
    print(adder(2))  # 4 4 4

Вместо функций прибавляющих разные числа от 0 до 2, мы получили 3 одинаковых функции, потому что внутри себя они поддерживают ссылку на одну и ту же переменную a, значение которой останется равным 2 после выполнения всего цикла целиком.

Есть простой прием, помогающий «зафиксировать» значения переменной в моменте: достаточно добавить во вложенную функцию дополнительный аргумент со значением по умолчанию, равным нужной переменной a=a:

def make_adders():
    adders = []
    for a in range(3):
        def adder(x, a=a):  # FIX!
            return a + x
        adders.append(adder)
    return adders

adders = make_adders()
for adder in adders:
    print(adder(2))  # 2 3 4

Еще лучше переименовать аргумент, чтобы избежать конфликтов имен и замечаний IDE, например, так:

def adder(x, that_a=a):  # FIX!
    return that_a + x

yield

Пока писал код для этого поста, я наткнулся на одну обманку. Люблю оформлять функции, возвращающие коллекции, как генераторы с ключевым словом yield. Вот так:

def make_adders():
    for a in range(3):
        def adder(x):
            return a + x
        yield adder

adders = make_adders()
for adder in adders:
    print(adder(2))  # 2 3 4

Видите, тут нет фикса a=a! Казалось бы, что код должен также содержать в себе баг и выводить «4 4 4», но он работает, как задумано изначально.

Однако, если мы применим list к генератору, извлекая все значения разом, то баг вернется:

adders = list(make_adders())
for adder in adders:
    print(adder(2))  # 4 4 4

Разгадка. В первом случае происходят следующие действия:

  • a = 0
  • yield функцию (a + x), make_adders становится на паузу
  • печать adder(2) = 0 + 2 = 2
  • make_adders запускается
  • a = 1
  • yield функцию (a + x), пауза
  • печать adder(2) = 1 + 2 = 2
  • … и так далее…

То есть мы запускаем adder только один раз в тот момент, пока переменная a еще равна нужному значению. 

Во втором код list прокручивает make_adders до конца, оставляя a = 2, и все функции выдают одинаковый результат.

Вывод мы должны сделать такой: yield не создает нового замыкания с отдельной переменной a и не освобождает нас от ответственности следить за переменными.

Еще кое-что. 

adders = make_adders()
for adder in adders:
    print(adder(2))  # 2 3 4

next(adders)  # StopIteration

После исполнения цикла в коде выше, генератор adders будет исчерпан. В нем больше не останется значений, и если еще раз запустить цикл по adders, то он пройдет ровно 0 итераций. 

Генератор – вещь одноразовая.

Специально для канала @pyway. Подписывайтесь на мой канал в Телеграм @pyway 👈 

Решение проблемы с неудаляемыми играми

Существует официальный баг в Game Center API, когда из списка не удаляется GKTurnBasedMatch. Баг достаточно распространенный, он происходит главным образом тогда, когда активный участник покидает игру (вызов participantQuitInTurnWithOutcome:…) и удаляет ее из своего списка матчей. Игра получает какое-то неверное состояние, после чего остальные игроки не могут ее удалить из списка. Причем не могут как из интерфейса матч-мэйкера, так и вызовом removeWithCompletionHandler:… Возвращается ошибка номер 17 (Error=17) о том, что какие-то параметры заданы неверно. При этом сам матч может иметь уже статус завершенного GKTurnBasedMatchStatusEnded!

Как я уже отметил, Apple призналась в баге, но не исправила до сих пор.

Баг замечен, например, в игре Ой Ё. Об этом было сказано в их блоге. Мы тоже достаточно быстро столкнулись с этой проблемой, и она висела нерешенной почти два месяца. Тут, наконец-то, изучая многочисленные форумы мне удалось найти решение.

Суть в том, чтобы несмотря на завершенность матча попытаться (в случае возникновения ошибки) завершить свой ход, передав его любому другому участнику, затем выйти из игры ВНЕ хода и снова отправить запрос на завершения матча. Этот workaround помог мне со всеми неудаляемыми играми!

Вот код:

- (void) removeGameFromGC
{

    [_match removeWithCompletionHandler:
     ^(NSError *error) {
         if(!error)
         {
             DLog(@"match removed!");
         }
         else
         {
             [self reportError:error];

             GKTurnBasedParticipant *nextParticipant = nil;
             for (GKTurnBasedParticipant *participant in _match.participants)
             {
                 participant.matchOutcome = GKTurnBasedMatchOutcomeTied;
                 if(![TurnBasedHelper isHeLocal:participant.playerID])
                     nextParticipant = participant;
             }

             [_match endTurnWithNextParticipants:@[nextParticipant]
                                     turnTimeout:GKTurnTimeoutDefault
                                       matchData:nil
                               completionHandler:
              ^(NSError *error)
              {
                  if (error)
                  {
                      [self reportError:error];
                  }

                  [_match participantQuitOutOfTurnWithOutcome:GKTurnBasedMatchOutcomeQuit
                                        withCompletionHandler:
                   ^(NSError *error)
                   {
                       if (error)
                       {
                           [self reportError:error];
                       }
                       [_match removeWithCompletionHandler:^(NSError *error)
                        {
                            if (error)
                            {
                                [self reportError:error];
                            }
                        }];
                   }];
              }];
         }

     }];
}