Сообщений 0    Оценка 315        Оценить  
Система Orphus

Разрешение имён в python 2.x

Автор: Холодилов Сергей Александрович
Опубликовано: 09.04.2013
Исправлено: 10.12.2016
Версия текста: 1.1
Перед точкой
Глобальный уровень
Модули
Простой вызов функции
Определение класса
Вложенные функции и классы
Задачка
После точки
Что такое классы
Old-style classes
New-style classes
О чём ещё можно было бы рассказать

Эйяфьядлайёкюдль


Python - динамический интерпретируемый язык, и о том, какое имя что означает, интерпретатор с удивлением узнаёт только в процессе исполнения. Дело это непростое и интересное, но не всегда хорошо документированное, поэтому многое получается понять только экспериментальным путём. В результате в статье не очень много слов, зато много маленьких примеров, всё они запускалось на Pythonе версии 2.7.3.

ПРЕДУПРЕЖДЕНИЕ

В Pythonе 3.х многое могло поменяться, в Pythonе младше 2.2 не было new-style классов, в Pythonах версий 2.2-2.6 не было каких-то возможностей, и могла отличаться внутренняя реализация. В общем, детали могут отличаться почти любые, но я надеюсь, что общий смысл сохраняется.

Статья состоит из двух частей, в первой описан процесс поиска имени объекта, во второй – поиск атрибута.

Перед точкой

Вопрос следующий: когда мы пишем “print x”, откуда Python узнаёт, что такое “x”? Откуда он знает, что такое “print”, понятно – ключевое слово, можно один раз запомнить, а вот “x” каждый раз означает разное.

Глобальный уровень

Тут всё просто. Имена берутся из словаря, доступного через функцию globals() (дальше я буду называть его просто «globals»), или из модуля __builtins__, и именно в такой последовательности.

>>> globals()
{'__builtins__': <module '__builtin__' (built-in)>, '__name__': '__main__',
'__doc__': None, '__package__': None}
>>> x = 10
>>> globals()                      # x появился в словаре
{'__builtins__': <module '__builtin__' (built-in)>, '__name__': '__main__', 'x': 10, '__doc__': None, '__package__': None}
>>> globals()['y'] = 17            
>>> y                              # y появился среди переменных
17

То есть завели переменную x, увидели, как это отразилось на globals, а потом наоборот -- изменили словарь и попробовали обратиться к переменной. Всё работает. Аналогично удаление:

>>> del x                   
>>> globals()                      # x пропал из словаря
{'__builtins__': <module '__builtin__' (built-in)>, '__package__': None, 'y': 17, '__name__': '__main__', '__doc__': None}
>>> del globals()['y']
>>> y                              # и y больше не известен
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'y' is not defined

Прямым обращением к globals можно напихать туда всякого мусора, и никто не обидится, но смысла особого нет - как это потом использовать не понятно.

>>> globals()[23] = 45
>>> globals()['23'] = 67
>>> globals()['qqq www'] = 99
>>> globals()  
{'23': 67, '__builtins__': <module '__builtin__' (built-in)>, 'qqq www': 99, 
'__package__': None, 23: 45, '__name__': '__main__', '__doc__': None}
>>> 23                             # просто число
23
>>> '23'# просто строка '23'
>>> qqq www                        # ошибка
  File "<stdin>", line 1
    qqq www
          ^
SyntaxError: invalid syntax
>>> dir()                          # а в списке все есть
[23, '23', '__builtins__', '__doc__', '__name__', '__package__', 'qqq www']
>>> del 23
  File "<stdin>", line 1
SyntaxError: can't delete literal

Видно, что всё успешно вставилось, и dir отрабатывает, но интерпретатор просто не догадывается запустить логику поиска и разрешения имени при встрече с чем-то, что не кажется ему именем. А как было бы прикольно, если бы можно было переопределить число 23 :)

ПРИМЕЧАНИЕ

Оказывается! При помощи особой магии C API переопределить значение числа 23 как раз можно. За эту взрывающую мозг информацию спасибо Владимиру Мозгалину.

