InfoCity
InfoCity - виртуальный город компьютерной документации
Реклама на сайте







Размещение сквозной ссылки

 

Глава 9. Классы


Механизм классов Python добавляет их к языку используя минимум нового синтаксиса и семантики. Это смесь механизмов классов, которые Вы найдете в С++ и Modula-3. Также как и модули, классы не ставят абсолютного барьера между определением и пользователем. Однако, гораздо больше полагаются на вежливость пользователя, которому не следует "вламываться в определение". Самые важные особенности классов полностью сохранены, однако: механизм наследования позволяет множественное наследование от нескольких предков, класс-потомок может переопределить любой метод родителя (родителей), каждый метод может вызвать родительский метод с таким же именем. Объекты могут содержать произвольное количество собственных данных.

Говоря в терминологии С++, все члены класса (включая поля данных) являются публичными (public), и все члены-функции - виртуальны (virtual). Нет никаких специальных конструкторов (constructor) или деструкторов (destructor). Как и в Modula-3, нет сокращения, позволяющего сослаться на член объекта из его метода: функция-метод декларируется с явным первым аргументом, представляющим сам объект, и подставляемым автоматически при вызове.

Подобно Smalltalk, классы сами являются объектами, хотя и более широком смысле этого слова: в Python все типы данных являются объектами. Это предоставляет семантику для импортирования и переименования. Но, только подобно C++ или Modula-3, встроенные типы данных нельзя использовать как базовый класс для расширения пользователем. Кроме этого, так же как в С++, но в отличие от Modula-3, большинство встроенных операций со специальным синтаксисом (арифметические операции и др.) можно переопределить для членов класса.


9.1 Несколько слов о терминологии


По причине отсутствия универсальной терминологии для разговора о классах, я изредка буду пользоваться терминами Smalltalk и С++. (Я бы предпочел термины Modula-3, поскольку его объектно-ориентированная семантика ближе Python чем С++, однако, я ожидаю, что немногие из читателей слышали о них...)

Мне также следует предупредить, что существует терминологическая ловушка для читателей, знакомых с объектно-ориентированным программированием: слово "объект" в Python не обязательно означает экземпляр класса. Подобно С++ и Modula-3, и в отличие от Smalltalk, не все типы Python являются классами, например: базовые встроенные типы данных, такие как целые или списки, и даже некоторые более экзотические, вроде файлового типа, тоже не являются классами. Однако, все типы Python участвуют некоторой части общей семантики, которую лучше всего описывать используя слово "объект".

Объекты существуют индивидуально, но одному и тому же объекту могут быть назначены несколько имен. В других языках это известно как множественное именование (aliasing). Данное свойство обычно не понимают при первом взгляде на Python, и, действительно, оно может быть проигнорировано в том случае, когда Вы имеете дело с немутируемыми базовыми типами (числа, строки, тьюплы). Однако, множественное именование имеет особый (преднамеренный !) эффект в семантике кода Python, включающей мутируемые объекты, такие как списки, словари, и большинство типов представляющих сущность вывода программы (файлы, окна и т. д.). Программы часто используют это в свою пользу, поскольку, множественное именование, в некоторых отношениях, ведет себя подобно указателям. Например, передача объекта в качестве аргумента не ведет к ухудшению эффективности, потому, что на самом деле передается лишь указатель на объект. И, если функция модифицирует объект, переданный как аргумент, то, после возврата, вызывавший увидит все изменения объекта; это устраняет нужду в двух различных способах передачи аргументов, как в Pascal 18 .


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


Перед введением классов, мне сперва следует рассказать кое-что о правилах областей имен Python. Определения классов проделывают несколько аккуратных фокусов с пространствами имен, и Вам нужно знать как работают области и пространства имен, для того, чтобы полностью понимать что происходит. Кстати, знание этого предмета будет полезно для каждого продвинутого Python программиста.

Давайте начнем с нескольких определений.

Пространство имен - это соответствие между именами и объектами. Большинство пространств имен реализованы как словари Python, но это, в любом случае, обычно не заметно, и может быть изменено в будущем. Вот несколько примеров пространств имен: множество встроенных имен (функции, подобно abs() , и имена встроенных исключений); глобальные имена в модуле; локальные имена при вызове функции. Самое важное, что следует знать о пространствах имен, - то, что нет абсолютно никакой связи между именами в разных пространствах имен; например, два различных модуля могут оба определить функцию "maximize" без всякого конфликта - пользователи модуля должны префиксировать ее имя именем модуля.

