Найти:

- Взлом
- Приватность
- Трюки
- Кодинг
- Админ
- Geek
- Обучение
- Магазин

- Windows
- Linux
- Android
- Железо
- Мессенджеры
- Утечки
- Коронавирус
- Все выпуски «Хакера»

### Содержание статьи

- Иерархия классов и `object`
- Инкапсуляция и «частные» атрибуты
- Проверка соответствия типов
- О регистре букв и слове self
- Статические методы и методы класса
- Изменяемость полей класса
- Множественное наследование
- Заключение

В этом году анонсирован последний выпуск Python 2.7, после чего Python Software Foundation перестанет поддерживать ветку 2.7. Множество популярных библиотек и фреймворков тоже прекращают официальную поддержку Python 2, а целый ряд дистрибутивов Linux уже не включают его в набор пакетов по умолчанию.

Конечно, Python 2 не исчез из реальности, как только питоночасы пробили 00:00, но он уже стал темным прошлым, а не суровым настоящим. Начинающие могут смело знакомиться с Python 3 без оглядки на 2.7, что сильно упрощает жизнь.

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

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

## Иерархия классов и `object`

Начнем с самого простого случая — с классов, у которых нет явно указанного предка.

В Python 3 у любого пользовательского класса есть как минимум один базовый класс. В корне иерархии классов находится встроенный класс `object` — предок всех классов.

В учебных материалах и коде часто можно видеть такую конструкцию:

    class MyClass(object):
        pass

В Python 3 она избыточна, поскольку `object` — базовый класс по умолчанию. Можно смело писать так:

    class MyClass:
        pass

Популярность явного синтаксиса в коде на Python 3 связана с существовавшей долгое время необходимостью поддерживать обе ветки.

В Python 2.7 синтаксис `MyClass(object)` был нужен, чтобы отличать «новые» классы от режима совместимости с доисторическими версиями. В Python 3 никакого режима совместимости с наследием старых версий просто не существует, поэтому наконец можно вернуться к более короткому старому синтаксису.

Что особенного в классе `object`?

1. У него самого нет базового класса.
2. У объектов этого класса не только нет атрибутов и методов — нет даже возможности их присвоить.

    >>> o = object()
    >>> o.my_attribute = None
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    AttributeError: 'object' object has no attribute 'my_attribute'

«Техническая» причина этому — отсутствие у `object` поля `__dict__`, в котором хранятся все поля и методы класса.

## Инкапсуляция и «частные» атрибуты

Попытка изменить значение поля у объекта класса `object` — это единственный случай, когда нечаянное или намеренное изменение атрибута уже созданного объекта завершится с ошибкой.

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

    >>> class MyClass:
    ...   def my_method(self):
    ...     print("I'm a method")
    ...
    >>> o = MyClass()
    >>> o.my_method = None
    >>> o.my_method()
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    TypeError: 'NoneType' object is not callable

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

Казалось бы, инкапсуляция — один из трех фундаментальных принципов ООП, наравне с наследованием и полиморфизмом. Главное правило инкапсуляции — не давать пользователю объекта вносить в него не предусмотренные автором изменения. Однако даже без доступа к исходному коду достаточно упертый пользователь найдет способ модифицировать что угодно. Дух этого правила в другом: однозначно дать понять пользователю, где заканчивается стабильный публичный интерфейс и начинаются детали реализации, которые автор может поменять в любой момент.

Для этого в Python есть один встроенный механизм. Те поля и методы, которые не входят в публичный интерфейс, называют с подчеркиванием перед именем: `_foo`, `_bar`. Никакого влияния на работу кода это не оказывает, это просто просьба не использовать такие поля бездумно.

Для создания частных (private) атрибутов применяются два подчеркивания (`__foo`, `__bar`). Такие поля будут видны изнутри объекта под своими исходными именами, но вне объекта к ним применяется name mangling — переименования в стиле `_MyClass__my_attribute`:

    class MyClass:
        x = 0
        _x = 1
        __x = 2
        def print_x(self):
            print(self.__x)

    >>> o = MyClass()
    >>> o.print_x()
    2
    >>> o.x
    0
    >>> o._x
    1
    >>> o.__x
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    AttributeError: 'MyClass' object has no attribute '__x'
    >>> o._MyClass__x
    2

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

## INFO

Переименование не применяется к полям и методам с подчеркиванием с двух сторон вроде `__init__`. По соглашению такие имена дают «магическим методам», на которых построены все внутренние интерфейсы стандартной библиотеки Python: к примеру, `o = MyClass()` — это эквивалент `o = MyClass.__new__(MyClass)`. Такие методы, очевидно, должны быть доступны извне объекта под исходными именами.

