9. Класи

Класи надають засоби об’єднання даних і функціональних можливостей. Створення нового класу створює новий тип об’єкта, що дозволяє створювати нові примірники цього типу. Кожен екземпляр класу може мати атрибути, приєднані до нього для підтримки його стану. Екземпляри класу також можуть мати методи (визначені його класом) для зміни свого стану.

Порівняно з іншими мовами програмування, механізм класів Python додає класи з мінімумом нового синтаксису та семантики. Це суміш механізмів класів, знайдених у C++ і Modula-3. Класи Python забезпечують усі стандартні функції об’єктно-орієнтованого програмування: механізм успадкування класів дозволяє створювати кілька базових класів, похідний клас може перевизначати будь-які методи свого базового класу або класів, а метод може викликати метод базового класу з тим самим іменем. . Об’єкти можуть містити довільні обсяги та типи даних. Як і для модулів, класи мають динамічну природу Python: вони створюються під час виконання та можуть бути змінені далі після створення.

У термінології C++ зазвичай члени класу (включно з членами даних) є публічними (за винятком див. нижче Приватні змінні), а всі функції-члени є віртуальними. Як і в Modula-3, немає скорочень для посилань на члени об’єкта з його методів: функція методу оголошується з явним першим аргументом, що представляє об’єкт, який неявно надається викликом. Як і в Smalltalk, класи самі є об’єктами. Це забезпечує семантику для імпортування та перейменування. На відміну від C++ і Modula-3, вбудовані типи можуть використовуватися як базові класи для розширення користувачем. Також, як і в C++, більшість вбудованих операторів зі спеціальним синтаксисом (арифметичні оператори, індексування тощо) можна перевизначати для екземплярів класу.

(Через відсутність загальноприйнятої термінології для розмови про класи, я час від часу використовую терміни Smalltalk і C++. Я б використовував терміни Modula-3, оскільки його об’єктно-орієнтована семантика ближча до семантики Python, ніж C++, але я очікую, що мало читачів чув про це.)

9.1. Слово про імена та предмети

Об’єкти мають індивідуальність, і кілька імен (у кількох областях) можуть бути прив’язані до одного об’єкта. Це відоме як псевдонім в інших мовах. Це зазвичай не оцінюється з першого погляду на Python, і його можна сміливо ігнорувати, коли маєте справу з незмінними базовими типами (числа, рядки, кортежі). Однак псевдонім має, можливо, дивовижний ефект на семантику коду Python, що включає змінні об’єкти, такі як списки, словники та більшість інших типів. Зазвичай це використовується на користь програми, оскільки псевдоніми в деяких аспектах поводяться як покажчики. Наприклад, передача об’єкта дешева, оскільки реалізація передає лише покажчик; і якщо функція змінює об’єкт, переданий як аргумент, абонент побачить зміни — це усуває потребу у двох різних механізмах передачі аргументів, як у Pascal.

9.2. Області та простори імен Python

Перш ніж представити класи, я спершу маю розповісти вам дещо про правила видимості Python. Визначення класів грають влучні трюки з просторами імен, і вам потрібно знати, як працюють області та простори імен, щоб повністю зрозуміти, що відбувається. До речі, знання з цієї теми знадобляться будь-якому досвідченому програмісту на Python.

Почнемо з деяких визначень.

Простір імен — це відображення імен на об’єкти. Більшість просторів імен наразі реалізовано як словники Python, але зазвичай це нічим не помітно (за винятком продуктивності), і це може змінитися в майбутньому. Прикладами просторів імен є: набір вбудованих імен (що містять такі функції, як abs(), і вбудовані імена винятків); глобальні імена в модулі; і локальні імена під час виклику функції. У певному сенсі набір атрибутів об’єкта також формує простір імен. Важливо знати про простори імен, що між іменами в різних просторах імен немає абсолютно ніякого зв’язку; наприклад, обидва різні модулі можуть визначати функцію maximize без плутанини — користувачі модулів повинні додавати перед нею назву модуля.

До речі, я використовую слово attribute для будь-якого імені після крапки — наприклад, у виразі z.real, real є атрибутом об’єкта z . Строго кажучи, посилання на імена в модулях є посиланнями на атрибути: у виразі modname.funcname, modname є об’єктом модуля, а funcname є його атрибутом. У цьому випадку відбувається пряме відображення між атрибутами модуля та глобальними іменами, визначеними в модулі: вони спільно використовують той самий простір імен! [1]

