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, никогда, никогда, никогда не делайте этого.
Комментарии (18)

hellman
14.05.2015 00:56+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.

Color
14.05.2015 00:59+3вообще, принцип «Explicit is better than implicit» и его реализация в языке — это то, за что стоит низко поклониться ван Россуму и Петерсу

m08pvv
14.05.2015 09:37+2Но иногда так хочется немного синтаксического сахара. Главное не заработать синтаксический сахарный диабет, а также знать что скрывается за конкретным кусочком сахара.

kr41
14.05.2015 11:08Import hooks вам в помощь.

m08pvv
14.05.2015 11:36Это была скорее общая фраза, нежели конкретно про питон. Язык программирования — это инструмент. В каких-то языках программист вынужден писать всё без какого-либо сахара, где-то его вынуждают использовать тонну сахара (иначе засмеют), а где-то этот сахар присутствует ровно в том количестве и в тех местах, что становится приятно, но не приторно.

kr41
14.05.2015 11:41Да, я понял. Я имел ввиду то, что конкретно в питоне, если очень хочется добавить свой сахар, то это возможно сделать штатными средствами.

JC_Piligrim
14.05.2015 17:02+1А можно подробнее? Есть примеры?

kr41
15.05.2015 14:11Если коротко, то вы можете написать загрузчик для модулей, которые написаны не на питоне, а вообще на чем угодно. И в процессе загрузки транслировать это в понятный для питона код. А если подробней, то надо статью писать (уже работаю над этим).

amarao
14.05.2015 02:00+18Утащил в продакшен. Полёт нормальный, спасибо.

JC_Piligrim
14.05.2015 03:59+13Главное теперь — не делать бекапы.
hsto.org/storage1/b8ef9be1/67c1e381/2ca10012/0cdd913b.jpg

TheShock
15.05.2015 12:25На самом деле необходимо не только обновлять this, но и возвращать предыдущее значение при выходе из функции, ибо потом будет неловно, когда после вызова другого метода в вашем методе будет некорректный this
EDIT: сори, уже вижу, что внизу описали эту проблему

kr41
14.05.2015 10:57+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. Да, мне говорили, что я зануда :)
BasicWolf Автор
14.05.2015 11:56+1Согласен, но сделано это намеренно, чтобы не усложнять код. Думаю особенным мазохизмом будет запуск подобного кода в многопоточной среде :)

ZyXI
15.05.2015 17:58Вообще?то для неявного this есть гораздо более безопасное решение: изменение AST (пример: habrahabr.ru/post/153949) и import hooks (либо изменение кода в setup.py перед установкой).

ZyXI
15.05.2015 18:16+1И это потокобезопасно. Ещё можно в декораторе/метаклассе «перекомпилировать» функцию, используя
func.__code__, но я не вижу для этого стандартных модулей (модуль dis успешно читаетfunc.__code__, но я не знаю, как превратить изменённый dis.Instruction в байткод). Код будет выглядеть как?то так:
, но не хватает критического куска на месте «convert instruction to byte code here». Кроме того, байткод официально нестабилен и данный способ может не подходить для не?CPython реализаций Python.#!/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)
moigagoo
Замечательная статья, спасибо!