True и False на имена похожи, определены в модуле __builtins__ и переопределить тоже можно. None тоже похоже на имя, тоже на первый взгляд определено в __builtins__, но переопределить его нельзя. И более того, забегая вперёд, прямая запись в __builtins__.__dict__['None'] проходит, но не оказывает на None никакого влияния. Не разбирался.

Аналогично работает импорт, определение функций и классов, сюда же попадают переменные-счётчики из циклов for и list comprehension, исключения из обработчиков исключений, переменные объявленные во вложенных конструкциях - пока речь не идёт о вызове функций, что такое "область видимости переменной" Python не очень в курсе.

Импорт, функции, классы:

>>> import math            # импорт просто добавляет несколько имён и
>>> from math import cos   # связывает их с соответствующими объектами
>>>
>>> globals()              
{'cos': <built-in function cos>, '__builtins__': <module '__builtin__'
(built-in)>, '__package__': None, '__name__': '__main__',
'__doc__': None, 'math': <module 'math' (built-in)>}
>>>
>>> def f(x, y): return x + y
... 
>>> class C:
...     pass
... 
>>> globals()                      # определение функции и класса тоже
{'cos': , 'f': <function f at 0x9cc21b4>, '__builtins__': <module
'__builtin__' (built-in)>, 'C': <class __main__.C at 0x9ccb0bc>, ...}
>>>
>>> globals()['f'](2,3)            # можно вызвать функцию вот так  
5
>>>
>>> del cos
>>> del math
>>> del f
>>> del C
>>> globals()                      # удаляются так же как обычные имена
{'__builtins__': <module '__builtin__' (built-in)>, '__package__': None,
'__name__': '__main__', '__doc__': None}

Видно, что никакой особой магии не требуется, они все ведут себя точно так же, как любые другие переменные.

Циклы:

>>> [x**2 for x in range(10)]             # list comprehension
[0, 1, 4, 9, 16, 25, 36, 49, 64, 81]
>>> for y in []:                          # for, тело не исполнится ни разу
...     pass
... 
>>> for z in [1,2]:                       # for, исполнится  
...     pass
... 
>>> globals()                             # x и z есть, y нету
{'__builtins__': <module '__builtin__' (built-in)>, '__package__': None,
'x': 9, '__name__': '__main__', 'z': 2, '__doc__': None}

В переменной остаётся последнее значение, которое в ней было, если не было никакого, переменная не создаётся.

Исключения, аналогично:

>>> try:
...     11                                # не выстрелит
... except Exception, e1:                 
...     pass
... 
11
>>> try:
...     raise Exception('1234')           # выстрелит 
... except Exception, e2:                 
...     pass
... 
>>> globals()
{'__builtins__': <module '__builtin__' (built-in)>, '__package__': None, 
'__name__': '__main__', '__doc__': None, 'e2': Exception('1234',)}

И переменные:

>>> if 1 < 2: x = 10
... 
>>> if 1 > 2: y = 20
... 
>>> globals()
{'__builtins__': <module '__builtin__' (built-in)>, '__name__': '__main__', 'x':10, '__doc__': None, '__package__': None}

В общем, всё то же самое.

__builtins__

Полслова нужно сказать про __builtins__. Вот такие:

>>> int                        # это тип
<type 'int'>
>>> int = 12                              
>>> int                        # это число
12
>>> globals()                  # число-число
{'__builtins__': <module '__builtin__' (built-in)>, '__name__': '__main__',
'int': 12, '__doc__': None, '__package__': None}
>>> del int
>>> int                        # опять тип
<type 'int'>
>>> del int                    # и удалить его нельзя
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'int' is not defined
>>> int(1.2)                   # несмотря на NameError работает нормально
1

То есть, имя, определённое в globals, имеет приоритет перед __builtins__.

globals()

Как много раз было показано выше, __builtins__ определён в словаре, возвращаемом функцией globals(). Если же заглянуть в __builtins__ при помощи функции dir, можно обнаружить, что globals определена там. Не возникает ли здесь замкнутого круга? Нет, не возникает.

Модули

Несмотря на название, globals не совсем глобален. Вот такой вот модуль:

        import math

x = 10
y = 11

def get_globals():
    return globals()