Кстати, я использую слово атрибут для каждого имени, записанного после точки - например, в выражении z.real , real является атрибутом объекта z . Строго говоря, ссылки на имена в модулях - это ссылки на атрибуты: в выражении modname.funcname , modname - модульный объект и funcname - его атрибут. В этом случае существует прямое соответствие между атрибутами модуля и глобальными именами, определенными в модуле: они делят одно и то же пространство имен ! 19

Атрибуты могут быть как "только для чтения", так и для записи. В последнем случае, атрибуту можно что-нибудь присвоить. Так вот, атрибутам модуля можно присваивать: Вы можете написать modname.the_answer=42 . Есть возможность удаления изменяемых атрибутов, используя оператор del : del modname.the_answer .

Пространства имен создаются в различные моменты времени и имеют различную продолжительность жизни. Пространство имен, содержащее встроенные имена, создается при запуске интерпретатора Python, и никогда не удаляется. Глобальное пространство имен для модуля создается когда считываются определения модуля; обычно, модульное пространство имен существует до выхода из интерпретатора. Операторы, исполняемые интерпретатором на верхнем уровне и считываемые либо из файла скрипта, либо интерактивно, считаются частью модуля под именем __main__ , следовательно, они имеют свое собственное пространство имен. (Встроенные имена, в действительности, тоже содержатся в модуле; он называется __builtin__ .)

Локальное пространство имен для функции создается при ее вызове, и удаляется при возврате из нее или возникновении исключения, необработанного внутри функции. Разумеется, каждый рекурсивный вызов имеет свое собственное локальное пространство имен.

Область - это текстовый регион программы, где пространство имен прямо доступно. Здесь "прямая доступность" означает, что ссылки, без дополнительной классификации, на имена пытаются найти имя в этом пространстве имен.

Хотя области определяются статически, используются они динамически. В любой момент времени выполнения используются ровно три области (т. е. Прямо доступны ровно три пространства имен): внутренняя область, где ищется в первую очередь, включающая локальные имена, средняя область, там поиск производится во вторую очередь, она включает текущие глобальные имена модуля, и внешняя область (в ней ищется в последнюю очередь) - это пространство имен, содержащая встроенные имена.

Обычно, локальная область ссылается на локальные имена текущей (по тексту) функции. Вне функций, локальная область ссылается на то же пространство имен, что и глобальная область: модульное пространство имен. Определения класса помещают еще одно пространство имен в локальную область.

Важно осознавать, что области определяются по тексту: глобальная область функции, определенной в модуле - пространство имен этого модуля, не зависимо от того, откуда или под каким именем функция была вызвана. С другой стороны, поиск имен происходит динамически, во время выполнения, но язык развивается в сторону статических имен, определяемых во время "компиляции", поэтому, не полагайтесь на решение в пользу динамических имен ! (Фактически, локальные переменные уже устанавливаются статически.)

У Python есть особая причуда: присваивания всегда происходят во внутренней области. Присваивание не копирует данные - оно только привязывает имена к объектам. То же самое верно и для удаления: команда del x убирает привязку x от пространства имен, на которую ссылается локальная область. В действительности, все операции, которые вводят новые имена, используют локальную область: в частности, операторы импорта и определения функции привязывают имя модуля или функции к локальной области. (Можно воспользоваться оператором global чтобы указать на то, что некоторые переменные находятся в глобальной области.)


9.3 Первый взгляд на классы


Классы вводят немного нового синтаксиса, три новых объектовых типа, и некоторое количество новой семантики.


9.3.1 Синтаксис определения класса


Простейшая форма определения класса выглядит следующим образом:

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

Определение класса, подобно определению функции (оператор def ), должно быть "выполнено" 20 для того, чтобы оно возымело необходимый эффект. (Ведь Вы могли поместить определение класса в ветку if , или внутрь некоторой функции.)

На практике, операторы внутри определения класса обычно будут определениями функций, однако, разрешены и другие операторы, и они иногда полезны - мы вернемся к этому позже. Определение функции внутри класса обычно имеет своеобразную форму списка аргументов, диктуемую соглашениями по вызову методов - опять-таки, это объясняется ниже.

Когда введено определение класса, создается новое пространство имен, и используется как локальная область - так, все присваивания локальным переменным происходят в этом новом пространстве имен. В частности, определение функции привязывает ее имя туда же.

