Основы системы объектов Python

PythonPythonBeginner
Практиковаться сейчас

This tutorial is from open-source community. Access the source code

💡 Этот учебник переведен с английского с помощью ИИ. Чтобы просмотреть оригинал, вы можете перейти на английский оригинал

Введение

Система объектов Python в значительной мере основана на реализации, включающей словари. В этом разделе обсуждается это.

Словарь, снова

Помните, что словарь - это коллекция именованных значений.

stock = {
    'name' : 'GOOG',
   'shares' : 100,
    'price' : 490.1
}

Словари обычно используются для простых структур данных. Однако, они используются для важнейших частей интерпретатора и могут быть самым важным типом данных в Python.

Словарь и модуль

Внутри модуля словарь хранит все глобальные переменные и функции.

## foo.py

x = 42
def bar():
 ...

def spam():
 ...

Если вы просмотрите foo.__dict__ или globals(), то увидите словарь.

{
    'x' : 42,
    'bar' : <function bar>,
   'spam' : <function spam>
}

Словарь и объекты

Пользовательские определяемые объекты также используют словари для данных экземпляра и классов. Фактически, вся система объектов представляет собой в основном дополнительный слой, который накладывается поверх словарей.

В словаре хранятся данные экземпляра, __dict__.

>>> from stock import Stock
>>> s = Stock('GOOG', 100, 490.1)
>>> s.__dict__
{'name' : 'GOOG','shares' : 100, 'price': 490.1 }

Вы заполняете этот словарь (и экземпляр), когда присваиваете значения self.

class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

Данные экземпляра, self.__dict__, выглядят так:

{
    'name': 'GOOG',
   'shares': 100,
    'price': 490.1
}

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

s = Stock('GOOG', 100, 490.1)     ## {'name' : 'GOOG','shares' : 100, 'price': 490.1 }
t = Stock('AAPL', 50, 123.45)     ## {'name' : 'AAPL','shares' : 50, 'price': 123.45 }

Если вы создадите 100 экземпляров какого-то класса, будут созданы 100 словарей, хранящих данные.

Члены класса

В отдельном словаре хранятся также методы.

class Stock:
    def __init__(self, name, shares, price):
        self.name = name
        self.shares = shares
        self.price = price

    def cost(self):
        return self.shares * self.price

    def sell(self, nshares):
        self.shares -= nshares

Словарь находится в Stock.__dict__.

{
    'cost': <function>,
   'sell': <function>,
    '__init__': <function>
}

Экземпляры и классы

Экземпляры и классы связаны между собой. Атрибут __class__ ссылается обратно на класс.

>>> s = Stock('GOOG', 100, 490.1)
>>> s.__dict__
{ 'name': 'GOOG','shares': 100, 'price': 490.1 }
>>> s.__class__
<class '__main__.Stock'>
>>>

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

Доступ к атрибутам

Когда вы работаете с объектами, вы получаете доступ к данным и методам с использованием оператора ..

x = obj.name          ## Получение
obj.name = value      ## Установка
del obj.name          ## Удаление

Эти операции напрямую связаны с словарями, лежащими под капотом.

Изменение экземпляров

Операции, которые изменяют объект, обновляют соответствующий словарь.

>>> s = Stock('GOOG', 100, 490.1)
>>> s.__dict__
{ 'name':'GOOG','shares': 100, 'price': 490.1 }
>>> s.shares = 50       ## Установка
>>> s.date = '6/7/2007' ## Установка
>>> s.__dict__
{ 'name': 'GOOG','shares': 50, 'price': 490.1, 'date': '6/7/2007' }
>>> del s.shares        ## Удаление
>>> s.__dict__
{ 'name': 'GOOG', 'price': 490.1, 'date': '6/7/2007' }
>>>

Чтение атрибутов

Предположим, что вы читаете атрибут у экземпляра.

x = obj.name

Атрибут может существовать в двух местах:

  • В локальном словаре экземпляра.
  • В словаре класса.

Оба словаря должны быть проверены. Во - первых, проверьте в локальном __dict__. Если не найдено, посмотрите в __dict__ класса через __class__.

>>> s = Stock(...)
>>> s.name
'GOOG'
>>> s.cost()
49010.0
>>>

Эта схема поиска объясняет, как члены класса общаются между всеми экземплярами.

Как работает наследование

Классы могут наследоваться от других классов.

class A(B, C):
 ...

Базовые классы хранятся в кортеже в каждом классе.

>>> A.__bases__
(<class '__main__.B'>, <class '__main__.C'>)
>>>

Это обеспечивает ссылку на родительские классы.

Чтение атрибутов с использованием наследования