def get_globals_id():
    return id(globals())

И вот такой вот код:

>>> x = 23
>>> y = 24
>>> import test
>>> 
>>> id(globals())                         # это разные словари3073143644L
>>> 
>>> test.get_globals_id()
167473196
>>>
>>> globals()                             # и у них разное содержимое
{..., 'test': <module 'test'from'test.py'>, 'x': 23, 'y': 24,
'__name__': '__main__', ...}
>>>
>>> test.get_globals()
{..., '__file__': 'test.py', 'get_globals': <function get_globals at
 0x9fa11b4>, 'x': 10, 'y': 11, '__name__': 'test', ...'math': <module
'math' (built-in)>}
>>> 
>>> x                                     # значение x не изменилось
23

Видно, что это разные словари, они имеют разные наборы ключей, и одинаковым ключам в них соответствуют разные значения. То есть globals локален для модуля.

ПРИМЕЧАНИЕ

Если более полно привести вывод вызова test.get_globals(), окажется, что __builtins__ в загруженном модуле почему-то выглядит совсем не так: не как модуль, а как словарь. Но с этим вопросом я не разбирался, проверил только что это не артефакт, вызванный выполнением кода из консоли.

Простой вызов функции

Это тоже просто. Давайте подумаем:

Нужно что-то среднее. Оно и есть.

>>> def f(): print x       # функция только читает x
... 
>>> x = 10                 # а x определён позже
>>> 
>>> f()                    # читает!
10
>>> def g(): x = 12        # функция только меняет x
... 
>>> g()
>>> x                      # x остался тот же 
10
>>> f()                    # и f() тоже так думает 
10
>>> def h():               # функция сначала читает x, а потом меняет
...     print x
...     x = 13
... 
>>> h()                    # упс
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in h
UnboundLocalError: local variable'x' referenced before assignment
>>> x                      # x остался тот же 
10

То есть, если в коде функции переменная не меняется, интерпретатор ассоциирует её с глобальным контекстом, а если меняется, то с локальным. Для того чтобы явным образом связать имя с глобальным контекстом, используется ключевое слово global:

>>> def j():              # функция меняет глобальный x
...     global x
...     x = 12
... 
>>> j()
>>> x
12
>>> def i():              # функция читает и меняет глобальный x
...     global x
...     print x
...     x = 14
... 
>>> i()
12
>>> x
14

И можно даже объявить внутри функции новую глобальную переменную:

>>> x                                # сначала x не было
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
NameError: name 'x' is not defined
>>> def f():
...     global x
...     x = 10
... 
>>> f()
>>> x                                 # теперь х есть
10

Локальный контекст возвращает функция locals(), в него автоматически попадают параметры и все переменные, которые объявляются внутри функции.

>>> def f(a, b):
...     print locals()
...     x = 10
...     print locals()
...     for y in [1,2]:
...         x += y
...     print locals()
... 
>>> f(1, 2)
{'a': 1, 'b': 2}
{'a': 1, 'x': 10, 'b': 2}
{'a': 1, 'x': 13, 'b': 2, 'y': 2}

Туда же попадает импорт

>>> def f():
...     import math
...     from math import sqrt
...     print locals()
... 
>>> f()
{'math': <module 'math' (built-in)>, 'sqrt': <built-in function sqrt>}
>>>
>>> globals()         # на этом уровне про math и sqrt ничего не известно
{'__builtins__': <module '__builtin__' (built-in)>, '__name__': '__main__',
'f': <function f at 0x923109c>, '__doc__': None, '__package__': None}

А также локальные функции и классы:

>>> def g():
...     def h(): return 1
...     class C:
...         pass
...     return locals()
... 
>>> g()
{'h': <function h at 0x9231144>, 'C': <class __main__.C at 0x922f1ac>}
>>> g()['h']()
1

А вот прямая запись в locals не работает совсем, даже для уже существующих переменных:

>>> def f():
...     x = 10
...     print locals()
...     x = 12
...     print locals()
...     locals()['x'] = 14
...     print locals()
... 
>>> f()
{'x': 10}
{'x': 12}
{'x': 12}