Attributes may be read-only or writable. In the latter case, assignment to attributes is possible. Module attributes are writable: you can write modname.the_answer = 42. Writable attributes may also be deleted with the del statement. For example, del modname.the_answer will remove the attribute the_answer from the object named by modname.

Простори імен створюються в різний момент і мають різний час життя. Простір імен, що містить вбудовані імена, створюється під час запуску інтерпретатора Python і ніколи не видаляється. Глобальний простір імен для модуля створюється, коли зчитується визначення модуля; зазвичай простори імен модулів також зберігаються, доки інтерпретатор не завершить роботу. Інструкції, що виконуються за допомогою виклику інтерпретатора верхнього рівня, або прочитані з файлу сценарію, або в інтерактивному режимі, вважаються частиною модуля під назвою __main__, тому вони мають власний глобальний простір імен. (Вбудовані імена насправді також містяться в модулі; це називається builtins.)

Локальний простір імен для функції створюється під час виклику функції та видаляється, коли функція повертає або викликає виняткову ситуацію, яка не обробляється цією функцією. (Насправді, забування було б кращим способом описати те, що насправді відбувається.) Звичайно, кожен рекурсивний виклик має свій власний локальний простір імен.

Область — це текстова область програми Python, де простір імен доступний безпосередньо. «Безпосередній доступ» тут означає, що некваліфіковане посилання на ім’я намагається знайти ім’я в просторі імен.

Хоча області визначаються статично, вони використовуються динамічно. У будь-який час під час виконання є 3 або 4 вкладені області, простори імен яких доступні безпосередньо:

  • внутрішня область, яка шукається першою, містить локальні назви

  • the scopes of any enclosing functions, which are searched starting with the nearest enclosing scope, contain non-local, but also non-global names

  • передостання область містить глобальні імена поточного модуля

  • крайня область (шукається останньою) — це простір імен, що містить вбудовані імена

If a name is declared global, then all references and assignments go directly to the next-to-last scope containing the module’s global names. To rebind variables found outside of the innermost scope, the nonlocal statement can be used; if not declared nonlocal, those variables are read-only (an attempt to write to such a variable will simply create a new local variable in the innermost scope, leaving the identically named outer variable unchanged).

Зазвичай локальна область посилається на локальні імена поточної (текстової) функції. Поза функціями локальна область посилається на той самий простір імен, що й глобальна область: простір імен модуля. Визначення класу розміщують ще один простір імен у локальній області.

Важливо розуміти, що області визначаються текстово: глобальна область дії функції, визначеної в модулі, є простором імен цього модуля, незалежно від того, звідки чи яким псевдонімом викликається функція. З іншого боку, фактичний пошук імен виконується динамічно, під час виконання — однак визначення мови розвивається в бік статичного дозволу імен під час «компіляції», тому не покладайтеся на динамічне дозвіл імен! (Насправді локальні змінні вже визначені статично.)

Особлива примха Python полягає в тому, що, якщо не діють оператори global або nonlocal, призначення імен завжди потрапляють у найглибшу область видимості. Призначення не копіюють дані — вони просто прив’язують імена до об’єктів. Те саме стосується видалень: оператор del x видаляє прив’язку x із простору імен, на який посилається локальна область видимості. Насправді всі операції, які вводять нові імена, використовують локальну область видимості: зокрема, оператори import і визначення функцій прив’язують назву модуля або функції в локальній області видимості.

Інструкція global може бути використана, щоб вказати, що певні змінні живуть у глобальній області видимості та мають бути перенаправлені туди; оператор nonlocal вказує на те, що певні змінні живуть в охоплюючій області видимості і мають бути перенаправлені туди.

9.2.1. Приклад областей і просторів імен

Це приклад, який демонструє, як посилатися на різні області та простори імен, і як global і nonlocal впливають на зв’язування змінних:

def scope_test():
    def do_local():
        spam = "local spam"

    def do_nonlocal():
        nonlocal spam
        spam = "nonlocal spam"

    def do_global():
        global spam
        spam = "global spam"

    spam = "test spam"
    do_local()
    print("After local assignment:", spam)
    do_nonlocal()
    print("After nonlocal assignment:", spam)
    do_global()
    print("After global assignment:", spam)

scope_test()
print("In global scope:", spam)

Результат коду прикладу:

After local assignment: test spam
After nonlocal assignment: nonlocal spam
After global assignment: nonlocal spam
In global scope: global spam