Логически процесс поиска атрибута следующий. Во - первых, проверьте в локальном __dict__. Если не найдено, посмотрите в __dict__ класса. Если не найдено в классе, посмотрите в базовых классах через __bases__. Однако, есть некоторые тонкости этого, о которых будет рассказано далее.

Чтение атрибутов с использованием одиночного наследования

В иерархиях наследования атрибуты ищутся путём последовательного обхода дерева наследования вверх.

class A: pass
class B(A): pass
class C(A): pass
class D(B): pass
class E(D): pass

При одиночном наследовании существует единственный путь к вершине. Вы останавливаетесь при первом совпадении.

Порядок разрешения методов или MRO

Python предварительно вычисляет цепочку наследования и хранит её в атрибуте MRO класса. Вы можете просмотреть его.

>>> E.__mro__
(<class '__main__.E'>, <class '__main__.D'>,
 <class '__main__.B'>, <class '__main__.A'>,
 <type 'object'>)
>>>

Эта цепочка называется порядком разрешения методов (Method Resolution Order). Чтобы найти атрибут, Python последовательно обходит MRO. Первый найденный атрибут выбирается.

MRO в множественном наследовании

При множественном наследовании нет единственного пути к вершине. Посмотрим на пример.

class A: pass
class B: pass
class C(A, B): pass
class D(B): pass
class E(C, D): pass

Что происходит, когда вы обращаетесь к атрибуту?

e = E()
e.attr

Производится процесс поиска атрибута, но какой порядок? Это проблема.

Python использует совместное множественное наследование, которое подчиняется некоторым правилам порядка классов.

  • Дети всегда проверяются перед родителями
  • Родители (если несколько) всегда проверяются в указанном порядке.

MRO вычисляется путём сортировки всех классов в иерархии согласно этим правилам.

>>> E.__mro__
(
  <class 'E'>,
  <class 'C'>,
  <class 'A'>,
  <class 'D'>,
  <class 'B'>,
  <class 'object'>)
>>>

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

Странный способ повторного использования кода (с использованием множественного наследования)

Рассмотрим два совершенно не связанных объекта:

class Dog:
    def noise(self):
        return 'Bark'

    def chase(self):
        return 'Chasing!'

class LoudDog(Dog):
    def noise(self):
        ## Общий код с LoudBike (ниже)
        return super().noise().upper()

И

class Bike:
    def noise(self):
        return 'On Your Left'

    def pedal(self):
        return 'Pedaling!'

class LoudBike(Bike):
    def noise(self):
        ## Общий код с LoudDog (выше)
        return super().noise().upper()

В реализации LoudDog.noise() и LoudBike.noise() есть общий код. На самом деле, код идентичен. Естественно, такой код неизбежно привлекает внимание программистов.

Паттерн "Mixin"

Паттерн Mixin представляет собой класс с фрагментом кода.

class Loud:
    def noise(self):
        return super().noise().upper()

Этот класс нельзя использовать в отдельности. Он "мешается" с другими классами путём наследования.

class LoudDog(Loud, Dog):
    pass

class LoudBike(Loud, Bike):
    pass

Загадочно, громкость теперь реализована всего один раз и повторно используется в двух совершенно не связанных классах. Этот трюк является одним из основных применений множественного наследования в Python.

Почему использовать super()

Всегда используйте super() при переопределении методов.

class Loud:
    def noise(self):
        return super().noise().upper()

super() делегирует вызов методу следующего класса в MRO.

Трудность заключается в том, что вы не знаете, какой это класс. Особенно это актуально, если используется множественное наследование.

Некоторые предостережения

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

В разделе 4 вы определили класс Stock, представляющий акционерноеholding. В этом упражнении мы будем использовать этот класс. Перезапустите интерпретатор и создайте несколько экземпляров:

>>> ================================ RESTART ================================
>>> from stock import Stock
>>> goog = Stock('GOOG',100,490.10)
>>> ibm  = Stock('IBM',50, 91.23)
>>>

Упражнение 5.1: Представление экземпляров

В интерактивной оболочке исследуйте внутренние словари двух экземпляров, которые вы создали:

>>> goog.__dict__
... посмотрите на вывод...
>>> ibm.__dict__
... посмотрите на вывод...
>>>

Упражнение 5.2: Изменение данных экземпляра

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

>>> goog.date = '6/11/2007'
>>> goog.__dict__
... посмотрите на вывод...
>>> ibm.__dict__
... посмотрите на вывод...
>>>

В выводе выше вы заметите, что у экземпляра goog есть атрибут date, в то время как у экземпляра ibm его нет. Важно отметить, что Python на самом деле не накладывает никаких ограничений на атрибуты. Например, атрибуты экземпляра не ограничиваются только теми, которые настраиваются в методе __init__().

Вместо установки атрибута попробуйте напрямую поместить новое значение в объект __dict__:

