Термінологічні зауваження
За браком загальноприйнятої термінології щодо класів, я час від часу використовуватиму терміни, запозичені із Smalltalk та C++. (Як на мене, то я б особисто надав перевагу термінології мови Modula-3, через те, що її обє'ктна семантика ближча до Пайтона, ніж семантика мови C++, але вважаю, що чули про цю мову набагато менше читачів).
Об'єкти зберігають свою індивідуальність, і численні назви (в численних контекстах) можуть бути прив'язані до одного і того ж об'єкта. В інших мовах це відомо як "псевдонімізація". На перший погляд ця риса не отримує належного визнання і може бути безпечно проігнорована при роботі з базовими незмінними типами (числами, рядками, кортежами). Але псевдонімізація має (навмисні!) наслідки в семантиці коду Пайтона при роботі зі змінними об'єктами, такими як списки, словники і більшість типів, що репрезентують зовнішні одиниці (файли, вікна тощо). Зазвичай це діє на користь програмі, тому що псевдоніми до певної міри поводяться як вказівники (пойтнери). Зокрема, передача об'єкта — доволі ефективна операція, тому що передається лише вказівник, і якщо функція змінює об'єкт, переданий аргументом, то зміна буде видима і в середовищі, що викликало цю функцію; це дозволяє уникнути подвійного механізму передачі аргументів, який існує в мові Pascal.
Контексти та іменні простори
Перед тим, як починати розповідь про класи, слід розповісти дещо про правила контексту в мові Пайтон. Визначення класів певним чином впливають на іменні простори і для того, щоб повністю розуміти, що відбувається, потрібно розуміти, як саме взаємодіють контексти та іменні простори. Знання цього матеріалу також корисне для будь-якого досвідченого програміста мови Пайтон.
Почнімо з деяких визначень.
Іменний простір — це проектування назв у об'єкти. Більшість іменних просторів імплементовані як словники мови Пайтон, що загалом зовсім непомітне (за винятком ефективності виконання програми), але це може змінитися в майбутньому. Приклади іменних просторів: вбудовані назви (функції, наприклад abs(), та вбудовані винятки); глобальні назви всередині модуля; локальні назви при виклику функції. В певному розумінні набір атрибутів об'єкта також утворює іменний простір. Необхідно знати, що назви із різних іменних просторів зовсім не пов'язані між собою; наприклад, два різних модулі можуть визначити функцію maximize без жодного ризику поплутання — при виклику потрібної функції користувачі модулів повинні префіксувати її назвою відповідного модуля.
Між іншим, слово атрибут тут стосується будь-якої назви, розташованої за крапкою — наприклад, у виразі z.real, real - це атрибут об'єкта z. Строго кажучи, посилання на назви всередині модуля — це атрибутивні посилання: у виразі modname.funcname, modname — назва об'єкта модуля і funcname - його атрибут. У цьому випадку відбувається пряме проектування між атрибутами модуля і визначеними в ньому глобальними назвами: вони всі поділяють один іменний простір! За винятком одного. Об'єкти модулів мають секретний атрибут, що доступний лише для зчитування, який зветься __dict__, і який повертає словник, що використовується для імплементації іменного простору модуля; назва __dict__ — лише атрибут, який не є глобальною назвою. Очевидно його використання порушує абстракцію дизайну іменного простору і тому повинно обмежуватися лише такими речами як зневаджувач при пошуку помилок.
Атрибути можуть бути призначені як для зчитування, так і для запису нових ин. Останнє стосується атрибутів модулів, для яких можна написати: "modname.the_answer = 42". Змінні атрибути можуть видалятися за допомогою твердження del. наприклад,"del modname.the_answer" видалить атрибут the_answer з об'єкта, на який посилається назва modname.
Іменні простори створюються в різний час і мають різну тривалість життя. Іменний простір вбудованих назв створюється під час завантаження інтерпретатора і ніколи не видаляється. Глобальний іменний простір модуля створюється під час зчитування його визначення і загалом існує до виходу інтерпретатора. Твердження, що виконуються інтерпретатором на найвищому рівні (зчитані зі скрипта чи введені у діалоговому режимі), вважаються частиною модуля __main__, і таким чином мають свій власний іменний простір. Вбудовані назви також живіть у окремому модулі, що зветься __builtin__.
Локальний іменний простір функції створюється під час її виклику і видаляється при поверненні нею результату або при зведенні нею винятку, необробленого у самій цій функції. (Власне термін "забування" краще відбивав би те, що відбувається насправді). І, звичайно, кожен рекурсивний виклик функції має свій власний іменний простір.
Контекст — це текстуальний регіон програми, написаної на Пайтоні, де іменний простір доступний напряму. "Доступний напряму" тут означає, що безпосереднє посилання на назву спричинить її пошук у цьому іменному просторі.
Хоча контексти визначаються статично, вони використовуються динамічно. У будь-який момент виконання програми існують принаймні три вкладених контексти з доступними напряму іменними просторами:
Внутрішній (з якого починається пошук) містить в собі локальні назви. Іменний простір оточуючих функцій, де пошук розпочинається з найближчого оточуючого контексту. Середній контекст (де продовжується пошук) містить глобальні назви поточного модуля. Зовнішній контекст, де пошук завершується, — це іменний простір, що містить вбудовані назви.
Якщо назва задекларована глобально, то всі посилання і призначення відбуваються у середньому контексті, де зберігаються глобальні назви модуля. В інакшому випадку, всі змінні, що зустрічаються поза внутрішнім контекстом, призначені лише для зчитування.
Загалом локальний контекст посилається на локальні назви (текстуально) поточної функції. Поза функціями локальний контекст посилається на той же іменний простір, що й глобальний контекст: іменний простір модуля. Визначення класів додають іще один іменний простір до локального контексту.
Важливо усвідомити, що контексти визначаються текстуально: глобальний контекст функції, визначеної в модулі, є іменним простором цього модуля, незалежно від того, звідки чи за допомогою якого псевдоніма її викликано. З іншого боку, сам пошук назв відбувається динамічно, під час виконання, хоча мова еволюціонує до статичного вирішення назв під час "компіляції", отже не варто покладатися на динамічне вирішення назв! (Уже зараз локальні змінні вирішуються статично).
Особливістю Пайтона є те, що призначення завжди відбуваються у внутрішньому контексті. Призначення не копіюють дані, а лише прив'язують назви до об'єктів. Те саме стосується і видалення: твердження "del x" видаляє прив'язку до x із іменного простору локального контексту. Насправді всі операції, що визначають нові назви, використовують локальний контекст: зокрема імпортування та визначення функцій прив'язують назву модуля чи функції у локальному контексті. (Твердження global може використовуватися як індикатор того, що дана змінна існує у глобальному контексті).
Перший погляд на класи
Класи додають трохи нового синтаксису, три нових типи об'єктів та трохи нової семантики.
Синтаксис визначення класів
Найпростіша форма визначення класу виглядає так:
class ClassName: твердження-1; . . . твердження-N;
Визначення класів, подібно до визначень функцій (заданих через твердження def) , повинні виконатися перед тим, як вони матимуть якийсь ефект. (Визначення класу в принципі може розташовуватися у розгалуженні твердження if чи всередині функції).
На практиці твердження всередині класу здебільшого містять визначення функцій, але й інші твердження тут дозволяються також — ми повернемося до цього пізніше. Визначення функцій, розташованих всередині класів мають особливу форму списку аргументів, що диктується правилами виклику методів, але про це також йтиметься далі.
При вході у визначення класу створюється новий іменний простір, що використовується у якості локального контексту, таким чином всі призначення нових ин локальним змінним потрапляють у цей новий іменний простір. Зокрема визначення функцій прив'язують назву нової функції тут.
Після нормального виходу з визначення класу (при його закінченні) створюється класовий об'єкт, який фактично є оболонкою змісту іменного простору, створеного цим визначенням (про класові об'єкти йтиметься в наступному розділі). Після цього поновлюється початковий локальний контекст (той, що існував перед входом до визначення класу), у якому класовий об'єкт прив'язується до вказаної у заголовці визначення класу назви (у наведеному вище прикладі ClassName).
Класові об'єкти
Класові об'єкти підтримують два типи операцій: атрибутивні посилання та реалізацію.
Атрибутивні посилання використовують стандартний синтаксис атрибутивних посилань мови Пайтон у вигляді obj.name. Дійсні атрибутивні назви складаються з усіх назв, що існували в іменному просторі класу під час створення класового об'єкта. Тобто, якщо визначення класу виглядало таким чином:
class MyClass: "Простенький приклад класу" i = 12345 def f(self): return 'привіт, світе'
то MyClass.i and MyClass.f є дійсними атрибутивними посиланнями, що повертають цілочислову ину та об'єкт методу відповідно. Атрибути класу можуть також набувати нових значень, тобто MyClass.i може змінюватися через призначення. __doc__ - також дійсний атрибут; він повертає рядок документації відповідного класу: "Простенький приклад класу".
Реалізація класу використовує нотацію функцій. Просто вдайте собі, що класовий об'єкт - це безпараметрова функція, яка повертає нову реалізацію класу. Відповідно до наведеного вище прикладу
x = MyClass()
створює нову реалізацію класу і призначає цей новостворений об'єкт локальній змінній x.
Операція реалізації ("виклик" класового об'єкта) створює пустий об'єкт. Часто класи створюють об'єкти із заданим початковим станом. Для цього клас може визначити спеціальний метод, що зветься __init__(), наприклад:
def __init__(self): self.data = []
Якщо в класі визначено метод __init__(), то він автоматично викликається при реалізації нового класу. Для наведеного вище прикладу нова ініціалізована реалізація класу може бути отримана так:
x = MyClass()
І звичайно, метод __init__() для більшої зручності може мати і аргументи, що передаються йому під час реалізації класу. Наприклад:
>>> class Complex: ... def __init__(self, realpart, imagpart): ... self.r = realpart ... self.i = imagpart ... >>> x = Complex(3.0, -4.5) >>> x.r, x.i (3.0, -4.5)
Реалізовані об'єкти
Що ж можна робити з реалізованими об'єктами? Єдиний тип операцій, зрозумілий реалізованим об'єктам — це атрибутивне посилання. Існують два типи дійсних атрибутивних назв.
Перший тип можна умовно назвати атрибутами даних, які відповідають "реалізованим змінним" у мові Smalltalk та "членам даних" у C++. Декларувати атрибути даних непотрібно; подібно до локальних змінних вони починають існувати при першому призначенні. Наприклад, якщо x є реалізацією вищезгаданого класу MyClass, то цей шматок коду виведе ину 16 і не залишить від неї жодного сліду:
x.counter = 1 while x.counter < 10: x.counter = x.counter * 2 print x.counter del x.counter
Другий тип атрибутивного посилання, зрозумілий реалізованим об'єктам - це методи. Метод — це просто функція, що "належить" об'єкту. (У Пайтоні термін "метод" вживається не лише по відношенню до реалізацій класів: інші типи об'єктів можуть також мати методи. Зокрема, спискові об'єкти мають методи для додавання, видалення, вставки, сортування тощо. Однак у цьому розділі термін "метод" вживатиметься винятково для позначення об'єктів реалізованих класів, якщо не зазначено інакше).
Дійсні назви методів реалізованих об'єктів залежать від відповідного класу. За визначенням усі атрибути класу, які є (визначеними користувачем) об'єктами функцій, визначають відповідні методи реалізованих об'єктів. Зокрема, x.f — це дійсне посилання методу, тому що MyClass.f — функція, але x.i - ні, тому що MyClass.i не є функцією. Але x.f - не те саме, що й MyClass.f -- це об'єкт методу, а не функції.
Об'єкти методів