Зверніть увагу, що призначення local (яке є типовим) не змінило прив’язки scope_test до spam. Призначення nonlocal змінило прив’язку scope_testдо спаму, а призначення global змінило прив’язку на рівні модуля.

Ви також можете побачити, що не було попереднього зв’язування для spam перед призначенням global.

9.3. Перший погляд на заняття

Класи представляють трохи нового синтаксису, три нових типи об’єктів і нову семантику.

9.3.1. Синтаксис визначення класу

Найпростіша форма визначення класу виглядає так:

class ClassName:
    <statement-1>
    .
    .
    .
    <statement-N>

Визначення класів, як і визначення функцій (оператори def), мають бути виконані, перш ніж вони матимуть будь-який ефект. (Імовірно, ви можете розмістити визначення класу в гілці оператора if або всередині функції.)

На практиці оператори всередині визначення класу зазвичай є визначеннями функцій, але допускаються й інші оператори, іноді корисні — ми повернемося до цього пізніше. Визначення функції всередині класу зазвичай мають особливу форму списку аргументів, продиктовану умовами виклику методів — знову ж таки, це пояснюється пізніше.

Коли вводиться визначення класу, створюється новий простір імен і використовується як локальна область — таким чином, усі призначення локальним змінним переходять до цього нового простору імен. Зокрема, визначення функції прив’язують тут назву нової функції.

When a class definition is left normally (via the end), a class object is created. This is basically a wrapper around the contents of the namespace created by the class definition; we’ll learn more about class objects in the next section. The original local scope (the one in effect just before the class definition was entered) is reinstated, and the class object is bound here to the class name given in the class definition header (ClassName in the example).

9.3.2. Об’єкти класу

Об’єкти класу підтримують два типи операцій: посилання на атрибути та інстанціювання.

Посилання на атрибути використовують стандартний синтаксис, який використовується для всіх посилань на атрибути в Python: obj.name. Дійсні імена атрибутів — це всі імена, які були в просторі імен класу під час створення об’єкта класу. Отже, якби визначення класу виглядало так:

class MyClass:
    """A simple example class"""
    i = 12345

    def f(self):
        return 'hello world'

then MyClass.i and MyClass.f are valid attribute references, returning an integer and a function object, respectively. Class attributes can also be assigned to, so you can change the value of MyClass.i by assignment. __doc__ is also a valid attribute, returning the docstring belonging to the class: "A simple example class".

Клас instantiation використовує нотацію функції. Просто уявіть, що об’єкт класу є функцією без параметрів, яка повертає новий екземпляр класу. Наприклад (припускаючи вищезгаданий клас):

x = MyClass()

створює новий примірник класу та призначає цей об’єкт локальній змінній x.

The instantiation operation («calling» a class object) creates an empty object. Many classes like to create objects with instances customized to a specific initial state. Therefore a class may define a special method named __init__(), like this:

def __init__(self):
    self.data = []

When a class defines an __init__() method, class instantiation automatically invokes __init__() for the newly created class instance. So in this example, a new, initialized instance can be obtained by:

x = MyClass()

Of course, the __init__() method may have arguments for greater flexibility. In that case, arguments given to the class instantiation operator are passed on to __init__(). For example,

>>> 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)

9.3.3. Об’єкти екземпляра

Тепер що ми можемо робити з об’єктами екземплярів? Єдиними операціями, які розуміють об’єкти екземплярів, є посилання на атрибути. Є два типи дійсних імен атрибутів: атрибути даних і методи.

data attributes correspond to «instance variables» in Smalltalk, and to «data members» in C++. Data attributes need not be declared; like local variables, they spring into existence when they are first assigned to. For example, if x is the instance of MyClass created above, the following piece of code will print the value 16, without leaving a trace:

x.counter = 1
while x.counter < 10:
    x.counter = x.counter * 2
print(x.counter)
del x.counter

The other kind of instance attribute reference is a method. A method is a function that «belongs to» an object.

Дійсні імена методів екземпляра об’єкта залежать від його класу. За визначенням, усі атрибути класу, які є функціональними об’єктами, визначають відповідні методи його екземплярів. Отже, у нашому прикладі x.f є дійсним посиланням на метод, оскільки MyClass.f є функцією, але x.i ні, оскільки MyClass.i не є таким. Але x.f — це не те саме, що MyClass.f — це об’єкт методу, а не об’єкт функції.