Сэкономить время на создание публичного интерфейса к частным полям можно с помощью встроенного декоратора `@property`. Он создает поле, которое выглядит как переменная только для чтения — попытка присвоить значение вызовет исключение `AttributeError`. Для примера создадим класс с логическим значением `_boolean_property`, которое можно поменять только методом `set_property`, отклоняющим значения всех типов, кроме `bool`.

    class MyClass:
        def __init__(self):
            self._boolean_property = True
        @property
        def my_property(self):
            return self._boolean_property
        def set_property(self, value):
            if type(value) == bool:
                self._boolean_property = value
            else:
                raise ValueError("Property value must be a bool")

    >>> o = MyClass()
    >>> o.my_property
    True
    >>> o.my_property = False
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
    AttributeError: cant set attribute

    >>> o.set_property(9)
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "<stdin>", line 11, in set_property
    ValueError: Property value must be a bool

    >>> o.set_property(False)
    >>> o.my_property
    False

Функцию `set_property` можно было завернуть в декоратор `@my_property.setter`, в этом случае присваивание `o.my_property = False` работало бы, а `o.my_property = 9` вызывало `ValueError`.

## Проверка соответствия типов

В предыдущем примере мы явно проверяем, что тип значения — `bool`, с помощью `type(value) == bool`. В случае с `bool` это оправданно, поскольку никакой другой тип не может его заменить в логических выражениях, а наследоваться от него Python нам не даст.

В общем случае проверять `type()` — плохая идея, поскольку не учитывает ни возможность наследования, ни возможность создать свои классы с совместимым интерфейсом.

Для проверки совместимости с неким базовым классом нужно использовать `isinstance(object, class)`. Эта проверка истинна для класса `class` и всех его потомков.

    >>> class Foo:
    ...   pass
    ...
    >>> class Bar(Foo):
    ...   pass
    ...
    >>> x = Foo()
    >>> y = Bar()
    >>> isinstance(x, Foo)
    True
    >>> isinstance(y, Foo)
    True
    >>> type(y) == Foo
    False

Такая проверка оправданна в функциях, которые работают с объектами заранее известных классов из твоих же модулей.

А вот в библиотеке для широкого круга пользователей даже `isinstance()` может быть слишком жестким ограничением. Почти все встроенные возможности Python основаны на соглашениях о «магических методах», включая арифметические операторы, итерацию, контексты (`with ... as ...`) и многое другое. Сторонний разработчик вполне может создать свой контейнер, который можно использовать наравне со встроенными `list` и `dict`, или, к примеру, запускать и завершать работу демонов через интерфейс контекста.

Поэтому, если твой код делает предположения только об интерфейсе объектов, но не о деталях их поведения, лучше всего не проверять их тип, а отлавливать возникшие при вызове метода исключения и выдавать ошибки в стиле Object is not iterable.

## О регистре букв и слове self

На телефонном собеседовании в одной крупной компании меня однажды спросили, какое ключевое слово обязательно использовать как первый аргумент метода в Python. Я сначала даже не понял смысл вопроса — такого обязательного ключевого слова не существует. Дело в том, что многие соглашения в Python так сильны, что их соблюдают, даже если это не обязательно.

В стандартной библиотеке Python и сообществе его пользователей повелось, что названия классов начинаются с заглавной буквы, а ссылку на сам объект именуют `self`. Однако ни то, ни другое не является элементом синтаксиса языка, в отличие от `this` в Java.

Эти два определения класса эквивалентны:

    class MyClass:
        def __init__(self, x):
            self.x = x

    class myclass:
        def __init__(this, x):
            this.x = x

Важно только то, что первый аргумент метода — это всегда ссылка на вызывающий его объект.

## Статические методы и методы класса

Этим метод объекта и отличается от просто функции — при вызове ему неявно передается ссылка на какой-то объект в качестве первого аргумента. На какой именно объект? По умолчанию — на экземпляр, которому принадлежит метод. С помощью декораторов `@classmethod` и `@staticmethod` это поведение можно изменить.

Декоратор `@classmethod` передает в метод ссылку на сам класс, а не на его конкретный экземпляр. Это дает возможность вызывать метод без создания объекта. А вот `@staticmethod`, по сути, и не метод вовсе, а просто функция, никакой неявной передачи ссылок таким методам не происходит.

Посмотрим на пример. Обрати внимание: `x` — поле класса, а не объектов-экземпляров.

    class MyClass:
        x = 10
        @staticmethod
        def foo():
            print(x)

        @classmethod
        def bar(self):
            print(self.x)

        def quux(self):
            print(self.x)

    >>> o = MyClass()
    >>> o.x = 30
    >>> o.foo()
    Traceback (most recent call last):
      File "<stdin>", line 1, in <module>
      File "<stdin>", line 5, in foo
    NameError: name 'x' is not defined
    >>> o.bar()
    10
    >>> o.quux()
    30

С помощью `o.x = 30` мы меняем значение поля `x` в объекте `o`. Как видишь, метод класса `bar` выводит исходное значение, потому что `self` для него — это сам класс `MyClass`. Для `quux` переменная `self` содержит ссылку именно на экземпляр `o`, поэтому метод выводит поле самого объекта.

Метод `foo` вовсе не работает, если во внешней области видимости нет переменной `x`. Если есть, то выводит ее. С помощью `@staticmethod` можно создать только такие методы, которым доступ к полям класса не нужен вообще, хотя зачем это может понадобиться в языке с модулями и пространствами имен — вопрос сложный.

