Python/Протокол дескрипторів

Матеріал з Вікіпідручника

Прив’язка методів[ред.]

Чим відрізняється функція від методу якогось об’єкта? Давайте подивимось. Спершу створимо об’єкт з методом:

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)

Посилання[ред.]