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
Замечательная статья, спасибо!