Введение
При написании классов обычно стараются скрыть внутренние детали. В этом разделе представлены несколько идиом программирования на 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, чтобы улучшить свои навыки.