Уж не знаю почему. Документация предупреждает, что менять этот словарь не стоит именно потому, что может ни на что не повлиять, но причину не раскрывает.

Определение класса

Как обычно всё просто, но, если вы раньше об этом не задумывались, может быть несколько неожиданно. В отличие от, например, С++, в Python код внутри методов класса не имеет почти никаких преимуществ по сравнению с внешним кодом: обращения к полям и методом класса производятся только явным образом, через self или через имя класса (для статических), поэтому рассматривать их отдельно смысла нет. Но зато само по себе определение класса это исполняемый код, и он образует контекст исполнения.

>>> class C:
...     print"hello!"
... 
hello!

Человек в здоровом уме вот именно такое писать не будет, но на исполняемости определения класса в Python основана, например, работа декораторов.

Вот такой модуль:

        class C:
    x = 10
    y = x ** 2
    print locals()

    def __init__(self, a):
        print locals()
        self.a = a

    def geta(self):
        print locals()
        return self.a

    print locals()

Вот так вызывается:

>>> import test         # пошло исполняться определение класса
{'y': 100, 'x': 10, '__module__': 'test'}
{'y': 100, 'x': 10, '__module__': 'test', '__init__': <function __init__ at 
0x9e483ac>, 'geta': <function geta at 0x9e4872c>}
>>> 
>>> c = test.C(123)     # видно, что у метода класса в контексте нет ничего                         # дополнительного, только входные параметры
{'a': 123, 'self': <test.C instance at 0xa095bac>}
>>> c.geta()
{'self': <test.C instance at 0xa095bac>}
123

Логика доступа к глобальным переменным вообще и ключевое слово global в частности, работают в этом случае так же, как и для функций.

Вложенные функции и классы

В этом случае у нас несколько уровней внешнего по отношению к функции контекста. И тут нам начинает не хватать слов... Впрочем, пока нужно только читать, всё работает отлично.

>>> x = 1000
>>> def f(y):                     # при каждом вызове порождает 
...     def g(a):                 # функцию одного аргумента
...         print locals()
...         return x + y + z + a
...     z = 100
...     return g
... 
>>> g1 = f(10)
>>> g1(1)                         # контекст ровно такой, как должен быть
{'a': 1, 'y': 10, 'z': 100}
1111
>>> x = 2000
>>> g1(1)                         # от x тоже зависит, как и хотелось
{'a': 1, 'y': 10, 'z': 100}
2111
>>> g2 = f(20)                    # создаём другой экземпляр
>>> g2(1)                         # контекст ровно такой, как должен быть
{'a': 1, 'y': 20, 'z': 100}
2121

Понятно, что для того, чтобы это работало вместе с функциями g1 и g2, хранится контекст, у каждой свой, со значениями y и z (доступен через атрибут, g1.func_closure). Но изменить сохранённое в этом контексте значение невозможно, увы. С global Python обращается к совсем глобальным переменным, а без него - к совсем локальным. Это ограничение снято в Python 3.x новым ключевым словом nonlocal.

Ну и просто чтобы показать, как далеко можно зайти...

>>> def f(x):
...     class C:
...         if x % 2 == 0:
...             def even(self): return True
...         else:
...             def even(self): return False
...     return C
... 
>>> C1 = f(1)
>>> C1().even()
False
>>> 
>>> C2 = f(2)
>>> C2().even()
True

Не думаю, что стоит использовать такое в мирных целях.

Задачка

Описанного выше достаточно для того, чтобы понять вот такой примерчик:

>>> for f in [(lambda: i) for i in range(5)]:
...     print f()
... 
4
4
4
4
4

И может быть, даже хватит, чтобы сообразить, как изменить его так, чтобы он заработал. За этот пример спасибо Роману Одайскому.

После точки

Найти объект полдела, дальше нужно с ним что-то сделать. Обычно это связано с доступом к полям/методам, впрочем, в Python между ними нет особой разницы.

Что такое классы

Для начала немного теории. В прошлой части выяснилось, что определение класса добавляет имя класса в глобальную область видимости. А с этим именем, соответственно, связан какой-то .. объект? И какого же класса этот объект?