9.3.4. Об’єкти методу

Зазвичай метод викликається одразу після його зв’язування:

x.f()

In the MyClass example, this will return the string 'hello world'. However, it is not necessary to call a method right away: x.f is a method object, and can be stored away and called at a later time. For example:

xf = x.f
while True:
    print(xf())

продовжуватиме друкувати hello world до кінця часу.

What exactly happens when a method is called? You may have noticed that x.f() was called without an argument above, even though the function definition for f() specified an argument. What happened to the argument? Surely Python raises an exception when a function that requires an argument is called without any — even if the argument isn’t actually used…

Насправді, ви, можливо, здогадалися відповідь: особливість методів полягає в тому, що об’єкт екземпляра передається як перший аргумент функції. У нашому прикладі виклик x.f() точно еквівалентний MyClass.f(x). Загалом, виклик методу зі списком n аргументів еквівалентний виклику відповідної функції зі списком аргументів, який створюється шляхом вставки об’єкта екземпляра методу перед першим аргументом.

In general, methods work as follows. When a non-data attribute of an instance is referenced, the instance’s class is searched. If the name denotes a valid class attribute that is a function object, references to both the instance object and the function object are packed into a method object. When the method object is called with an argument list, a new argument list is constructed from the instance object and the argument list, and the function object is called with this new argument list.

9.3.5. Змінні класу та екземпляра

Загалом, змінні екземпляра призначені для даних, унікальних для кожного екземпляра, а змінні класу призначені для атрибутів і методів, спільних для всіх екземплярів класу:

class Dog:

    kind = 'canine'         # class variable shared by all instances

    def __init__(self, name):
        self.name = name    # instance variable unique to each instance

>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.kind                  # shared by all dogs
'canine'
>>> e.kind                  # shared by all dogs
'canine'
>>> d.name                  # unique to d
'Fido'
>>> e.name                  # unique to e
'Buddy'

Як обговорювалося в Слово про імена та предмети, спільні дані можуть мати несподіваний ефект із залученням mutable об’єктів, таких як списки та словники. Наприклад, список tricks у наступному коді не слід використовувати як змінну класу, тому що лише один список буде спільний для всіх екземплярів Dog:

class Dog:

    tricks = []             # mistaken use of a class variable

    def __init__(self, name):
        self.name = name

    def add_trick(self, trick):
        self.tricks.append(trick)

>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.add_trick('roll over')
>>> e.add_trick('play dead')
>>> d.tricks                # unexpectedly shared by all dogs
['roll over', 'play dead']

Правильний дизайн класу повинен використовувати замість змінної екземпляра:

class Dog:

    def __init__(self, name):
        self.name = name
        self.tricks = []    # creates a new empty list for each dog

    def add_trick(self, trick):
        self.tricks.append(trick)

>>> d = Dog('Fido')
>>> e = Dog('Buddy')
>>> d.add_trick('roll over')
>>> e.add_trick('play dead')
>>> d.tricks
['roll over']
>>> e.tricks
['play dead']

9.4. Випадкові зауваження

Якщо однакове ім’я атрибута зустрічається як в екземплярі, так і в класі, пошук атрибутів визначає пріоритет екземпляру:

>>> class Warehouse:
...    purpose = 'storage'
...    region = 'west'
...
>>> w1 = Warehouse()
>>> print(w1.purpose, w1.region)
storage west
>>> w2 = Warehouse()
>>> w2.region = 'east'
>>> print(w2.purpose, w2.region)
storage east

На атрибути даних можуть посилатися як методи, так і звичайні користувачі («клієнти») об’єкта. Іншими словами, класи не можна використовувати для реалізації чистих абстрактних типів даних. Фактично, ніщо в Python не дозволяє примусово приховувати дані — все базується на конвенції. (З іншого боку, реалізація Python, написана мовою C, може повністю приховати деталі реалізації та контролювати доступ до об’єкта, якщо це необхідно; це можна використовувати розширеннями Python, написаними мовою C.)

Клієнти повинні використовувати атрибути даних обережно — клієнти можуть зіпсувати інваріанти, які підтримуються методами, штампуючи свої атрибути даних. Зауважте, що клієнти можуть додавати власні атрибути даних до об’єкта екземпляра, не впливаючи на валідність методів, за умови уникнення конфліктів імен — знову ж таки, угода про іменування може позбавити тут багато головного болю.

