Всем привет! Здесь я хотел бы скомпоновать все основные приемы и особенности python-Reflection. В программировании reflection (с англ. «осмысление») означает процесс, во время которого программа может отслеживать и модифицировать собственную структуру и поведение во время выполнения. Ниже описаны наиболее важные атрибуты и функции языка для работы с рефлексией:
- __class__
- __base__
- __dict__
- dir()
- hasattr() и delattr()
- setattr() и getattr()
- type()
- type(»,(),{})
- isinstance()
Итак, поехали:
__class__
Начнем с самого простого атрибута. Это атрибут __class__. атрибут __class__ возвращает ссылку на класс, экземпляром которого объект является. В python2 этот атрибут есть не у всех объектов: исключением являются пользовательские классы, не имеющие родителей, например:
class MyClass: pass MyClass.__class__
в python2 вернет ошибку AttributeError.
Это единственное исключение. Все экземпляры класса MyClass будут иметь атрибут __class__ = <MyClass>
__base__
Атрибут __base__ вернет первый родительский класс для класса объекта, если он есть. Есть так же атрибут __bases__, в отличие от __base__ он вернет всех непосредственных родителей класса объекта в виде кортежа:
class f(object): pass class d(f): pass class C(d ,f): pass print C.__base__ print C.__bases__
Получим:
<class '__main__.d'> (<class '__main__.d'>, <class '__main__.f'>)
__dict__
__dict__ — это атрибут объекта python, который вернет словарь всех пользовательских атрибутов объекта и их значений.
На что следует обратить внимание, работая с __dict__?
- Каждый класс в питоне является всего лишь объектом, который отличается от обычных объектов тем, что он может сам «создавать жизнь». Достигается эта сверхспособность наличием атрибута __metaclass__ (который так же, как и __dict__, является системным; по умолчанию атрибут __metaclass__ класса имеет значение type, но его можно изменить), но она не делает класс чем-то особенным
- Понятие атрибут в питоне — довольно объемлющее, включает в себя поля, свойства и методы класса
Перейдем к примерам и объявим класс:
class MyObject: MyField = 1 MyField2 = 2 def __init__(self): self.a = "2"
И посмотрим, что нам покажет print MyObject.__dict__
(python 2.7):
{'__module__': '__main__', 'MyField2': 2, 'MyField': 1, '__init__': <function __init__ at 0x01F5E630>, '__doc__': None}
Помимо наших атрибутов: полей MyField, MyField2 и метода __init__ в выводе мы увидели имя модуля (атрибут __module__) и атрибут __doc__, который должен содержать описание класса.
Обратите внимание, что в словаре нет атрибута a, поскольку атрибут a не является атрибутом класса-объекта MyObject, а является атрибутом объекта self
, им создаваемым — в этом ключевое отличие от классического рефлекшена, например, java или C#, где нет четкой разницы между статическими методами/полями класса и обычными. Т.е., например, в C# метод GetFields()
по дефолту вернет все поля, объявленные в классе, независимо от того, являются ли они статическими, в т.ч. поля экземпляров. __dict__
же не может вернуть поля экземпляра без самого экземпляра, поскольку его еще не существует — он, с точки зрения шарпа, вернет только статические поля и методы, объявленные в классе, в т.ч. методы, принимающие в качестве первого параметра экземпляр класса, поскольку они являются такими же статическими методами, хоть и не помечены @staticmethod
. Это легко доказать:
class MyObject(object): def Do(self): print 'do' print MyObject.Do(MyObject())
В питоне, начиная с версии 2.2, можно выключить атрибут __dict__ через магический атрибут __slots__. Если написать:
class Foo(object): __slots__ = [] bar = 'spam' b = 5 print Foo().__dict__
То мы получим ошибку 'Foo' object has no attribute '__dict__'
— чудесно, правда? Но в python2 не упустите один важный момент: для того, чтобы __slots__ работал, необходимо унаследовать класс от object (в python3 — все классы по дефолту унаследованы от object). Кроме того, __slots__ не позволяет добавлять атрибуты к объекту:
— поскольку наличие __slots__ ограничивает возможные имена атрибутов объекта теми, которые там указаны — это кстати, довольно интересная возможность.Foo().a = 5
Важно отметить, что атрибут __dict__ по дефолту является атрибутом, определенным интерпретатором — а значит не вернет сам себя. Но мы можем его определить явно: тип object явно переопределяет его, поэтому когда вы явно наследуетесь от object, то получите в словаре в т.ч. атрибут __dict__:
class MyObject(object): MyField = 1 MyField2 = 2 def __init__(self): self.a = "2" print MyObject.__dict__
Теперь мы видим два новых атрибута: __dict__ и __weakref__:
'__dict__': <attribute '__dict__' of 'MyObject' objects>, '__weakref__': <attribute '__weakref__' of 'MyObject' objects>
К слову, __weakref__
— это слабая ссылка, но это скорее относится к сборке мусора, нежели к рефлекшену, так что мы его опустим в этой статье
Т.к. в 3-ей все объекты унаследованы от object, то в ней __dict__ вернет себя у всех объектов среды
dir
dir(object) — это метод объекта, который вернет список имен всех его атрибутов. Очень похож на атрибут __dict__, но есть важное отличие. Давайте рассмотрим следующий пример:
class C: def f(self): print self.__dict__ print dir(self) c = C() c.f()
И получим следующий результат:
{} ['__doc__', '__module__', 'f']
Здесь важно понять отличие dir и __dict__. Чуть чуть изменим наш код, добавив конструктор:
class C: def f(self): print self.__dict__ print dir(self) def __init__(self): self.a='4' c = C() c.f()
И:
{'a': '4'} ['__doc__', '__init__', '__module__', 'a', 'f']
Что все это значит?
dir — более высокоуровневая API, нежели __dict__, она перечисляет не только атрибуты объекта, к которому применен метод, но и атрибуты класса объекта.
Если применить dir к классу, а не к его экземпляру, то результат будет идентичен `класс.__dict__.keys()`
__class__
В питоне можно узнать класс объекта, если он есть, через атрибут __class__ этого объекта. Этот атрибут вернет класс, экземпляром, которого текущий объект является — то есть объект который является генератором экземпляров.
hasattr, delattr (obj: object, fielt: str)
hasattr — проверяет наличие атрибута у объекта, delattr — удаляет атрибут по его имени. Если атрибут не найден, то пользователь получит AttributeError. Эти методы, как и dir, проверяют атрибут не только в самом объекте, но и в классе этого объекта, поскольку при отсутствии такого атрибута непосредственно у объекта, питон обращается к атрибуту класса этого объекта. Это довольно любопытная особенность языка: можно сказать, что создание экземпляров класса в питоне является операцией простого наследования объекта от объекта — с наследованием атрибутов и возможностью их переопределения. Но об этом в другой раз
Итак, оба этих метода ищут атрибут сначала в экземпляре класса, и если его нет, то — в классе объекта:
class C: r=7 c = C() c.r = 6 print hasattr(c, 'r') print c.r delattr(c, 'r') print hasattr(c, 'r') print c.r
Результат:
True 6 True 7
Как мы видим, функция hasattr в обоих случаях вернула True, и она нам нисколько не позволяет понять, в каком именно объекте (классе или экземпляре) этот атрибут объявлен :(. Но по значению c.r до и после delattr, мы видим разницу: delattr удалила первый по приоритету атрибут — переопределенный r=6 в объекте-инстансе, и после его удаления видим значение атрибута r=7 класса (объекта-эталона).
getattr, setattr (obj: object, fielt: str)
Эти методы позволяют читать и назначать значения атрибутам объекта. Принцип поиска атрибута подобен принципу двух предыдущих рассмотренных методов. По сути они идентичны обращению к переменной через точку из предыдущего примера: c.r
— весь профит использования getattr/setattr в том, что мы можем задать имя атрибута в виде строковой переменной!
type
Здесь наверное стоит заметить, что в большинстве языков программирования, объявленный класс является типом для своих экземпляров. В C/C++/C# мы можем использовать конструкцию typeof, чтобы узнать тип переменной. Казалось бы в питоне для этого есть свой метод — type. Давайте рассмотрим следующий пример:
print type('d') print 'd'.__class__
В результате мы получим:
<type 'str'> <type 'str'>
Казалось бы, это одно и то же. Но это кажущееся свойство. Чтобы распознать его призрачность, рассмотрим следующий пример. Возьмем экземпляр класса C, объявленного ранее, и посмотрим, что нам покажет type:
print c.__class__ print type(c)
Результат:
__main__.C <type 'instance'>
То есть класс и тип в питоне не совсем одно и то же. Этот несколько конфузный момент опять же исправляет наследование от object:
class C(object):
Теперь мы увидим одинаковый результат:
<class '__main__.C'> <class '__main__.C'>
Т.к. в питоне 3 все объекты по дефолту реализуют object, то там __class__ и type идентичны.
Но в python2 не все классы являются типами: классы, унаследованные от object — являются, не унаследованные — нет. Еще больше вы можете в этом убедиться, если попытаетесь переопределить конструктор класса, не унаследованного от object….
type(obj: object, (), {})
Функцию type мы уже применяли выше для того, чтобы проверить тип объекта. Но она умеет не только проверять тип, но и создавать его. Создаваемый тип по дефолту в обоих версиях питона будет унаследован от object. Итак, как использовать: функция принимает в качестве параметра три типа:
- Название нового класса/типа
- Кортеж классов-родителей для нового класса
- Словарь атрибутов нового класса
Пример:
Foo = type('Foo', (object, ), {'bar':56}) f = Foo() print type(f) class MyClass(Foo): pass print(MyClass.bar)
Foo будет являться полноценным типом/классом на все время выполнения программы
isinstance
Простой метод, который проверяет, является ли текущий объект экземпляром конкретного класса. С первого взгляда может показаться, что использование instance(c,C) идентично type(c) == C, но это неверно, поскольку type(c) вернет непосредственно объект-эталон, а isinstance проверяет реализацию в объекте всей цепочки наследования- и если в ней содержится класс C, то вернет True. Пример:
class C(object): r=7 c = C() print isinstance(c,object) print type(c)==object
Результат:
True False
Резюме:
Мы рассмотрели наиболее важные моменты питоно-рефлексии. Разумеется, это далеко не все инструменты, а только малая их часть. И хотя для начинающего junior-а таких знаний, может быть, и достаточно, мне кажется, не стоит останавливаться на достигнутом.
Полезные ссылки по теме:
- Класс или объект
- Заметки об объектной системе в python
Удачи и пока