Пользователь
0,0
рейтинг
13 мая 2015 в 23:39

Разработка → Ночные кошмары Питона: неявный `this`

Обсуждение статьи "Не совсем крутой Ruby" зашло достаточно далеко: недостатки и достоинства Ruby между делом перетекали в обсуждение недостатков и достоинств Python. Не сильно удивило то, что передача self в качестве первого аргумента метода класса, некоторым хабравчанам кажется лишней. Ну что ж, не хотите явного self, будет вам неявный this! Под катом, немного магии на чистом Python.

Но сначала, давайте всё-таки поговорим о том, почему self передаётся явным образом. Как мне кажется, причины на то две. Первая — это The Zen of Python, в котором чёрным по белому написано:
Explicit is better than implicit (явное лучше неявного).
Это относится и к передачи данного объекта в метод явным образом, через self.

Вторая причина не менее важна — это дескрипторы. ООП в Python реализован на уровне функций, которые привязываются к объекту динамически посредством механизма дескрипторов (обязательно прочтите статью Руководство к дескрипторам). Итак, вернёмся к функциям: многие ли из нас любят волшебные переменные, через которые могут передаваться аргументы функции? Это например $ в Perl, arguments в JS, func_get_args() в PHP. В Python нет таких волшебных переменных, всё, что передаётся в функцию, передаётся явным образом (в т.ч. и через *args и **kwargs). Так почему же для методов, которые Python обрабатывает как обыкновенные функции, должно быть сделано исключение в виде неявной передачи self?

Однако, в качестве упражнения сделать это совсем несложно. Давайте начнём с простого декоратора:

# Все примеры на Python 3!

def add_this(f):
    def wrapped(self, *args, **kwargs):
        f.__globals__['this'] = self
        return f(*args, **kwargs)
    return wrapped

class C:
    name = 'Alex'

    @add_this
    def say(phrase):
        print("{} says: {}".format(this.name, phrase))

c = C()
c.say('Can you believe it? There is no `self` here!')

На выходе:

Alex says: Can you believe it? There is no `self` here!

Как видите, декоратор add_this добавляет переменную this в область видимости функции, и присваивает ей значение self. Вспомните, что __globals__ — это поле ссылающееся на словарь содержащий глобальные переменные функции, т.е. глобальное пространство имён модуля, в котором эта функция объявлена. Таким образом, вышенаписанный код — это грязнющий хак, добавляющий (и затирающий!) переменную this в глобальное пространство модуля. Всё это подойдёт для наших экспериментов, но упаси вас писать такое в настоящем коде!

Предвкушая комментарии аудитории о том, что так каждую функцию придётся обрамлять в декоратор, предлагаю взвалить эту задачу на плечи метакласса:

import types

class AddThisMeta(type):
    def __new__(cls, name, bases, classdict):
        new_classdict = {
            key: add_this(val) if isinstance(val, types.FunctionType) else val
            for key, val in classdict.items()
        }
        new_class = type.__new__(cls, name, bases, new_classdict)
        return new_class

class D(metaclass=AddThisMeta):
    name = 'Daniel'

    def say(phrase):
        print("{} says: {}".format(this.name, phrase))

    def run():
        print("{} runs away :)".format(this.name))

d = D()
d.say('And now, there is only AddThisMeta!')
d.run()

На выходе:

Daniel says: And now, there is only AddThisMeta!
Daniel runs away :)

Метакласс проходит по всем полям класса и их значениям, выбирает подходящие по типу (важный момент: простая проверка на callable() не подойдёт, т.к. она также сработает для classmethod и staticmethod) и обрамляет эти функции декоратором add_this.