Не існує скорочення для посилання на атрибути даних (або інші методи!) з методів. Я вважаю, що це фактично підвищує читабельність методів: немає жодного шансу сплутати локальні змінні та змінні екземпляра під час перегляду методу.

Часто перший аргумент методу називається self. Це не що інше, як конвенція: назва self не має абсолютно ніякого особливого значення для Python. Зауважте, однак, що через недотримання угоди ваш код може бути менш читабельним для інших програмістів на Python, і також можливо, що програма браузера класів може бути написана, яка спирається на таку угоду.

Будь-який функціональний об’єкт, який є атрибутом класу, визначає метод для екземплярів цього класу. Не обов’язково, щоб визначення функції було текстово укладено у визначення класу: присвоєння об’єкта функції локальній змінній у класі також є нормальним. Наприклад:

# Function defined outside the class
def f1(self, x, y):
    return min(x, x+y)

class C:
    f = f1

    def g(self):
        return 'hello world'

    h = g

Now f, g and h are all attributes of class C that refer to function objects, and consequently they are all methods of instances of Ch being exactly equivalent to g. Note that this practice usually only serves to confuse the reader of a program.

Методи можуть викликати інші методи, використовуючи атрибути методу аргументу self:

class Bag:
    def __init__(self):
        self.data = []

    def add(self, x):
        self.data.append(x)

    def addtwice(self, x):
        self.add(x)
        self.add(x)

Методи можуть посилатися на глобальні імена так само, як і звичайні функції. Глобальною областю, пов’язаною з методом, є модуль, що містить його визначення. (Клас ніколи не використовується як глобальна область видимості.) Хоча рідко можна зустріти вагому причину для використання глобальних даних у методі, існує багато законних застосувань глобальної області видимості: з одного боку, функції та модулі, імпортовані в глобальну область видимості, можуть використовуватися методами, а також функціями та класами, визначеними в ньому. Зазвичай клас, що містить метод, сам визначений у цій глобальній області видимості, і в наступному розділі ми знайдемо кілька вагомих причин, чому метод хоче посилатися на власний клас.

Кожне значення є об’єктом і, отже, має клас (також називається його типом). Він зберігається як object.__class__.

9.5. Спадщина

Звичайно, функція мови не була б гідною назви «клас» без підтримки успадкування. Синтаксис для визначення похідного класу виглядає так:

class DerivedClassName(BaseClassName):
    <statement-1>
    .
    .
    .
    <statement-N>

The name BaseClassName must be defined in a namespace accessible from the scope containing the derived class definition. In place of a base class name, other arbitrary expressions are also allowed. This can be useful, for example, when the base class is defined in another module:

class DerivedClassName(modname.BaseClassName):

Виконання визначення похідного класу відбувається так само, як і для базового класу. Коли об’єкт класу створено, базовий клас запам’ятовується. Це використовується для вирішення посилань на атрибути: якщо запитуваний атрибут не знайдено в класі, пошук продовжується до базового класу. Це правило застосовується рекурсивно, якщо сам базовий клас є похідним від якогось іншого класу.

У створенні похідних класів немає нічого особливого: DerivedClassName() створює новий екземпляр класу. Посилання на метод розв’язуються наступним чином: виконується пошук відповідного атрибута класу, спускаючись вниз по ланцюжку базових класів, якщо необхідно, і посилання на метод є дійсним, якщо це дає об’єкт функції.

Похідні класи можуть перевизначати методи своїх базових класів. Оскільки методи не мають спеціальних привілеїв під час виклику інших методів того самого об’єкта, метод базового класу, який викликає інший метод, визначений у тому самому базовому класі, може призвести до виклику методу похідного класу, який замінює його. (Для програмістів C++: усі методи в Python фактично «віртуальні».)

Перевизначений метод у похідному класі може фактично захотіти розширити, а не просто замінити однойменний метод базового класу. Існує простий спосіб безпосередньо викликати метод базового класу: просто викличте BaseClassName.methodname(self, arguments). Це також іноді корисно для клієнтів. (Зауважте, що це працює, лише якщо базовий клас доступний як BaseClassName у глобальній області.)

Python має дві вбудовані функції, які працюють із успадкуванням:

  • Використовуйте isinstance(), щоб перевірити тип екземпляра: isinstance(obj, int) буде True лише якщо obj.__class__ є int або якийсь похідний клас з int.

  • Використовуйте issubclass(), щоб перевірити успадкування класу: issubclass(bool, int) має значення True, оскільки bool є підкласом int. Однак issubclass(float, int) є False, оскільки float не є підкласом int.