>>> goog.__dict__['time'] = '9:45am'
>>> goog.time
'9:45am'
>>>

Здесь вы действительно замечаете тот факт, что экземпляр - это просто слой поверх словаря. Примечание: следует подчеркнуть, что прямой манипуляция словарем - это необычное действие - вы должны всегда писать свой код, используя синтаксис (.).

Упражнение 5.3: Роль классов

Определения, составляющие определение класса, общие для всех экземпляров этого класса. Обратите внимание, что все экземпляры имеют ссылку на их связанный класс:

>>> goog.__class__
... посмотрите на вывод...
>>> ibm.__class__
... посмотрите на вывод...
>>>

Попробуйте вызвать метод для экземпляров:

>>> goog.cost()
49010.0
>>> ibm.cost()
4561.5
>>>

Заметим, что имя 'cost' не определено ни в goog.__dict__, ни в ibm.__dict__. Вместо этого оно предоставляется словарем класса. Попробуйте это:

>>> Stock.__dict__['cost']
... посмотрите на вывод...
>>>

Попробуйте вызвать метод cost() напрямую через словарь:

>>> Stock.__dict__['cost'](goog)
49010.0
>>> Stock.__dict__['cost'](ibm)
4561.5
>>>

Заметим, как вы вызываете функцию, определенную в определении класса, и как аргумент self получает экземпляр.

Попробуйте добавить новый атрибут в класс Stock:

>>> Stock.foo = 42
>>>

Заметим, как этот новый атрибут теперь появляется у всех экземпляров:

>>> goog.foo
42
>>> ibm.foo
42
>>>

Однако, заметим, что он не является частью словаря экземпляра:

>>> goog.__dict__
... посмотрите на вывод и заметьте, что нет атрибута 'foo'...
>>>

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

Примечание: Эта часть упражнения иллюстрирует то, что известно как классная переменная. Предположим, например, у вас есть класс такого вида:

class Foo(object):
     a = 13                  ## Классная переменная
     def __init__(self,b):
         self.b = b          ## Экземплярная переменная

В этом классе переменная a, присвоенная в теле класса самой, является "классной переменной". Она общая для всех созданных экземпляров. Например:

>>> f = Foo(10)
>>> g = Foo(20)
>>> f.a          ## Проверьте классную переменную (одинаковая для обоих экземпляров)
13
>>> g.a
13
>>> f.b          ## Проверьте экземплярную переменную (отличается)
10
>>> g.b
20
>>> Foo.a = 42   ## Измените значение классной переменной
>>> f.a
42
>>> g.a
42
>>>

Упражнение 5.4: Связанные методы

Некоторый тонкий аспект Python заключается в том, что вызов метода на самом деле включает два шага и что-то, что называется связанным методом. Например:

>>> s = goog.sell
>>> s
<bound method Stock.sell of Stock('GOOG', 100, 490.1)>
>>> s(25)
>>> goog.shares
75
>>>

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

>>> s.__func__
<function sell at 0x10049af50>
>>>

Это то же значение, что и в словаре Stock.

>>> Stock.__dict__['sell']
<function sell at 0x10049af50>
>>>

Связанные методы также запоминают экземпляр, который является аргументом self.

>>> s.__self__
Stock('GOOG',75,490.1)
>>>

Когда вы вызываете функцию с использованием (), все элементы объединяются. Например, вызов s(25) на самом деле делает это:

>>> s.__func__(s.__self__, 25)    ## То же, что и s(25)
>>> goog.shares
50
>>>

Упражнение 5.5: Наследование

Создайте новый класс, который наследуется от Stock.

>>> class NewStock(Stock):
        def yow(self):
            print('Yow!')

>>> n = NewStock('ACME', 50, 123.45)
>>> n.cost()
6172.50
>>> n.yow()
Yow!
>>>

Наследование реализуется путём расширения процесса поиска атрибутов. Атрибут __bases__ содержит кортеж непосредственных родителей:

>>> NewStock.__bases__
(<class'stock.Stock'>,)
>>>

Атрибут __mro__ содержит кортеж всех родителей в порядке, в котором они будут искаться при поиске атрибутов.

>>> NewStock.__mro__
(<class '__main__.NewStock'>, <class'stock.Stock'>, <class 'object'>)
>>>

Вот, как метод cost() экземпляра n выше будет найден:

>>> for cls in n.__class__.__mro__:
        if 'cost' in cls.__dict__:
            break

>>> cls
<class '__main__.Stock'>
>>> cls.__dict__['cost']
<function cost at 0x101aed598>
>>>

Резюме

Поздравляем! Вы завершили лабораторную работу "Повторение словарей". Вы можете выполнить больше лабораторных работ в LabEx, чтобы улучшить свои навыки.