Классы и инкапсуляция

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

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

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

Введение

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

Публичное vs Приватное

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

Проблема

В Python почти все, касающееся классов и объектов, является открытым.

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

Это проблема, когда вы пытаетесь изолировать детали внутренней реализации.

Инкапсуляция в Python

Python relies on programming conventions to indicate the intended use of something. These conventions are based on naming. There is a general attitude that it is up to the programmer to observe the rules as opposed to having the language enforce them.

Инкапсуляция в Python

Python полагается на соглашения программирования для обозначения предполагаемого использования чего-либо. Эти соглашения основаны на именовании. В целом принятое мнение заключается в том, что遵守规则м программисту, а не языку, за это отвечать.

Приватные атрибуты

Любое имя атрибута, начинающееся с _, считается приватным.

class Person(object):
    def __init__(self, name):
        self._name = 0

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

>>> p = Person('Guido')
>>> p._name
'Guido'
>>> p._name = 'Dave'
>>>

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

Простые атрибуты

Рассмотрим следующий класс.

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

Поразительным является то, что вы можете устанавливать атрибуты любым значением:

>>> s = Stock('IBM', 50, 91.1)
>>> s.shares = 100
>>> s.shares = "hundred"
>>> s.shares = [1, 0, 0]
>>>

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

s.shares = '50'     ## Raise a TypeError, this is a string

Как бы вы это сделали?

Управляемые атрибуты

Одним подходом является введение методов доступа.

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

    ## Функция, которая слоирует операцию "получения"
    def get_shares(self):
        return self._shares

    ## Функция, которая слоирует операцию "установки"
    def set_shares(self, value):
        if not isinstance(value, int):
            raise TypeError('Expected an int')
        self._shares = value

К сожалению, это сломает весь наш существующий код. s.shares = 50 становится s.set_shares(50)

Свойства

Есть альтернативный подход к предыдущему паттерну.

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

    @property
    def shares(self):
        return self._shares

    @shares.setter
    def shares(self, value):
        if not isinstance(value, int):
            raise TypeError('Expected int')
        self._shares = value

Теперь обычный доступ к атрибуту запускает методы getter и setter, расположенные под @property и @shares.setter.

>>> s = Stock('IBM', 50, 91.1)
>>> s.shares         ## Triggers @property
50
>>> s.shares = 75    ## Triggers @shares.setter
>>>

При этом паттерне изменения в исходном коде не требуются. Новый setter также вызывается при присваивании внутри класса, включая внутри метода __init__().

class Stock:
    def __init__(self, name, shares, price):
     ...
        ## This assignment calls the setter below
        self.shares = shares
     ...

 ...
    @shares.setter
    def shares(self, value):
        if not isinstance(value, int):
            raise TypeError('Expected int')
        self._shares = value

Часто возникает путаница между свойством и использованием приватных имен. Хотя свойство внутри использует приватное имя, такое как _shares, остальная часть класса (не свойство) может продолжать использовать имя, такое как shares.

Свойства также полезны для вычисляемых атрибутов данных.

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

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

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

>>> s = Stock('GOOG', 100, 490.1)
>>> s.shares ## Instance variable
100
>>> s.cost   ## Computed Value
49010.0
>>>

#统一访问

Последний пример показывает, как создать более единый интерфейс для объекта. Если вы этого не сделаете, использование объекта может быть запутанным:

>>> s = Stock('GOOG', 100, 490.1)
>>> a = s.cost() ## Метод
49010.0
>>> b = s.shares ## Атрибут данных
100
>>>

Почему для cost требуются круглые скобки, а для shares нет? Свойство может это исправить.

Синтаксис декоратора

Синтаксис @ называется "декорированием". Он задает модификатор, который применяется к определению функции, которая непосредственно следует за ним.

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

Подробнее изложено в разделе 7.

Атрибут __slots__

Вы можете ограничить набор имен атрибутов.

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

Для других атрибутов будет возбуждена ошибка.

>>> s.price = 385.15
>>> s.prices = 410.2
Traceback (most recent call last):
File "<stdin>", line 1, in?
AttributeError: 'Stock' object has no attribute 'prices'

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

Последние замечания по инкапсуляции

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

Упражнение 5.6: Простые свойства

Свойства - это полезный способ добавить "вычисляемые атрибуты" к объекту. В stock.py вы создали объект Stock. Обратите внимание, что в вашем объекте есть небольшая несовместимость в том, как извлекаются разные виды данных:

>>> from stock import Stock
>>> s = Stock('GOOG', 100, 490.1)
>>> s.shares
100
>>> s.price
490.1
>>> s.cost()
49010.0
>>>

В частности, обратите внимание, как вам приходится добавлять дополнительные круглые скобки к cost, потому что это метод.

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

>>> ================================ RESTART ================================
>>> from stock import Stock
>>> s = Stock('GOOG', 100, 490.1)
>>> s.cost
49010.0
>>>

Попробуйте вызвать s.cost() в качестве функции и заметьте, что это уже не работает, так как cost теперь определено как свойство.

>>> s.cost()
... не работает...
>>>

Внесение этого изменения, вероятно, сломает ваш ранний pcost.py-программу. Возможно, вам придется вернуться и убрать круглые скобки из метода cost().

✨ Проверить решение и практиковаться

Упражнение 5.7: Свойства и сеттеры

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

>>> ================================ RESTART ================================
>>> from stock import Stock
>>> s = Stock('GOOG',100,490.10)
>>> s.shares = 50
>>> s.shares = 'a lot'
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
TypeError: ожидается целое число
>>>
✨ Проверить решение и практиковаться

Упражнение 5.8: Добавление слотов

Измените класс Stock так, чтобы он имел атрибут __slots__. Затем, проверьте, что нельзя добавить новые атрибуты:

>>> ================================ RESTART ================================
>>> from stock import Stock
>>> s = Stock('GOOG', 100, 490.10)
>>> s.name
'GOOG'
>>> s.blah = 42
... посмотрите, что произойдет...
>>>

Когда вы используете __slots__, Python использует более эффективное внутреннее представление объектов. Что произойдет, если вы попытаетесь проверить внутренний словарь s выше?

>>> s.__dict__
... посмотрите, что произойдет...
>>>

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

✨ Проверить решение и практиковаться

Резюме

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