9.5.1. Множинне успадкування

Python також підтримує форму множинного успадкування. Визначення класу з кількома базовими класами виглядає так:

class DerivedClassName(Base1, Base2, Base3):
    <statement-1>
    .
    .
    .
    <statement-N>

For most purposes, in the simplest cases, you can think of the search for attributes inherited from a parent class as depth-first, left-to-right, not searching twice in the same class where there is an overlap in the hierarchy. Thus, if an attribute is not found in DerivedClassName, it is searched for in Base1, then (recursively) in the base classes of Base1, and if it was not found there, it was searched for in Base2, and so on.

Насправді це трохи складніше; порядок вирішення методів динамічно змінюється для підтримки кооперативних викликів super(). Цей підхід відомий у деяких інших мовах із множинним успадкуванням як call-next-method і є потужнішим, ніж супервиклик у мовах з одним успадкуванням.

Dynamic ordering is necessary because all cases of multiple inheritance exhibit one or more diamond relationships (where at least one of the parent classes can be accessed through multiple paths from the bottommost class). For example, all classes inherit from object, so any case of multiple inheritance provides more than one path to reach object. To keep the base classes from being accessed more than once, the dynamic algorithm linearizes the search order in a way that preserves the left-to-right ordering specified in each class, that calls each parent only once, and that is monotonic (meaning that a class can be subclassed without affecting the precedence order of its parents). Taken together, these properties make it possible to design reliable and extensible classes with multiple inheritance. For more detail, see The Python 2.3 Method Resolution Order.

9.6. Приватні змінні

«Приватні» змінні екземпляра, до яких можна отримати доступ лише зсередини об’єкта, не існують у Python. Проте існує конвенція, якої дотримується більшість коду Python: ім’я з префіксом підкреслення (наприклад, _spam) має розглядатися як непублічна частина API (незалежно від того, чи це функція, метод чи член даних). Це слід розглядати як деталь впровадження та може бути змінено без попередження.

Оскільки існує дійсний варіант використання для приватних членів класу (а саме, щоб уникнути зіткнень імен імен з іменами, визначеними підкласами), існує обмежена підтримка такого механізму, що називається name mangling. Будь-який ідентифікатор у формі __spam (принаймні два символи підкреслення на початку, не більше одного символу підкреслення в кінці) текстово замінюється на _classname__spam, де classname є поточною назвою класу з підкресленням на початку оголений. Це спотворення виконується без урахування синтаксичної позиції ідентифікатора, доки воно трапляється у визначенні класу.

Викривлення імен корисне для того, щоб дозволити підкласам перевизначати методи, не порушуючи виклики внутрішньокласових методів. Наприклад:

class Mapping:
    def __init__(self, iterable):
        self.items_list = []
        self.__update(iterable)

    def update(self, iterable):
        for item in iterable:
            self.items_list.append(item)

    __update = update   # private copy of original update() method

class MappingSubclass(Mapping):

    def update(self, keys, values):
        # provides new signature for update()
        # but does not break __init__()
        for item in zip(keys, values):
            self.items_list.append(item)

Наведений вище приклад працював би, навіть якби MappingSubclass вводив ідентифікатор __update, оскільки він замінений на _Mapping__update у Mapping класі _MappingSubclass__update у Клас MappingSubclass відповідно.

Зауважте, що правила псування призначені здебільшого для уникнення нещасних випадків; все ще можна отримати доступ або змінити змінну, яка вважається приватною. Це навіть може бути корисним за особливих обставин, наприклад, у налагоджувачі.

Зауважте, що код, переданий у exec() або eval(), не вважає назву класу викликаного класу поточним класом; це схоже на дію оператора global, дія якого так само обмежена кодом, який скомпільовано разом. Те саме обмеження стосується getattr(), setattr() і delattr(), а також прямого посилання на __dict__.

9.7. Обривки

Sometimes it is useful to have a data type similar to the Pascal «record» or C «struct», bundling together a few named data items. The idiomatic approach is to use dataclasses for this purpose:

from dataclasses import dataclass

@dataclass
class Employee:
    name: str
    dept: str
    salary: int
>>> john = Employee('john', 'computer lab', 1000)
>>> john.dept
'computer lab'
>>> john.salary
1000