После того, как определение класса нормально пройдено (до конца), создается классовый объект. Это, по существу, оболочка вокруг содержимого пространства имен, созданного определением класса; мы подробнее изучим классовые объекты в следующей секции. Текущая локальная область (бывшая активной перед тем, как мы ввели определение класса) восстанавливается в предыдущем положении, и классовый объект помещается туда, с именем, заданным в определении класса (ClassName в приведенном примере).


9.3.2 Классовые объекты


Классовые объекты поддерживают две операции: ссылку на атрибут и создание нового экземпляра класса.

Ссылка на атрибут использует стандартный синтаксис, применяемый для всех ссылок на атрибуты в Python: obj.name . Действительными именами атрибутов являются все имена, помещенные в пространство имен класса, когда создавался классовый объект. Итак, если определение класса выглядит подобно следующему:

    class MyClass:
            i=12345
            def f(x):
                    return ‘hello world’

то MyClass.i и MyClass.f - действительные ссылки на атрибуты, возвращающие целое и объект-функцию, соответственно. Атрибуты класса можно также изменять, т. е. Вы можете поменять значение MyClass.i путем присваивания.

Создание нового экземпляра использует нотацию функций. Просто притворитесь, что классовый объект - это функция без параметров, которая возвращает новый экземпляр класса. Например, (учитывая класс, описанный выше):

    x=MyClass()

создаст новый экземпляр класса, и присвоит этот объект локальной переменной x .


9.3.3 Объекты-экземпляры


Теперь, что мы можем делать с объектами-экземплярами ? Единственная операция, которую они понимают - это ссылка на атрибут. Существует две разновидности корректных имен атрибутов.

Первую я буду называть атрибутом данных. Они соответствуют "экземплярам-переменным" в Smalltalk, и "членам-данным" в С++. Атрибуты данных не нуждаются в декларировании; подобно локальным переменным, они начинают существовать после того, как им первый раз будет что-нибудь присвоено. Например, если x -это экземпляр MyClass , который создан выше, то следующий фрагмент кода выведет значение 16:

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

Второй разновидностью ссылок, понимаемой объектами-экземплярами, является метод. Метод - это функция, которая "принадлежит" объекту. (В Python термин "метод" относится не только к экземплярам классов: и другие объектовые типы также могут иметь методы, например, списковые объекты имеют методы append, insert, remove, sort, и так далее. Однако, ниже мы будем под термином "метод" подразумевать только методы объектов-экземпляров класса, при отсутствии явного указания на противное.)

Имена методов объекта-экземпляра зависят от его класса. По определению, все атрибуты класса, которые являются объектами-функциями, определяют соответствующие методы экземпляров этого класса. Так, в нашем примере, x.f является действительной ссылкой на метод, поскольку, MyClass.f - функция, но x.i - не метод, потому, что MyClass.i так же не является методом. Но, x.f - совсем не то же самое, что MyClass ; это объект-метод, а не объект-функция.


9.3.4 Объекты-методы


Обычно, метод вызывается непосредственно:

    x.f()

В нашем примере это вернет строку ‘hello world’ . Однако, метод необязательно вызывать непосредственно: x.f является объектом методом, ему можно дать еще одно имя и вызвать позднее. Например:

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

будет продолжать выводить hello world до скончания времен.

Что на самом деле происходит когда вызывается метод ? Вы могли заметить, что x.f() было вызвано без аргумента, хотя определение функции f и специфицировало один аргумент. Что случилось с аргументом ? Конечно же, Python инициировал бы исключение в случае, когда функция, требующая аргумент, вызвалась без него - даже если аргумент не используется в теле функции...

Действительно, Вы могли отгадать ответ: особым свойством методов является то, что объект 21 передается в качестве первого аргумента функции. В нашем примере, вызов x.f() точно соответствует MyClass(x) . В общем, вызов метода со списком из n аргументов эквивалентен вызову соответствующей функции со списком аргументов, полученным путем добавления объекта, вызвавшего метод, перед первым аргументом.

Если Вы все еще не понимаете, как работают методы, возможно, взгляд на реализацию прояснит дело. Когда Вы ссылаетесь на атрибут экземпляра, который не является атрибутом данных, тогда ищется его класс. Если имя обозначает атрибут класса, который является объектом-функцией, то создается объект-метод путем упаковки вместе объекта-экземпляра и только что найденного объекта-функции - вот мы и получили объект-метод. При вызове объекта-метода со списком аргументов, он обратно распаковывается, создается новый список аргументов, путем соединения объекта-экземпляра с исходным списком аргументов, и вызывается объект-функция с новым списком аргументов.


9.4 Случайные замечания


