Введение
При написании классов обычно стараются скрыть внутренние детали. В этом разделе представлены несколько идиом программирования на Python для этого, включая приватные переменные и свойства.
This tutorial is from open-source community. Access the source code
💡 Этот учебник переведен с английского с помощью ИИ. Чтобы просмотреть оригинал, вы можете перейти на английский оригинал
При написании классов обычно стараются скрыть внутренние детали. В этом разделе представлены несколько идиом программирования на 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 полагается на соглашения программирования для обозначения предполагаемого использования чего-либо. Эти соглашения основаны на именовании. В целом принятое мнение заключается в том, что遵守规则м программисту, а не языку, за это отвечать.
Любое имя атрибута, начинающееся с _
, считается приватным.
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. Однако для большинства повседневных задач программирования они не обязательны.
Свойства - это полезный способ добавить "вычисляемые атрибуты" к объекту. В 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()
.
Измените атрибут 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: ожидается целое число
>>>
Измените класс 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, чтобы улучшить свои навыки.