A piece of Python code that expects a particular abstract data type can often be passed a class that emulates the methods of that data type instead. For instance, if you have a function that formats some data from a file object, you can define a class with methods read() and readline() that get the data from a string buffer instead, and pass it as an argument.

Instance method objects have attributes, too: m.__self__ is the instance object with the method m(), and m.__func__ is the function object corresponding to the method.

9.8. Ітератори

Наразі ви, мабуть, помітили, що більшість об’єктів-контейнерів можна зациклювати за допомогою оператора for:

for element in [1, 2, 3]:
    print(element)
for element in (1, 2, 3):
    print(element)
for key in {'one':1, 'two':2}:
    print(key)
for char in "123":
    print(char)
for line in open("myfile.txt"):
    print(line, end='')

Цей стиль доступу є зрозумілим, лаконічним і зручним. Використання ітераторів пронизує й уніфікує Python. За лаштунками оператор for викликає iter() об’єкта контейнера. Функція повертає об’єкт-ітератор, який визначає метод __next__(), який отримує доступ до елементів у контейнері по одному. Коли елементів більше немає, __next__() викликає виняток StopIteration, який повідомляє циклу for про завершення. Ви можете викликати метод __next__() за допомогою вбудованої функції next(); цей приклад показує, як це все працює:

>>> s = 'abc'
>>> it = iter(s)
>>> it
<str_iterator object at 0x10c90e650>
>>> next(it)
'a'
>>> next(it)
'b'
>>> next(it)
'c'
>>> next(it)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
    next(it)
StopIteration

Having seen the mechanics behind the iterator protocol, it is easy to add iterator behavior to your classes. Define an __iter__() method which returns an object with a __next__() method. If the class defines __next__(), then __iter__() can just return self:

class Reverse:
    """Iterator for looping over a sequence backwards."""
    def __init__(self, data):
        self.data = data
        self.index = len(data)

    def __iter__(self):
        return self

    def __next__(self):
        if self.index == 0:
            raise StopIteration
        self.index = self.index - 1
        return self.data[self.index]
>>> rev = Reverse('spam')
>>> iter(rev)
<__main__.Reverse object at 0x00A1DB50>
>>> for char in rev:
...     print(char)
...
m
a
p
s

9.9. Генератори

Generators — простий і потужний інструмент для створення ітераторів. Вони написані як звичайні функції, але використовують оператор yield, коли вони хочуть повернути дані. Кожного разу, коли на ньому викликається next(), генератор продовжує роботу з того місця, де зупинився (він запам’ятовує всі значення даних і який оператор був виконаний останнім). Приклад показує, що генератори можна тривіально легко створити:

def reverse(data):
    for index in range(len(data)-1, -1, -1):
        yield data[index]
>>> for char in reverse('golf'):
...     print(char)
...
f
l
o
g

Anything that can be done with generators can also be done with class-based iterators as described in the previous section. What makes generators so compact is that the __iter__() and __next__() methods are created automatically.

Інша ключова особливість полягає в тому, що локальні змінні та стан виконання автоматично зберігаються між викликами. Це зробило функцію легшою для написання та набагато зрозумілішою, ніж підхід із використанням змінних екземплярів, таких як self.index і self.data.

На додаток до автоматичного створення методу та збереження стану програми, коли генератори завершуються, вони автоматично викликають StopIteration. У поєднанні ці функції дозволяють легко створювати ітератори без зусиль, ніж написання звичайної функції.

9.10. Генератор виразів

Деякі прості генератори можна коротко закодувати як вирази за допомогою синтаксису, схожого на розуміння списків, але з круглими дужками замість квадратних. Ці вирази розроблені для ситуацій, коли генератор використовується одразу функцією охоплення. Вирази генератора є більш компактними, але менш універсальними, ніж повні визначення генераторів, і, як правило, більш зручні для пам’яті, ніж еквівалентні списки.

Приклади:

>>> sum(i*i for i in range(10))                 # sum of squares
285

>>> xvec = [10, 20, 30]
>>> yvec = [7, 5, 3]
>>> sum(x*y for x,y in zip(xvec, yvec))         # dot product
260

>>> unique_words = set(word for line in page  for word in line.split())

>>> valedictorian = max((student.gpa, student.name) for student in graduates)

>>> data = 'golf'
>>> list(data[i] for i in range(len(data)-1, -1, -1))
['f', 'l', 'o', 'g']

Виноски