Прив’язка методів Чим відрізняється функція від методу якогось об’єкта? Давайте подивимось. Спершу створимо об’єкт з методом:
class X: def __init__(self, name): self.name = name
def hello(self): print('hello, my name is', self.name)
x = X('petro') x.hello() # OUT: hello, my name is petro
Все працює, метод викликається. Тепер давайте створимо функцію, запишемо її в один з атрибутів об’єкта і звіди викликатимемо:
def getname(self): return self.name
x.getname = getname x.getname() # OUT: Traceback (most recent call last): # OUT: File "<input>", line 1, in <module> # OUT: TypeError: getname() takes exactly 1 argument (0 given)
Чому коли ми викликаємо функцію з об’єкта їй не передається цей об’єкт в перший параметр (self)?
А тому, що функція - це ще не метод:
x.getname # Функція # OUT: <function getname at 0xa6c636c> x.hello # метод # OUT: <bound method X.hello of <__main__.X object at 0xa7a7f6c>>
Це в об’єкта. А в класі?
X.hello # OUT: <function hello at 0xa6c626c>
А в класі - просто функція. Відмінність метода в тому - що він обгортка навколо звичайної функції, яка повертається з атрибутів екземпляра, і коли ми пишемо x.hello(), тобто викликаємо цю обгортку, всередині неї виконується звичайний X.hello(x).
А давайте покладемо в клас і нашу функцію:
X.getname = getname x.getname # OUT: <function getname at 0xa6c636c>
Бачимо що в об’єкті нічого не змінилось. А все тому, що ми переписали атрибут екземпляра, а при пошуку атрибутів спершу дивляться чи це не атрибут екземпляра (в словнику __dict__), і тільки після того лізуть в клас.
x.__dict__ # OUT: {'getname': <function getname at 0xa6c636c>, 'name': 'petro'} del x.__dict__['getname'] x.getname # OUT: <bound method X.getname of <__main__.X object at 0xa7a7f6c>> x.getname() # OUT: 'petro'
І справді, наша функція тепер стала методом, хоча єдине що ми зробили - присвоїли її атрибуту класу, а не екземпляру.
Як це все працює всередині? Хто загортає функції в метод? Дескриптори!
TODO: розібрати метод __get__ самих функцій.
Приклади
Статичний метод
Виклик процедури в стилі мови Паскаль В мові Паскаль, якщо процедура не має аргументів, щоб її викликати не обов’язково писати в кінці її імені пару порожніх дужок. Можна просто написати
clrscr; writeln;
І процедури виконаються.
Давайте зробимо таке саме в Python, щоправда для методів звісно. Виглядатиме все так:
class X: @PascalMethod def hello(): print('hello')
@PascalMethod def world(): print('world')
def say(): X.hello # Виклики методів які зовсім не виглядають як виклики методів X.world # А все тому що це паскалівські методи.
say() # Та все ж це виклики, тому що нам роздруковується фраза: # OUT: hello # OUT: world
А ось сам декоратор PascalMethod: class PascalMethod: def __init__(self, m): self.m = m # Запам’ятовуємо наш метод в конструкторі def __get__(self, obj, cls): return self.m() # При будь-якому читанні атрибута - викликаємо
Перевірка значення атрибута на відповідність умові class ConstrainedField: def __init__(self, initial, check): self.check = check self.value = initial
def __set__(self, obj, value): if self.check(value): self.value = value else: raise TypeError("Value {!r} don't pass check".format(value))
def __get__(self, obj, cls): if obj: return self.value else: return self
def is_adult(age): return isinstance(age, int) and age >= 18
class Adult: age = ConstrainedField(20, is_adult)
Посилання
Understanding __get__ and __set__ and Python descriptors StackOverflow Introduction to Python descriptors Alex Starostin, IBM DeweloperWorks, 26 Червня 2012 The Python Language Reference > Special method names > Data model > Customizing attribute access
Python instance descriptors: when class descriptors aren’t dynamic enough 29 січня 2009 Brian Beck