Атрибуты данных переопределяют атрибуты-методы с тем же именем; для избежания случайных конфликтов имен, которые могут послужить причиной трудноуловимых ошибок в больших программах, будет разумно придерживаться некоторых договоренностей, которые минимизируют вероятность конфликтов, например, записывать имена методов с прописной буквы, префиксировать имена атрибутов данных короткими уникальными последовательностями символов (возможно, просто символом подчеркивания), или использовать глаголы для методов и существительные для атрибутов данных.

Методы могут ссылаться на атрибуты данных точно так же, как обычные пользователи ("клиенты") объекта. Другими словами, классы не годятся для реализации чисто абстрактных типов данных. Фактически, ничто в Python не может заставить данные стать невидимыми - тут все зависит только от соглашений 22 . (С другой стороны, реализация Python, написанная на С, может полностью спрятать детали реализации и управлять доступом к объектам, если это необходимо; подобное может быть использовано расширениями Python, написанными на С.)

Клиентам следует использовать атрибуты данных с осторожностью, иначе они могут запортить те из них, которые обслуживаются методами, оставив след на их атрибутах данных. Клиенты могут добавлять свои собственные атрибуты данных к объекту-экземпляру класса, без всякого влияния на работоспособность методов, если только не будут допущены конфликты имен; опять-таки, соблюдение соглашений об именах, спасет Вас от лишней головной боли.

Не существует сокращенного способа ссылки на атрибуты данных (или на другие методы) изнутри метода. Я нахожу, что это увеличивает читабельность методов: исключаются всякая возможность путаницы локальных переменных и переменных объекта-экземпляра, при просмотре метода.

По договоренности, первый аргумент часто называется self. Это всего лишь соглашение: имя self абсолютно не имеет никакого специального значения для Python. (Тем не менее, если Вы не будете следовать соглашению, Ваш код может стать менее читабельным для других Python программистов, и, вполне допустимо, что будет написана программа браузер классов, которая будет полагаться на это соглашение.)

Любой объект-функция, являющийся атрибутом класса, определяет метод для экземпляров этого класса. Совсем необязательно, чтобы определение функции, по ходу текста, было внутри определения класса: в порядке вещей присваивание объекта-функции локальной переменной класса. Например:

# Определение функции вне класса
def f1(self,x,y):
         return min(x,x+y)

class C:
         f=f1
         def g(self):
                  return ‘Привет, мир’
         h=g

Теперь f , g и h - атрибуты класса C , которые ссылаются на объекты-функции, и, следовательно, все они являются методами экземпляров C . Причем, h полностью эквивалентно g . Однако, такая практика обычно только запутывает читателя программы.

Методы могут вызывать другие методы, путем использования атрибутов-методов аргумента self :

    class Bag:
        def empty(self):
                self.data=[]
        def add(self,x):
                self.data.append(x)
        def addtwice(self,x):
                self.add(x)
                self.add(x)

Операция создания нового экземпляра ("вызов" классового объекта) порождает пустой объект. Многие классы любят создавать объекты с известным, проинициализированным состоянием. Поэтому, класс может определить специальный метод под названием __init__ , как здесь:

        def __init__(self):
                self.empty()

Если класс определяет метод __init__ , то при создании нового экземпляра класса автоматически будет вызван __init__ для только что созданного экземпляра. Итак, в примере с Bag , новый и инициализированный экземпляр может быть получен так:

    x=Bag()

Конечно, метод __init__ может иметь аргументы, для большей гибкости. В этом случае, аргументы, полученные оператором создания экземпляра класса, будут переданы методу __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)
>>>

Методам разрешается ссылаться на глобальные имена таким же способом, как и обычные функции. Глобальная область, ассоциированная с методом, - это модуль, где содержится определение класса. (Сам класс никогда не используется как глобальная область!) В то время, как нечасто встречаются веские причины для использования глобальных данных в методе, существует много законных использований глобальной области: функции и модули, импортированные в глобальную область, можно использовать в методах точно так же, как функции и классы, определенные в нем. Обычно, класс, содержащий метод, сам определен в этой глобальной области, и в следующей секции мы обнаружим несколько хороших причин того, почему метод может захотеть сослаться на свой собственный класс!


9.5 Наследование


Безусловно, никакую особенность языка не стоило бы называть именем "класс", без поддержки ею наследования. Синтаксис для определения наследующего класса выглядит следующим образом:

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

Имя BaseClassName должно быть определено в области, включающей определение наследующего класса. Вместо имени базового класса разрешается использовать выражение. Что бывает полезно, когда базовый класс находится в другом модуле:

    class DerivedClassName (modname.BaseClassName):