Ага.

Класс, экземплярами которого являются классы, обычно называют метаклассом, просто чтобы не путаться в словах. В Python есть два стандартных метакласса, один – для новых классов (new-style classes), второй – для старых (old-style/classic classes). И можно создавать свои, но эта дорожка уведёт нас слишком далеко, так что по ней мы не пойдём.

>>> class COld: pass# old-style класс
... 
>>> COld                                 # получившийся объект
<class __main__.COld at 0x9d4f08c>
>>> COld.__class__                       # так он не умеет
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: class COld has no attribute '__class__'
>>> type(COld)                           # но вот так можно
<type 'classobj'>
>>> 
>>> class CNew(object): pass# new-style класс
... 
>>> CNew                                 # получившийся объект
<class '__main__.CNew'>
>>> CNew.__class__                       # этот умеет и так и так
<type 'type'>
>>> type(CNew)
<type 'type'>

Распарсив определение класса, интерпретатор делает следующее (подробности см. в статье Unifying types and classes in Python 2.2):

Давайте попробуем повторить это самостоятельно. Метаклассом для новых классов работает type.

>>> def c__init__(self, x): self.x = x   # это будет __init__           
... 
>>> C = type('C', (), {'a':1, '__init__':c__init__}) # object можно явно не указывать
>>> c = C(23)
>>> c.x
23
>>> c.a
1
>>> C
<class '__main__.C'>
>>> c.__class__
<class '__main__.C'>

Метакласс старых классов доступен через types.ClassType, в первом приближении он работает так же.

>>> import types
>>> 
>>> def c__init__(self, x): self.x = x
... 
>>> C = types.ClassType('C', (), {'a':1, '__init__':c__init__})
>>> C
<class __main__.C at 0x8b7508c>
>>> c = C(32)
>>> c
<__main__.C instance at 0x8b81ecc>
>>> c.x
32
>>> c.a
1

Итого:

И в этой милой конструкции как минимум в двух местах встречается бесконечная рекурсия:

>>> object.__class__           # класс object это экземпляр метакласса ...
<type 'type'>
>>> object.__class__.__base__           # который унаследован от класса ...
<type 'object'>
>>> object.__class__.__base__.__class__ # который является экземпляром ...
<type 'type'>
>>> 
>>> type.__class__             # при этом сам type это экземпляр ...
<type 'type'>
>>> type.__class__.__class__
<type 'type'>

Но, кажется, это не мешает Python-у работать :)

Со старыми же классами ситуация следующая:

>>> types.ClassType
<type 'classobj'>
>>> types.ClassType.__base__
<type 'object'>
>>> types.ClassType.__class__
<type 'type'>

Несмотря на это, старые классы от новых отличаются довольно сильно. Но они немного проще, поэтому начнём с них.

Old-style classes

Сначала небольшой эксперимент

>>> class A: pass# пустой класс
<type 'object'>
... 
>>> a = A()
>>> dir(a)                         # но кое-что доступно через экземпляр
['__doc__', '__module__']
>>> dir(A)                         # то же самое доступно и через сам класс
['__doc__', '__module__']
>>> a.__dict__                     # __dict__ это такое волшебное слово
{}
>>> A.__dict__
{'__module__': '__main__', '__doc__': None}
>>> a.x                            # атрибута x нет
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: A instance has no attribute 'x'
>>> a.x = 12                       # но присвоить ему можно
>>> dir(a)                         # и вот он появился
['__doc__', '__module__', 'x']
>>> a.__dict__                     # и тут тоже
{'x': 12}

И второй:

>>> class B:                       # класс посложнее
...     v = 10
...     def __init__(self): self.x = 23
...     def f(self): pass
... 
>>> b = B()
>>> dir(b)                         # здесь x есть
['__doc__', '__init__', '__module__', 'f', 'v', 'x']
>>> dir(B)                         # а здесь нет
['__doc__', '__init__', '__module__', 'f', 'v']
>>> b.__dict__                     # вот отсюда x берётся
{'x': 23}
>>> B.__dict__                     # а отсюда берётся всё остальное
{'__module__': '__main__', 'f': <function f at 0x941b10c>, '__doc__': None,
'__init__': <function __init__ at 0x941b144>, 'v': 10}
>>> b.__module__
'__main__'

