Как стать автором
Обновить

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

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

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.
вообще, принцип «Explicit is better than implicit» и его реализация в языке — это то, за что стоит низко поклониться ван Россуму и Петерсу
Но иногда так хочется немного синтаксического сахара. Главное не заработать синтаксический сахарный диабет, а также знать что скрывается за конкретным кусочком сахара.
Import hooks вам в помощь.
Это была скорее общая фраза, нежели конкретно про питон. Язык программирования — это инструмент. В каких-то языках программист вынужден писать всё без какого-либо сахара, где-то его вынуждают использовать тонну сахара (иначе засмеют), а где-то этот сахар присутствует ровно в том количестве и в тех местах, что становится приятно, но не приторно.
Да, я понял. Я имел ввиду то, что конкретно в питоне, если очень хочется добавить свой сахар, то это возможно сделать штатными средствами.
А можно подробнее? Есть примеры?
Если коротко, то вы можете написать загрузчик для модулей, которые написаны не на питоне, а вообще на чем угодно. И в процессе загрузки транслировать это в понятный для питона код. А если подробней, то надо статью писать (уже работаю над этим).
Утащил в продакшен. Полёт нормальный, спасибо.
Главное теперь — не делать бекапы.

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

EDIT: сори, уже вижу, что внизу описали эту проблему
А как это будет работать, если при рекурсивном вызове?
Я конечно понимаю, что это все не серьезно, но в коде есть баг — обертка, возвращаемая 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. Да, мне говорили, что я зануда :)
Согласен, но сделано это намеренно, чтобы не усложнять код. Думаю особенным мазохизмом будет запуск подобного кода в многопоточной среде :)
Вообще‐то для неявного this есть гораздо более безопасное решение: изменение AST (пример: habrahabr.ru/post/153949) и import hooks (либо изменение кода в setup.py перед установкой).
И это потокобезопасно. Ещё можно в декораторе/метаклассе «перекомпилировать» функцию, используя 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.
Зарегистрируйтесь на Хабре, чтобы оставить комментарий

Публикации