2. Об'єкти. Початок .
3. Об'єкти. Хвіст.
У попередній частині ми почали вивчення об'єктної системи Пітона: зрозуміли, що саме можна вважати об'єктом і яким чином об'єкти виконують свою роботу. Продовжимо розгляд питання.
Вітаю вас в третій частині нашого циклу статей про нутрощах Пітона (строго рекомендую прочитати другу частину, якщо ви цього ще не зробили, інакше нічого не зрозумієте). У цьому епізоді ми поговоримо про важливе поняття, до якого все ніяк не підберемо, - про атрибути. Якщо ви хоч що-небудь писали на Python, то вам доводилося користуватися ними. Атрибути об'єкта - це інші, пов'язані з ним, об'єкти, доступні через оперетор. (Точка), наприклад: >>> my_object.attribute_name. Коротко опишемо поведінку Пітона при зверненні до атрибутів. Це поведінка залежить від типу об'єкта, доступного по атрибуту (вже зрозуміли, що це відноситься до всіх операцій, пов'язаних з об'єктами?).
У типі можна описати спеціальні методи, що модифікують доступ до атрибутів його примірників. Ці методи описані тут (як ми вже знаємо, вони будуть пов'язані з необхідними слотами типу функцією fixup_slot_dispatchers. Де створюється тип ... ви ж прочитали попередній пост. Так адже?). Ці методи можуть робити все, що завгодно; описуєте ви свій тип на C або на Python, ви можете написати такі методи, які зберігають і повертають атрибути з якогось неймовірного сховища, якщо вам так завгодно, ви можете передавати і отримувати атрибути по радіо з МКС або навіть зберігати їх в реляційної базі даних. Але в більш-менш звичайних умовах ці методи просто записують атрибут у вигляді пари ключ-значення (ім'я атрибута / значення атрибута) в якомусь словнику об'єкта, коли атрибут встановлюється, і повертають атрибут з цього словника, коли він запитується (або викидається виключення AttributeError. якщо в словнику немає ключа, відповідного імені запитуваного атрибута). Це все так просто і прекрасно, спасибі за увагу, на цьому, мабуть, закінчимо.
Стояти. Друзі мої, фекалії ще тільки почали своє стрімке наближення до обертається вітрогенератору. Пропадати, так всім пропадати. Пропоную спільно вивчити, що відбувається в інтерпретаторі, і задати, як ми зазвичай робимо, кілька дражливих питань.
Читаємо уважно код або відразу переходимо до текстового опису:
Давайте переведемо це на людську мову: у object (це найпростіший вбудований тип, якщо ви забули), як ми бачимо, є словник, і все, до чого ми можемо підступитися через атрибути, ідентичний тому, що ми бачимо в object .__ dict__. Нас повинно здивувати, що екземпляри типу object (наприклад, об'єкт o) не підтримують визначення додаткових атрибутів і взагалі не мають __dict__. але підтримують доступ до наявних атрибутів (спробуйте o .__ class__. o .__ hash__ і т.п .; ці команди щось повертають). Після цього ми створили новий клас C. успадковували його від object. додали атрибут A і побачили, що він доступний через C.A і C .__ dict __ [ 'A']. як і очікувалось. Потім ми створили екземпляр o2 класу C і побачили, що визначення атрибута змінює __dict__. і навпаки, зміна __dict__ впливає на атрибути. Після ми з подивом дізналися, що __dict__ класу доступний тільки для читання, незважаючи на те, що визначення атрибутів (C.A2) прекрасно працює. Нарешті, ми побачили, що у об'єктів __dict__ примірника і класу різні типи - звичний dict і загадковий dict_proxy відповідно. А якщо всього цього недостатньо, згадайте головоломку з попередньої частини: якщо спадкоємці чистого object (наприклад, o) не мають __dict__. а C розширює object. додаючи нічого значного, звідки раптом у примірників класу C (o2) з'являється __dict__?
У цьому прикладі останній рядок демонструє звернення до атрибуту типу. У цьому випадку, щоб знайти атрибут bar. буде викликана функція доступу до атрибутів класу Foo (на яку вказує tp_getattro). Приблизно те ж саме відбувається при визначенні та видаленні атрибутів (для інтерпретатора, до речі, «видалення» - це всього лише установка значення NULL). Сподіваюся, до сих пір все було зрозуміло, а ми тим часом обговорили звернення до атрибутів.
Перед тим, як розглянути доступ до атрибутів екземплярів, дозвольте сказати про маловідомий (але дуже важливому!) Понятті: дескрипторі. Дескриптори відіграють особливу роль при доступі до атрибутів екземплярів, і мені варто пояснити, що це таке. Об'єкт вважається дескриптором, якщо його один або два слота його типу (tp_descr_get і / або tp_descr_set) заповнені ненульовими значеннями. Ці слоти пов'язані зі спеціальними методами __get__. __set__ і __delete__ (наприклад, якщо ви визначите клас з методом __get__. який зв'яжеться зі слотом tp_descr_get. і створите об'єкт цього класу, то цей об'єкт буде дескриптором). Нарешті, об'єкт вважається дескриптором даних. якщо ненульовим значенням заповнений слот tp_descr_set. Як ми побачимо, дескриптори грають важливу роль в доступі до атрибутів, і я ще дам деякі пояснення і посилання на необхідну документацію.
Так, ми розібралися, що таке дескриптори, і зрозуміли, як відбувається доступ до атрибутів типів. Але більшість об'єктів - НЕ типи, тобто їх тип - НЕ type. а що-небудь більш прозаїчне, наприклад, int. dict або призначений для користувача клас. Всі вони покладаються на універсальні функції доступу до атрибутів, які або визначені в типі, або успадковані від батька типу при його створенні (цю тему, успадкування слотів, ми обговорили в «Голові»). Алгоритм роботи універсальної функції звернення до атрибутів (PyObject_GenericGetAttr) виглядає так:
- Шукати в словнику типу примірника і в словниках усіх батьків типу. Якщо виявлений дескриптор даних. викликати його функцію tp_descr_get і повернути результат. Якщо знайдено щось інше, запам'ятати це на всякий випадок (наприклад, під ім'ям X).
- Шукати в словнику об'єкта, і повернути результат, якщо він знайдений.
- Якщо в словнику об'єкта нічого не було знайдено, перевірити X. якщо він був встановлений; якщо X - дескриптор, викликати його функцію tp_descr_get і повернути результат. Якщо X - звичайний об'єкт, повернути його.
- Нарешті, якщо нічого не було знайдено, викинути виняток AttributeError.
Зауважу, що ми тільки що отримали повне розуміння об'єктно-орієнтованого спадкування в Пітоні: тому що пошук атрибутів починається з типу об'єкта, а потім у всіх батьків, ми розуміємо, що звернення до атрибуту A об'єкта O класу C1. який успадковується від C2. який в свою чергу успадковується від C3. може повернути A і з O. і C1. і C2 і C3. що визначається таким собі порядком дозволу методів, який непогано описаний тут. Цього способу вирішення атрибутів спільно з успадкуванням слотів досить, щоб пояснити більшу частину функціоналу спадкування в Пітоні (хоча диявол, як завжди, криється в деталях).
Ми створили новий клас, об'єкт і визначили у нього атрибут (o.foo = 'bar'), увійшли в gdb. разименовалі тип об'єкта (C) і знайшли його tp_dictoffset (16), а потім перевірили, що ж знаходиться за цією зміщення в C-структурі об'єкта. Не дивно, але ми виявили там словник об'єкта з одним ключем foo. що вказує на значення bar. Природно, якщо перевірити tp_dictoffset типу, у якого немає __dict__. наприклад у object. то ми виявимо там нуль. Мурашки по шкірі, так?
Той факт, що словники типів і словники примірників схожі, але їх реалізації чимало розрізняються, може збентежити. Залишається ще кілька загадок. Давайте підіб'ємо підсумок і визначимо, що ми упустили: визначаємо порожній клас C успадковані від object. створюємо об'єкт o цього класу, виділяється додаткова пам'ять для покажчика на словник по зсуву tp_dictoffset (місце виділено з самого початку, але словник виділяється тільки при першому (будь-якому) зверненні; ось же пройдисвіти.). Потім виконуємо в інтерпретаторі o .__ dict__. компілюється байт-код з командою LOAD_ATTR. яка викликає функцію PyObject_GetAttr. яка разименовивает тип об'єкта o і знаходить слот tp_getattro. який запускає стандартний процес пошуку атрибутів, описаний вище і реалізований в PyObject_GenericGetAttr. У підсумку, після того, як це все відбувається, що повертає словник нашого об'єкта? Ми знаємо де зберігається словник, але можна побачити, що в __dict__ немає його самого, таким чином виникає проблема курки і яйця: що повертає нам словник, коли ми звертаємося до __dict__. якщо в самому ж словнику його немає?
Щось, у чого є пріоритет над словником об'єкта - дескриптор. дивіться:
Ось це так! Відти, що є щось під назвою getset_descriptor (файл ./Objects/typeobject.c), якась група функцій, що реалізує дескрипторна протокол, і яка повинна знаходитися в об'єкті __dict__ типу. Цей дескриптор перехопить всі спроби доступу до o .__ dict__ об'єктів даного типу і поверне все, що йому хочеться, в нашому випадку, це буде покажчик на словник по зсуву tp_dictoffset в o. Це також пояснює, чому ми бачили dict_proxy трохи раніше. Якщо в tp_dict знаходиться покажчик на простий словник, чому ми бачимо його обернутим в об'єкт, в який неможливо щось записати? Це робить дескриптор __dict__ типу нашого типу, type.
Закінчимо таким прикладом:
Цей дескриптор - функція, яка обертає словник простим об'єктом, який симулює поведінку звичайного словника за винятком того, що він доступний тільки на читання. Чому ж так важливо запобігти втручанню користувача в __dict__ типу? Тому що простір імен типу може містити спеціальні методи, наприклад __sub__. Коли ми створюємо тип зі спеціальними методами, або коли визначаємо їх у типу через атрибути, виконується функція update_one_slot. яка зв'яже ці методи зі слотами типу, наприклад, як це відбувалося з операцією віднімання в попередньому пості. Якби ми могли додати ці методи прямо в __dict__ типу, вони б не зв'язалися зі слотами, і ми отримали ми тип, який схожий на те, що потрібно (наприклад, у нього є __sub__ в словнику), але який поводиться інакше .
А знаєте що? Не тільки дівчатам. Нам теж такі хлопці подобаються. Приходьте до нас. Разом веселіше.