В общем, идея должна быть понятна.

ПРИМЕЧАНИЕ

Я ввожу __dict__ через пример, потому что в документации нигде нет прямых утверждений на эту тему. Видимо, считается, что это «внутренняя деталь реализации». Но при этом это ещё и некое общее знание, которым все пользуются в многочисленных косвенных ссылках. Например, см. в документации, в описании механизма работы __slots__.

Что происходит при записи (продолжение второго примера):

>>> B.v                             # v это атрибут класса
10
>>> b.v                             # но доступен и через экземпляры
10
>>> b.v = 20
>>> b.v                             # значение изменилось
20
>>> B.v                             # а так не изменилось
10
>>> dir(b)                          # здесь ничего не изменилось
['__doc__', '__init__', '__module__', 'f', 'v', 'x']
>>> b.__dict__                      # а здесь изменилось
{'x': 23, 'v': 20}
>>> B.__dict__                      # а здесь не изменилось
{'__module__': '__main__', 'f': <function f at 0x941b10c>, '__doc__': None, 
'__init__': <function __init__ at 0x941b144>, 'v': 10}

То есть при установке значения атрибута устанавливается атрибут экземпляра, даже если у класса есть такой же. Такой прозрачный переход от атрибута класса к атрибуту экземпляра позволяет использовать атрибуты класса как значения по умолчанию.

Аналогично работает удаление (продолжение продолжения):

>>> b.v
20
>>> del b.v                         # удаляем атрибут экземпляра
>>> b.v                             # становится виден атрибут класса
10
>>> del b.v                         # нет такого!
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: B instance has no attribute 'v'
>>> b.v                             # есть!
10
>>> del B.v                         # удаляем атрибут класса
>>> b.v                             # вот теперь нет
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: B instance has no attribute 'v'

Что ещё нужно сказать:

__xxxattr__

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

Если эти методы не определены, работает реализация по умолчанию. Небольшой пример:

          class C:
    a = 1 
    def __init__(self): self.x = 2 

    # все неизвестные атрибуты будут равны 3def __getattr__(self, name): 
        print'getattr', name
        return 3          

    # при установке атрибута 'a' будет изменён атрибут классаdef __setattr__(self, name, value):  
        print'setattr', name, value
        if name == 'a': C.a = value       
        else: self.__dict__[name] = value

    # а __delattr__ только выводит строчку и больше ничего не делаетdef __delattr__(self, name):          
        print'delattr', name

И вот так вызывается:

>>> c = C()       # __setattr__ вызывается даже при обращении из __init__
setattr x 2
>>> c.a           # при обращении к известным атрибутам всё как обычно
1
>>> c.x
2
>>> c.y           # такого он не знает, __getattr__
getattr y
3
>>> c.z = 23      # __setattr__ добавит z в __dict__
setattr z 23
>>> c.z           # теперь это известный атрибут, __getattr__ не нужен
23
>>> c.a = 4       # __setattr__ должен установить атрибут класса
setattr a 4
>>> C.a           # получилось
4
>>> del c.z       # удаление вызывает __delattr__
delattr z
>>> c.z           # но поскольку он ничего не делает...
23
>>> c.a = 10      # ещё раз установим значение a
setattr a 10
>>> del C.__setattr__  # так тоже можно!
>>> c.a = 20      # обратите внимание на отсутствие строчки 'setattr a 20'
>>> c.a
20
>>> C.a           # и атрибут класса не изменился
10

А вот как всё падает при неправильной реализации __setattr__:

>>> class C: 
...     def __init__(self): self.x = 1
...     # это совсем явная глупость, но может быть что-то более тонкое, 
...     # например обновление какого-нибудь счётчика
...     def __setattr__(self, name, value): self.x = value 
... 
>>> c = C()
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 2, in __init__
  File "<stdin>", line 3, in __setattr__
  ...много аналогичных строчек пропущено...
  File "<stdin>", line 3, in __setattr__
RuntimeError: maximum recursion depth exceeded