Как вы видите, добавить неявный self (или this) в методы классa совсем не сложно. Но прошу вас, ради всего хорошего, что есть в Python, никогда, никогда, никогда не делайте этого.
Zaur Nasibov @BasicWolf
карма
46,2
рейтинг 0,0
Реклама помогает поддерживать и развивать наши сервисы

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

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

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

  • 0
    Замечательная статья, спасибо!
  • +6
    Дык есть же

    import this
    

    зачем ещё что-то городить? ;)

    PS: для полноты ощущений можно ещё все аттрибуты тоже заглобалить:

    def add_this(f):
        def wrapped(self, *args, **kwargs):
            f.__globals__.update(self.__class__.__dict__)
            f.__globals__.update(self.__dict__)
            f.__globals__['this'] = self
            return f(*args, **kwargs)
        return wrapped
    
    class D(metaclass=AddThisMeta):
        name = 'Daniel'
        def say(phrase):
            print("{} says: {}".format(name, phrase))
    


    Правда записать в атрибуты так ничего не получится, только через this.
  • +3
    вообще, принцип «Explicit is better than implicit» и его реализация в языке — это то, за что стоит низко поклониться ван Россуму и Петерсу
    • +2
      Но иногда так хочется немного синтаксического сахара. Главное не заработать синтаксический сахарный диабет, а также знать что скрывается за конкретным кусочком сахара.
      • 0
        Import hooks вам в помощь.
        • 0
          Это была скорее общая фраза, нежели конкретно про питон. Язык программирования — это инструмент. В каких-то языках программист вынужден писать всё без какого-либо сахара, где-то его вынуждают использовать тонну сахара (иначе засмеют), а где-то этот сахар присутствует ровно в том количестве и в тех местах, что становится приятно, но не приторно.
          • 0
            Да, я понял. Я имел ввиду то, что конкретно в питоне, если очень хочется добавить свой сахар, то это возможно сделать штатными средствами.
            • +1
              А можно подробнее? Есть примеры?
              • 0
                Если коротко, то вы можете написать загрузчик для модулей, которые написаны не на питоне, а вообще на чем угодно. И в процессе загрузки транслировать это в понятный для питона код. А если подробней, то надо статью писать (уже работаю над этим).
  • +18
    Утащил в продакшен. Полёт нормальный, спасибо.
    • +1
      8O
    • +13
      Главное теперь — не делать бекапы.

      hsto.org/storage1/b8ef9be1/67c1e381/2ca10012/0cdd913b.jpg
    • 0
      На самом деле необходимо не только обновлять this, но и возвращать предыдущее значение при выходе из функции, ибо потом будет неловно, когда после вызова другого метода в вашем методе будет некорректный this

      EDIT: сори, уже вижу, что внизу описали эту проблему
  • +1
    А как это будет работать, если при рекурсивном вызове?
  • +11
    Я конечно понимаю, что это все не серьезно, но в коде есть баг — обертка, возвращаемая add_this, должна восстанавливать прежнее значение this перед возвратом результата. Если этого не делать, то вызов метода с неявным this внутри другого такого же метода затрет this первого.

    class C:
        name = 'Alex'
    
        @add_this
        def say(phrase):
            print("{} says: {}".format(this.name, phrase))
    
    
    class Echo:
        name = 'Echo'
    
        @add_this
        def say(c, phrase):
            c.say(phrase)
            print("{} says: {}".format(this.name, phrase))
    
    
    c = C()
    e = Echo()
    e.say(c, "does it work?")
    
    


    Выводит:

    Alex says: does it work?
    Alex says: does it work?
    


    Вместо:

    Alex says: does it work?
    Echo says: does it work?
    


    Что бы исправить, нужно переписать add_this вот так:

    def add_this(f):
        def wrapped(self, *args, **kwargs):
            old_this = f.__globals__.pop('this', None)
            f.__globals__['this'] = self
            result = f(*args, **kwargs)
            f.__globals__['this'] = old_this
            return result
        return wrapped
    


    P.S. Да, мне говорили, что я зануда :)
    • +1
      Согласен, но сделано это намеренно, чтобы не усложнять код. Думаю особенным мазохизмом будет запуск подобного кода в многопоточной среде :)
  • 0
    Вообще‐то для неявного this есть гораздо более безопасное решение: изменение AST (пример: habrahabr.ru/post/153949) и import hooks (либо изменение кода в setup.py перед установкой).
    • +1
      И это потокобезопасно. Ещё можно в декораторе/метаклассе «перекомпилировать» функцию, используя func.__code__, но я не вижу для этого стандартных модулей (модуль dis успешно читает func.__code__, но я не знаю, как превратить изменённый dis.Instruction в байткод). Код будет выглядеть как‐то так:

      #!/usr/bin/python3.4
      
      import dis
      
      
      def f(foo):
          print(this, foo)
      
      
      fc = f.__code__
      
      new_code = []
      
      for instruction in dis.get_instructions(fc):
          if instruction.opname in {'LOAD_GLOBAL', 'STORE_GLOBAL', 'DELETE_GLOBAL'} and instruction.argval == 'this':
              newopname = instruction.opname.replace('GLOBAL', 'FAST')
              instruction = dis.Instruction(
                  opname = newopname,
                  opcode = dis.opmap[newopname],
                  arg = 0,
                  argval = 0,
                  argrepr = 'this',
                  offset = instruction.offset,
                  starts_line = instruction.starts_line,
                  is_jump_target = instruction.is_jump_target,
              )
          elif instruction.opname in {'LOAD_FAST', 'STORE_FAST', 'DELETE_FAST'}:
              instruction.arg += 1
              instruction.argval += 1
          # Convert instruction to byte code here.
      
      new_fc = f.__code__.__class__(
          fc.co_argcount + 1,
          fc.co_kwonlyargcount,
          fc.co_nlocals + 1,
          fc.co_stacksize,
          fc.co_flags,
          b''.join(new_code),
          fc.co_consts,
          fc.co_names,
          ('this',) + fc.co_varnames,
          fc.co_filename,
          fc.co_name,
          fc.co_firstlineno,
          fc.co_lnotab,
          fc.co_freevars,
          fc.co_cellvars,
      )
      
      new_f = f.__class__(new_fc, f.__globals__)
      
      new_f(1, 2)
      
      , но не хватает критического куска на месте «convert instruction to byte code here». Кроме того, байткод официально нестабилен и данный способ может не подходить для не‐CPython реализаций Python.

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