Выполнение определения наследующего класса протекает точно также, как для базового класса. Когда конструируется классовый объект, запоминается базовый класс. Затем это используется при ссылке на атрибут: если в классе не найден запрошенный атрибут, то он ищется в базовом классе. Данное правило применяется рекурсивно, в случае, когда базовый класс, в свою очередь, наследует от какого-нибудь класса.

В создании нового экземпляра наследующего класса нет ничего особенного: DerivedClassName() порождает новый экземпляр. Ссылки на методы разрешаются следующим образом: ищется соответствующий атрибут класса, проходя вниз цепочку базовых классов, в случае необходимости. Если при этом будет найден объект-функция, то ссылка на метод корректна.

Наследующие классы могут переопределять методы своих базовых классов. Методы не имеют каких-либо особых привилегий при вызове других методов того же самого объекта. Метод базового класса, который вызывает другой метод, определенный в том же базовом классе, фактически, может вызвать переопределяющий метод наследующего класса. (Для программистов С++: все методы Python являются "виртуальными функциями" 24 .)

Переопределяющий метод в наследующем классе может в действительности захотеть расширить, вместо того, чтобы просто заменить, метод с таким же именем в базовом классе. Есть простой способ прямого вызова метода базового класса: BaseClassName.methodname(self,arguments) 25 . Это изредка полезно также и для клиентов. Не забудьте, что это будет работать только в случае, когда базовый класс определен прямо в глобальной области, либо, импортирован туда.


9.5.1 Множественное наследование


Python поддерживает ограниченную форму множественного наследования. Определение класса с несколькими базовыми классами выглядит следующим образом:

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

Единственное правило, которое необходимо объяснить, касается порядка просмотра классов при поиске атрибута. Порядок следующий: сперва в глубину, затем слева направо. Так, если некий атрибут не найден в DerivedClassName , то он ищется в Base1 , затем (рекурсивно) в базовых для Base1 классах, и только если он там не найден, то поиск продолжается в Base2 , и так далее.

(Для некоторых людей более естественным кажется поиск сперва в Base2 и Base3 , перед просмотром базовых для Base1 классов. Однако, в таком случае, от Вас потребовалось бы знание того, определен ли некий атрибут Base1 , на самом деле, в самом Base1 , или в каком-нибудь из его базовых классов, прежде чем Вы сможете выяснить последствия конфликта имен с атрибутом класса Base2 . Правило приоритетного просмотра в глубину ликвидирует разницу между родными и унаследованными атрибутами Base1 .)

Очевидно, что беспорядочное использование множественного наследования, для Вас станет сплошным кошмаром, вырабатывающим зависимость от конвенции Python, для избежания случайных конфликтов имен. Хорошо известной проблемой множественного наследования является случай, когда класс наследует от двух предков, имеющих общий базовый класс. Несмотря на то, что достаточно легко понять, что происходит в этом случае (у экземпляра будет одинарная копия "переменной экземпляра", т. е. атрибута данных, используемого общим базовым классом), не совсем ясно, что это семантика в любом случае полезна.


9.6 Добавления и окончания


Иногда бывает полезно иметь тип данных наподобие "record" Pascal или "struct" C, связывающий вместе пару именованных элементов данных. То же самое можно проделать с помощью определения класса:

    class Employee:
             pass

    john=Employee() # Создание пустой записи employee

    # Заполнение полей записи
    john.name='John Doe'
    john.dept=’Computer lab’
    john.salary=1000

Часто, коду Python, ожидающему особый абстрактный тип данных, можно вместо него передать класс, эмулирующий методы этого типа данных. Например, если у Вас есть функция, форматирующая некоторые данные от файлового объекта, то Вы можете определить класс с методами read() и readln() , которые, вместо файлового объекта, получат строки из буфера, и передать этот класс в качестве аргумента. (К сожалению, данная техника имеет ограничения: класс не может определить операции, имеющие специальный синтаксис, такие, как выделение части последовательности или арифметические операции. Кроме этого, присваивание таких "псевдо-файлов" переменной sys.stdin не заставит интерпретатор читать последующий ввод из него.)

Объекты-методы экземпляров тоже имеют атрибуты: m.im_self() - объект, которому принадлежит метод, и m.im_func - объект-функция, соответствующая методу.

[Назад][Содержание][Вперед]


Реклама на InfoCity

Яндекс цитирования



Финансы: форекс для тебя








1999-2009 © InfoCity.kiev.ua