New-style classes

На первый взгляд, с точки зрения использования, изменений немного, и обратная совместимость сохранена почти полностью. Но это только на первый взгляд и только с точки зрения использования, внутри всё не так. Во-первых, new-style классы сами являются объектами во всех (известных мне) смыслах слова. Во-вторых, все волшебные слова беспощадно затолкали в __dict__. Даже сам __diсt__ затолкали в __dict__! Но:

В рамках логики old-style классов эта задачка не решается, требуется совсем другой механизм -- дескрипторы (attribute descriptors; descriptors). Выглядит это примерно так:

>>> class C(object):
...     a = 1
...     def f(self): pass
...     def __init__(self): self.x = 2
... 
>>> c = C()
>>> c.__dict__
{'x': 2}  
>>> C.__dict__
dict_proxy({'a': 1, ... '__dict__': <attribute '__dict__' of 'C' objects>, ...})
>>> object.__dict__
dict_proxy({.., '__class__': <attribute '__class__' of 'object' objects>, ..})

Обратить внимание стоит на следующие моменты:

Заглянем глубже:

>>> dir(C.__dict__['__dict__'])          # кто ты, добрый молодец?
['__class__', .. , '__delete__', '__doc__', .. '__get__', '__name__', '__set__']
>>> 
>>> C.__dict__['__dict__'].__name__      # my name is ..'__dict__'
>>> C.__dict__['__dict__'].__class__     # I am from ..
<type 'getset_descriptor'>
>>> C.__dict__['__dict__'].__doc__       # I am ..'dictionary for instance variables (if defined)'
>>> C.__dict__['__dict__'].__get__(c)    # а вот так можно получить                                          # значение для конкретного c
{'x': 2}
>>> C.__dict__['__dict__'].__set__(c, {'x':2, 'y':3})  # а так изменить
>>> c.y
3
>>> c2 = C()
>>> c2.y                   # и не для всех, а только для одного экземпляра
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: 'C' object has no attribute 'y'
>>> C.__dict__['__dict__'].__get__(c)['z'] = 45        # так тоже можно 
>>> c.z
45

Знакомьтесь, это и есть дескриптор.

Дескрипторы

Как и всё в Python, дескриптор – это такой объект. Но, как и в случае с метаклассами, эта дорожка может завести слишком далеко, так что ограничимся кратким и грубоватым описанием.

Итак, дескриптор атрибута – это объект new-style класса, он хранится в классе и предоставляет доступ к атрибуту. При обращении к атрибуту Python, наткнувшись на дескриптор, вызывает его стандартные методы (собственно, по их наличию Python и понимает, что это именно дескриптор, а не значение атрибута). Стандартные методы:

Давайте попробуем.

>>> class MyDesc(object):         # простой дескриптор
...     def get(self, obj, cls=None): 
...         print'get obj=%s, cls=%s' % (repr(obj), repr(cls)) 
...         return 0
...
>>> class C(object):              # простой класс
...     d = MyDesc()
... 
>>> c = C()
>>> c.d                           # чтение через экземпляр
get obj=<__main__.C object at 0x9f35f4c>, cls=<class '__main__.C'>
0
>>> C.d                           # чтение через класс
get obj=None, cls=<class '__main__.C'>
0
>>> c.__dict__
{}
>>> C.__dict__                    # вот он тут
dict_proxy({..., 'd': <__main__.MyDesc object at 0x9f35eec>, ...})
>>> c.d = 123                     # установка
>>> c.d                           # т.к. __set__ не определили, выполняется                                   # действие по умолчанию
123
>>> c.__dict__                    # вот такое
{'d': 123}
>>> C.d                           # а тут всё на месте
get obj=None, cls=<class '__main__.C'>
0

Так работает __get__. Продолжаем пример:

>>> def my__set__(self, obj, value): 
...     print'surprise! value=%s' % repr(value)
... 
>>> MyDesc.__set__ = my__set__           # это можно сделать на ходу!
>>> c.d                                  # и локальный d перестал быть виден
get obj=<__main__.C object at 0x9f35f4c>, cls=<class '__main__.C'>
0
>>> c.d = 456
surprise! value=456
>>> c.__dict__                           # хотя никуда не делся
{'d': 123}