## Изменяемость полей класса

В прошлом примере мы ввели поле класса `x`. В большинстве случаев вся инициализация переменных делается в методе `__init__`, вроде такого:

    class MyClass:
        def __init__(self):
            self.x = 10
            self.hello = "hello world"

Почему так? Дело в том, что классы сами относятся к объектам класса `type` и их поля так же изменяемы, как поля всех остальных объектов.

## INFO

Теперь ты знаешь, как проверить, что пользователь передал именно класс в твою абстрактную фабрику абстрактных фабрик или еще какую-то функцию для создания объектов произвольного класса. Совершенно верно, `isinstance(c, type)`.

В момент создания объекты содержат ссылки на все поля и методы своего класса. Если кто-то случайно модифицирует поле самого класса (в стиле `MyClass.x = y`), это автоматически изменит поле `x` для всех уже созданных объектов этого класса.

    class MyClass:
        x = 10

    o = MyClass()

    >>> o.x
    10
    >>> MyClass.x = 70
    >>> o.x
    70

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

    >>> o.x = 80
    >>> MyClass.x = 100
    >>> o.x
    80

Если инициализировать переменные в `__init__`, это будет происходить каждый раз, когда кто-то создает новый объект, и тогда эти переменные будут действительно локальными для конкретного объекта (instance variables).

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

Получить прямой доступ к информации о предках класса можно с помощью поля `__bases__`. В рабочем коде лучше так не делать, а обратиться к `isinstance()`, но ради интереса можно попробовать.

    >>> import collections
    >>> collections.OrderedDict.__bases__
    (<class 'dict'>,)

    >>> dict.__bases__
    (<class 'object'>,)

Ты видишь, там находится не одно значение, а кортеж (tuple), что намекает нам на возможность иметь более одного родителя. В Python на самом деле есть множественное наследование. Применять его или не применять — вопрос сложный (и в большинстве проектов решенный в пользу «не применять»). При наличии удобного синтаксиса для декораторов можно добавить новое поведение в класс или метод куда проще.

Тем не менее возможность есть и порядок поиска методов (method lookup) достаточно логичный: от первого указанного предка к последнему.

Для примера создадим два базовых класса и двух потомков, которые наследуются от них в разном порядке:

    class Foo:
        def hello(self):
            print("hello world")

    class Bar:
        def hello(self):
            print("hi world")

    class Baz(Foo, Bar):
        pass

    class Quux(Bar, Foo):
        pass

    x = Baz()
    y = Quux()

    >>> x.hello()
    hello world
    >>> y.hello()
    hi world

    >> Baz.__bases__
    (<class '__main__.Foo'>, <class '__main__.Bar'>)
    >>> Quux.__bases__
    (<class '__main__.Bar'>, <class '__main__.Foo'>)

То же самое относится к вызовам методов предка через `super()` — вызов отправится к первому предку в списке:

    class Xyzzy(Foo, Bar):
        def hello(self):
            super().hello()
    print("This is what my parent does")

    x = Xyzzy()

    >>> x.hello()
    hello world
    This is what my parent does

## Заключение

Даже небольшие приложения будет проще поддерживать и расширять, если уделить внимание взаимоотношениям между кодом и данными. Теперь ты знаешь детали поведения объектов — надеюсь, это тебе поможет.

← Ранее Firefox будет информировать пользователей о собранной телеметрии
Далее → Арестованы трое подозреваемых во взломе мальтийского банка
1 комментарий

#### Последние взломы

### HTB EarlyAccess. Эксплуатируем веб-уязвимости и мисконфиги Docker

### HTB Horizontall. Захватываем веб-сервер через уязвимости в Strapi и Laravel

### Препарируем P-Code. Как реверсить старый софт на Visual Basic

### HTB Anubis. Захватываем домен через сертификаты Active Directory

#### Компьютерные трюки

### Анатомия терминала. Осваиваем текстуальную графику

### Змей-стражник. Сигнализация для велосипеда из старого смартфона на Android

### M1 для хакера. Тестируем Homebrew, виртуалки, Docker и Node на новой архитектуре Apple

### Отключить всё! Ускоряем работу Windows 11 на старом железе

#### Свежие новости

- 13 часов назад

  ### Неизвестная хак-группа годами атаковала авиационный и транспортный секторы

- 14 часов назад

  ### Конференция ZeroNights 2022 состоится 23 июня 2022 года

- 15 часов назад

  ### Участник Team Xecuter получил 40 месяцев тюрьмы

- 16 часов назад

  ### Google выпустила срочное исправление для 0-day уязвимости в Chrome

- 2 дня назад

  ### Накануне Супербоула клуб «Сан-Франциско Форти Найнерс» подвергся атаке шифровальщика

Вопросы по материалам и подписке: support@glc.ru
Отдел рекламы и спецпроектов: yakovleva.a@glc.ru
Контент 18+
Сайт защищен Qrator —
самой забойной защитой от DDoS в мире
Подписка для физлиц
Подписка для юрлиц
Реклама на «Хакере»
Контакты
Posting....
wpDiscuz