Ну а с __delete__ всё просто, тут и пример не нужен :)

Что ещё важно: дескрипторы срабатывают только если определены на уровне класса, а не объекта. То есть:

>>> class MyDesc(object):                # тот же простой дескриптор
...     def __get__(self, obj, cls=None): 
...         print'get obj=%s, cls=%s' % (repr(obj), repr(cls))
...         return 0
... 
>>> class C(object):                     # немного другой простой класс
...     d = MyDesc()
...     def __init__(self):
...         self.d2 = MyDesc()
... 
>>> c = C()
>>> c.d                                  # вызвался __get__ с параметрами
get obj=<__main__.C object at 0x99e7fcc>, cls=<class '__main__.C'>
0
>>> c.d2                                 # получили сам дескриптор
<__main__.MyDesc object at 0x99ea04c>

И тем более нет смысла использовать дескрипторы на верхнем уровне, вне классов.

Применение дескрипторов в мирных целях

Мало кому приходится определять свои собственные дескрипторы, но стандартные используются постоянно. С помощью механизма дескрипторов в new-style классах реализованы:

Давайте с последним пунктом разберёмся. Как вообще вызвать метод? Этот процесс состоит из двух частей:

Очень просто. Но на втором этапе нужно как-то сообразить, что вместо того чтобы просто делать как написано программистом, нужно незаметно подпихнуть в качестве первого параметра сам объект. А чтобы соображать было не нужно, используются дескрипторы.

Для начала посмотрите, что такое методы:

>>> class C(object):
...     def f(a,b,c): pass
... 
>>> c = C()
>>> C.f
<unbound method C.f>
>>> c.f
<bound method C.f of <__main__.C object at 0x957fdec>>

Смысл должен быть понятен: unbound method не привязан к конкретному экземпляру и ждёт три параметра, а bound method уже привязан, и ему не хватает только двух. А теперь фокус:

>>> def f(a,b,c): print a,b,c            # просто функция
... 
>>> dir(f)                               # не просто функция!
[..., '__get__', ...]
>>> 
>>> f.__get__(None, int)                 # вот вам unbound method класса int
<unbound method int.f>
>>> 
>>> f.__get__(1, int)                    # а вот bound
<bound method int.f of 1>
>>> 
>>> f.__get__(1, int)(2,3)               # и он даже работает как надо!
1 2 3
>>> 
>>> class C(object): pass
... 
>>> f.__get__(None, C)                   # с пользовательским классом аналогично
<unbound method C.f>
>>> 
>>> c = C()
>>> f.__get__(c, C)
<bound method C.f of <__main__.C object at 0x954ad4c>>
>>> 
>>> f.__get__(c, C)(1,2)
<__main__.C object at 0x954ad4c> 1 2
>>> 
>>> f.__get__('qwerty', int)             # а реализация тупая-тупая :)
<bound method int.f of 'qwerty'>
>>> f.__get__('qwerty', int)(3,4)
qwerty 3 4

То есть функции просто реализуют интерфейс дескриптора, и не надо ни о чём догадываться: объект передаётся дескриптору явным образом.

Как устроены статические методы, методы класса и proreptу, тоже должно быть понятно.

__getattribute__

Это новый переопределяемый метод, появившийся в new-style классах. Он ведёт себя так же, как __setattr__ и __delattr__, то есть вызывается при любой попытке поиска атрибута, и точно так же как __setattr__ может привести к бесконечной рекурсии. Если верить статье Descriptor HowTo Guide, именно в реализации этого метода в классе object вызываются методы __get__ дескрипторов. Звучит логично, но как это проверить я не знаю.

Переопределять его, скорее всего, не стоит.

О чём ещё можно было бы рассказать

Несколько тем, писать подробно о которых не очень хочется, но упомянуть надо.

Совсем здорово было бы написать что-нибудь про разные версии 2.х и что-нибудь про 3.х, но, к сожалению, об этом я совсем-совсем ничего не знаю :)


mag
    Сообщений 0    Оценка 315        Оценить