Приватность свойств. Name Mangling (_name и __name
)
Для установления приватности для класса добавляют __
в начале имени свойства (переменной, функции)
from datetime import datetime
class Person:
def __init__(self, name, surname, birthday):
self._name = name
self._surname = surname
self.__birthday = birthday
self.name = f"{self._name} {self._surname}"
@staticmethod
def _get_current_time():
return datetime.now()
def _show_birthday(self):
return self.__birthday
def print_data(self):
print(f"Now {self._get_current_time()}: {self.name} Birthday: {self._show_birthday()}")
people1 = Person("Dmitry", "Budaev", "14.04.1989")
print(people1.print_data)
# дословно - связный метод Person.print_data объекта Person
# <bound method Person.print_data of <__main__.Person object at 0x7fb9ef1a1690>>
print(hex(id(people1)))
# 0x7fb9ef1a1690s
print(people1.__dict__)
# {'_name': 'Dmitry', '_surname': 'Budaev', '_Person__birthday': '14.04.1989', 'name': 'Dmitry Budaev'}
# '_Person__birthday' - это и есть защищённый метод self.__birthday
Переменные (свойства / атрибуты) класса
Область видимости:
Local -> Enclosing -> Global -> Builtins
name = "Ivan"
class Person:
name = "Dima"
def print_name(self):
print(name)
p = Person()
p.print_name()
# Ivan
Переменная name
в вызываемом методе print_name
будет искаться в начале локально (Local), потом во вложенных функциях (Enclosing) (их нет в print_name
) потом глобально (Global) - и тут, как раз находит name = "Ivan"
[!warning] Использование nonlocal
Это поведение можно избежать, если обозначить переменную, как nonlocal
name = "Ivan"
class Person:
def primer(self):
name = "Dima"
def print_name(self):
nonlocal name # обозначим, что переменная не в локальной области видимости
print(name)
p = Person()
p.print_name()
# Dima
[!warning] Применяется только во вложенных функциях
nonlocal может применяться только в функциях, имеющих вложенную область видимости.
Вы получаете вложенную область видимости только тогда, когда определяете свою функцию внутри другой функции.
Анотация типов
Аннотации типов – это способ объявить ожидаемые типы аргументов и возвращаемого значения функции, переменных и атрибутов.
Допустимые значения анотации типов
- конкретный класс, например str или FrenchDeck;
- параметризованный тип коллекции, например
list[int], tuple[str, float]
и т. д.; typing.Optional
, напримерOptional[str]
, для объявления поля, которое может принимать значения типаstr
илиNone
.- Переменную можно также инициализировать значением. (
var_name: some_type = a_value
)
[!info] Важно
на этапе импорта, когда модель загружается, Python читает их и строит словарь annotations, который typing.NamedTuple и @dataclass затем используют для дополнения класса.
class DemoPlainClass:
a: int # заносится в анотацию
b: float = 1.1 # заносится в анотацию и проставляется значение атрибуту класса
c = 'spam' # обычный атрибут класса, а не анотация
- Для поля a заводится запись в
__annotations__
, но больше оно никак не используется: атрибут с именем a в классе не создается. - Поле b сохраняется в аннотациях и, кроме того, становится атрибутом класса со значением
1.1
. - c – это самый обычный атрибут класса, а не аннотация.
@staticmethod
@staticmethod – используется для создания метода, который ничего не знает о классе или экземпляре, через который он был вызван. Он просто получает переданные аргументы, без неявного первого аргумента, и его определение неизменяемо через наследование.
class Person():
@staticmethod
def is_adult(age): # нету self, т.к. метод не знает ничего о классе
if age > 18:
return True
else:
return False
@classmethod
[!info] Основное применение
Для создания нескольких инициализаторов в классе. Т.к. позволяет изменять свойства класса
Если захотим изменить переменную класса (которая находится в классе а не в __init__
) ...
...
p = Person()
p.name = 'asdfadsfdsgdsfag'
...
Ничего не выйдет - как раз из-за пространства имен (LEGB
)
Это можно сделать с помощью @classmethod
class Person:
name = "Dima"
@classmethod
def change_name(cls, name): # cls просто ставится вместо self
cls.name = name # чтобы показать, что речь идёт о классе а не о экземпляре
p = Person()
print(p.__dict__)
p.change_name("Ivan")
Еще пример кода:
...
@classmethod
def from_obj(cls, obj):
if hasattr(obj, 'name'): # Проверяет наличие метода у объекта
name = getattr(obj, 'name')
...
@property
Основная идея - создания буферных зон.
Чтобы скрыть от пользователя то, что должно быть скрыто. Либо для того чтобы создать интерфейс взаимодействия - его можно будет с легкостью видоизменять в дальнейшем.
class Person:
def __init__(self, name):
self._name = name
def get_name(self):
print("From get_name: ")
return self._name
def set_name(self, name):
print("From set_name: ")
self._name = name
name = property(fget=get_name, fset=set_name)
# вариант 2
# name = property()
# name.setter(set_name)
# name.getter(get_name)
# вариант 3
# name = property(get_name)
# name.setter(set_name)
p = Person("Dima")
# p.__dict__
# {'name': 'Dima'}
p.name
# From get_name:
# 'Dima'
p.name = "Ivan"
# From set_name:
# 'Ivan'
При использовании декоратора получится следующее
...
@property # работает как name = property(name)
def name(self):
return self._name
# теперь укажем сеттер
@name.setter # работает как name = name.setter(set_name)
def name(self, value): # при использовании декоратора функцию переименовываем
self._name = value # в name, как у @property
# Если имя функции у сеттера будет другим, а не таким же как у геттера (с @property), то получим исключение
[!info] Для чего можно использовать @property
- методы класса со свойством только для чтения
- метод класса для вычисляемых свойств (например, для конкатенации свойств класса и его вывода)
Самое простое - это создать функцию с декоратором @property
но только геттер (без сеттера не будет возможности записи значения).
Пример вычисляемого свойства:
class Person:
def __init__(self, name, lastname):
self._name = name
self._lastname = lastname
# вычисляемое свойство
@property
def full_name(self):
return f"{self._name} {self._lastname}""
Вычисляемые свойства имеет смысл использовать тогда, когда нет смысла хранить состояния (они будут считаться каждый раз при вызове)
Кеширование результата вычисляемых свойств
class Person:
def __init__(self, name, lastname):
self._name = name
self._lastname = lastname
self._fullname = None # для сохранения результата вычисления
@property
def name(self):
return self._name
@name.setter
def name(self, value):
self._name = value
self._fullname = None
@property
def lastname(self):
return self._lastname
@lastname.setter
def lastname(self, value):
self._lastname = value
self._fullname = None
@property
def full_name(self):
# Проверяем и кешируем вычисляемое свойство
if self._fullname is None:
self._fullname = f"{self._name} {self._lastname}"
return self._fullname
Наследование
Правила поиска имен (функций, переменных) в пространстве имен, при наследовании работает по принципу (Local -> Enclosing -> Global -> Builtins)
class Person:
def __init__(self, age):
self.age = age
def print_age(self):
print(f{self.age})
class Student(Person):
...
s = Student(10)
s.print_age()
print(s.__dict__)
# {'age': 10}
print(Student.__dict__)
# {'__module__': '__main__', '__doc__': None}
print(Person.__dict__)
# {'__module__': '__main__', '__init__': <function Person.__init__ at 0x7fe9358f7240>, 'print_age': <function Person.print_age at 0x7fe93596d1c0>, '__dict__': <attribute '__dict__' of 'Person' objects>, '__weakref__': <attribute '__weakref__' of 'Person' objects>, '__doc__': None}
print(Student.__dict__)
- покажет, что переменной age
нет в классе Student, т.к. она определена в классе Person
[!info] Неявное наследование классаobject
Все объекты (классы) которые создаются будут неявно наследоваться от базового класса object - наследоваться необходимые базовые методы (например,__module
,__doc__
и т.д.)
Для проверки цепочки наследования класса, можно воспользоваться встроенной функцией issubclass
:
class One:
...
class Two(One):
...
class Three(Two):
...
issubclasss(Three, One)
# True
Определение одного родителя
Если нужно узнать принадлежат ли два объекта одному классу - один родитель. Можно сделать так (сравнить тип объекта с помощью isinstance
или issubclass
совместно с type()
):
first_number = One()
number = Three()
isinstance(number, type(first_number))
# False
# т.к. экземпляр класса number не является экземпляром класса first_number
issubclasss(type(number), type(first_number))
# False
Перегрузка
Основная идея все та же - порядок разрешений и поиск имени в пространстве имен (LEGB)
class Person:
def print_h(self):
print("I am Human")
class Student(Person):
def print_h(self):
print("I am a student")
s = Student()
s.print_h()
# I am a student
Множественное наследование
При множественном наследовании, применяется правило MRO (Method Resolution Order) - порядок разрешения методов - тот кто левее, тот главнее
Будет искать метод сначала в Proffessor потом в Student. Т.к. в Proffessor есть такой метод, то выведет его.
class Person:
def hello(self):
print("I am Human")
class Student(Person):
def hello(self):
print("I am a student")
class Proffessor(Person):
def hello(self):
print("I am a proffessor")
class SomeOne(Proffessor, Student):
pass
s = SomeOne()
s.hello()
# I am a proffessor
Чтобы посмотреть, как будет произведен поиск в пространстве имен нужных свойств и методов, можно вызвать функцию mro()
у экземпляра класса.
...
s.__class__.mro()
# [__main__.SomeOne, __main__.Prof, __main__.Student,__main__.Person, object]
Mixins
Сама идея миксинов - подразумевает расширение возможностей объекта или класса (самого же создания экземпляра класса для миксина не предполагается)
class FoodMixins(Person):
food = None
def get_food(self):
if self.food is None:
raise ValueError("Food should be set")
print(f"I like {self.food}")
class Person:
def hello(self):
print("I am Human")
# Первым вставляем миксину для расширения класса
class Student(FoodMixins, Person):
food = "Pizza"
def hello(self):
print("I am a student")
s = Student()
s.hello()
[!info] Когда применяются Mixins
Например, когда нам нужно добавить какую-нибудь фичу большому количеству не связных между собой классов
Полиморфизм
Полиморфизм - разное поведение для одного и того же метода для разных классов
Например, +
- вызывает дандер метод __add__()
, который для разных типов данных делает разные вещи. Его можно переопределить задать любое поведение.
За сравнение _=_
отвечает метод __eq__()
super().__init__()
- Инициализация из родительского класса
Используется, в паттерне DRY, например.
class Person:
def __init__(self, name)
self.name = name
class Student(Person):
def __init__(self, name, surname):
super().__init__(name)
self.surname = surname
s = Student("Ivan", "Ivanoff")
print(s.__dict__)
# {"name": "Ivan", "surname": "Ivanoff"}
[!info]super().__init__()
- расположение имеет значение
Если он будет снизу всех переменных, то вызов__init__()
родительского класса может переписать переменные с одинаковым именем.
class GreetingFormal:
def __init__(self):
self.formal_greeting = "Добрый день,"
def greet_formal(self, name):
return f"{self.formal_greeting} {name}!"
class GreetingInformal:
def __init__(self):
self.informal_greeting = "Привет,"
def greet_informal(self, name):
return f"{self.informal_greeting} {name}!"
class GreetingMix(GreetingFormal, GreetingInformal):
def __init__(self):
GreetingFormal.__init__(self)
GreetingInformal.__init__(self)
mixed_greeting = GreetingMix()
print(mixed_greeting.greet_formal("Пользователь"))
print(mixed_greeting.greet_informal("Пользователь"))
В методе __init__
класса GreetingMix
не используется super()
- используется непосредственный вызов из базовых классов с указанием имён этих классов.
Из-за того, что метод __init__
присутствует в обоих базовых классах и происходит конфликт.
Интерпретатор при использовании функции super()
в нашем примере использовал бы метод того класса, который стоит левее при перечислении в объявлении производного класса.
Хешированные объекты
Хешированные объекты - ключевые объекты при работе со словарями (dict) и множествами (set)
class Person:
def hello(self, name):
self._name = name
@property
def name(self):
return self._name
# позволит работать с хешированными объектами
def _hash_(self):
return hash(self.name)
def _eq_(self, person_obj):
return isinstance(person_obj, Person) and self.name == person_obj.name
p1 = Person("Ivan")
p2 = Person("Ivan")
# это позволит передавать объекты в качестве ключей словаря
d = {p1: "Roman Romanoff"}
d.get(p1)
# Roman Romanoff
Абстрактные классы
Абстрактные классы - это классы, которые предназначены для наследования, но избегают реализации конкретных методов, оставляя только сигнатуры методов, которые должны реализовывать подклассы.
from abc import ABCMeta
class AbstractClass(object):
# атрибут метакласса всегда должен
# быть установлен как переменная класса
__metaclass__ = ABCMeta
# декоратор abstractmethod регистрирует этот метод как undefined
@abstractmethod
def virtual_method_subclasses_must_define(self):
# Можно оставить полностью пустым
# или предоставить базовую реализацию
# Обратите внимание, что обычно пустая интерпретация
# неявно возвращает `None`, но при регистрации
# это поведение больше не применяется.
Причины использования абстрактных классов
class MontyPython:
def joke(self):
raise NotImplementedError()
def punchline(self):
raise NotImplementedError()
class ArgumentClinic(MontyPython):
def joke(self):
return "Hahahahahah"
Когда мы создаем объект и называем это два метода, мы получим ошибку (как и ожидалось) с punchline()
метод.
sketch = ArgumentClinic()
sketch.punchline()
# AttributeError: 'ArgumentClinic' object has no attribute 'punchline'
Этого можно избежать, используя модуль Abstract Base Class (ABC).
from abc import ABCMeta, abstractmethod
class MontyPython(metaclass=ABCMeta):
@abstractmethod
def joke(self):
pass
@abstractmethod
def punchline(self):
pass
# у наследуемого класса 2 @abstractmethod, а мы использовали только один
class ArgumentClinic(MontyPython):
def joke(self):
return "Hahahahahah"
c = ArgumentClinic()
# TypeError: "Can't instantiate abstract class ArgumentClinic with abstract methods punchline"
На этот раз, когда мы пытаемся создать экземпляр объекта из неполного класса, мы немедленно получаем TypeError!
class ArgumentClinic(MontyPython):
def joke(self):
return "Hahahahahah"
def punchline(self):
return "Send in the constable!"
c = ArgumentClinic()
c
# <__main__.ArgumentClinic object at 0x7fee680d3640>
Простой пример с множественным наследованием
class A(object):
__metaclass__ = abc.ABCMeta
@abc.abstractmethod
def do(self):
pass
class B(object):
def do(self):
print "do"
class C(B, A):
pass
c = C()
Абстрактный класс наследуется от абстрактного класса
from abc import ABCMeta, abstractmethod
class OriginalAbstractclass(metaclass=ABCMeta):
@abstractmethod
def sample_method(self):
pass
class InheritedAbstractClass(OriginalAbstractclass, metaclass=ABCMeta):
@abstractmethod
def another_method(self):
pass
class ConcreteClass(InheritedAbstractClass):
def some_method(self):
pass
def another_method(self):
pass
__slots__
в классе и при наследовании
- https://ru.stackoverflow.com/questions/1206730/Какова-цель-slots-в-python
- https://proproprogs.ru/python_oop/python-kak-rabotaet-slots-s-property-i-pri-nasledovanii
class AbstractA(ABC):
__slots__ = ()
class AbstractB(ABC):
__slots__ = ()
class BaseA(AbstractA):
__slots__ = ('a',)
class BaseB(AbstractB):
__slots__ = ('b',)
class Child(AbstractA, AbstractB):
__slots__ = ('a', 'b')
c = Child()
То есть мы вместо того, чтобы наследоваться от классов с конкретной реализацией (BaseA, BaseB), наследуемся от абстрактных классов
Однако __slots__
может вызвать проблемы при множественном наследовании.
class BaseA(object):
__slots__ = ('a',)
class BaseB(object):
__slots__ = ('b',)
Создание дочернего класса от родителей с обоими непустыми слотами не удается:
>>> class Child(BaseA, BaseB): __slots__ = ()
Traceback (most recent call last):
File "<pyshell#68>", line 1, in <module>
class Child(BaseA, BaseB): __slots__ = ()
TypeError: Error when calling the metaclass bases
multiple bases have instance lay-out conflict
Если вы столкнетесь с этой проблемой, вы можете просто удалить __slots__
у родителей или, если вы контролируете родителей, дать им пустые слоты или выполнить рефакторинг для абстракций:
from abc import ABC
class AbstractA(ABC):
__slots__ = ()
class BaseA(AbstractA):
__slots__ = ('a',)
class AbstractB(ABC):
__slots__ = ()
class BaseB(AbstractB):
__slots__ = ('b',)
class Child(AbstractA, AbstractB):
__slots__ = ('a', 'b')
c = Child() # Нет ошибок
Добавьте '__dict__'
к __slots__
чтобы получить динамическое назначение:
class Foo(object):
__slots__ = 'bar', 'baz', '__dict__'
и сейчас
>>> foo = Foo()
>>> foo.boink = 'boink'
Таким образом, с '__dict__'
в слотах мы теряем некоторые преимущества размера с преимуществом наличия динамического назначения и по-прежнему наличия слотов для имен, которые мы ожидаем
Когда вы наследуете объект, который не имеет слотов, вы получаете такую же семантику, когда используете __slots__
- имена, которые находятся в __slots__
, указывают на значения, размещенные в слотах, тогда как любые другие значения помещаются в __dict__
экземпляра.
Избегать __slots__
, потому что вы хотите иметь возможность добавлять атрибуты на лету, на самом деле не является хорошей причиной - просто добавьте '__dict__'
в свой __slots__
, если это необходимо.
Вы можете точно так же явно добавить __weakref__
в __slots__
, если вам нужна эта функция.
Подводя итоги: если вы составляете миксины или используете абстрактные базовые классы, которые не предназначены для создания экземпляров, пустой __slots__
в этих родителях кажется лучшим способом гибкости для подклассов.
Чтобы продемонстрировать, сначала давайте создадим код с классом, который мы хотели бы использовать при множественном наследовании.
class AbstractBase:
__slots__ = ()
def __init__(self, a, b):
self.a = a
self.b = b
def __repr__(self):
return f'{type(self).__name__}({repr(self.a)}, {repr(self.b)})'
Мы могли бы использовать вышесказанное непосредственно путем наследования и объявления ожидаемых слотов:
class Foo(AbstractBase):
__slots__ = 'a', 'b'
Но нас это не волнует, это тривиальное одиночное наследование, нам нужен другой класс, от которого мы также могли бы унаследовать, возможно, с шумным атрибутом:
class AbstractBaseC:
__slots__ = ()
@property
def c(self):
print('getting c!')
return self._c
@c.setter
def c(self, arg):
print('setting c!')
self._c = arg
Теперь, если бы на обеих базах были непустые слоты, мы не смогли бы сделать следующее. (На самом деле, если бы мы хотели, мы могли бы дать AbstractBase
непустые слоты a
и b
и исключить их из приведенного ниже объявления - оставлять их было бы неправильно):
class Concretion(AbstractBase, AbstractBaseC):
__slots__ = 'a b _c'.split()
И теперь у нас есть функциональность от обоих через множественное наследование, и мы все еще можем запретить создание экземпляров __dict__
и __weakref__
:
>>> c = Concretion('a', 'b')
>>> c.c = c
setting c!
>>> c.c
getting c!
Concretion('a', 'b')
>>> c.d = 'd'
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
AttributeError: 'Concretion' object has no attribute 'd'
Композиция
С композицией класс Composite имеет экземпляр класса Component и может использовать его реализацию. Класс Component может быть повторно использован в других классах, совершенно не связанных с Composite.
Наследование vs Композиция
Наследование лучше всего использовать для моделирования отношений, тогда как для составных моделей лучше использовать композицию.
- Используйте наследование вместо композиции для моделирования четких отношений. Во-первых, обоснуйте связь между производным классом и его базой. Затем измените отношения и попытайтесь их оправдать. Если вы можете оправдать отношения в обоих направлениях, то вам не следует использовать наследство между ними.
- Используйте наследование вместо композиции, чтобы использовать как интерфейс, так и реализацию базового класса.
- Используйте наследование вместо композиции для предоставления функций смешивания (mixin) нескольким несвязанным классам, когда может существовать только одна реализация этой функции.
- Использование композиции вместо наследования для моделирования имеет отношение, которое использует реализацию класса компонента.
- Используйте композицию вместо наследования для создания компонентов, которые могут повторно использоваться несколькими классами в ваших приложениях.
- Используйте композицию вместо наследования для реализации групп поведений и политик, которые можно применять взаимозаменяемо к другим классам для настройки их поведения.
- Используйте композицию вместо наследования, чтобы включить изменения поведения во время выполнения, не затрагивая существующие классы.
Дескрипторы (__get__()
, __set__()
, __del__()
)
Дескриптором - любой объект, который реализует хотя бы один из следующих методов: get, set или delete
Дескрипторы позволяют переиспользовать повторяющийся код.
Дескрипторы бывают:
- No Data Descriptor - они не хранят данные а только отдают
- Data Descriptor - хранят данные (в них обязательно реализованы методы
__set__()
. Метод__delete()
необязательный и его может и не быть )
No Data Descriptor
Геттер (__get__()
) с аргументами:
- instance = экземпляр класса с которого происходит обращение к свойству
- owner_class = класс собственник
from time import time
class Epoch:
# intance - экземпляр класса с которого происходит обращение к свойству
# owner_class - класс собственник
def __get__(self, instance, owner_class):
return int(time())
class MyTime:
epoch = Epoch()
m = MyTime()
print(m.epoch)
# 15984124424 - время в секундах
Можно использовать и обычный геттер на @property
но в таком случае, будет возникать много повторяемого кода...
from random import choice
class Game:
@property
def rock_paper_scissors(self):
return choice(["Rock", "Paper", "Scissors"])
@property
def dice(self):
return choice(range(1, 7))
@property
def flip(self):
return choice(["Head", "Tails"])
g = Game()
print(g.flip)
# Tails
...
В этом случае поможет определение отдельного класса (No Data Descriptor) на основе дандер метода __get__ ()
from random import choice
class Choise:
# * - принимает любые аргументы и упаковывает их в список
def __init__(self, *choice):
self._choice = choice
def __get__(self, instance, owner_class):
return choice(self._choice)
class Game:
dice = ChoiceGame(1, 2, 3, 4, 5, 6)
flip = ChoiceGame("Head", "Tails")
rock_paper_scissors = ChoiceGame("Rock", "Paper", "Scissors")
g = Game()
print(g.flip)
# Tails
...
Data Descriptor
from time import time
class Epoch:
def __get__(self, instance, owner):
print(f"Self object {self}")
print(f"Instance object {instance}")
print(f"Owner object {owner}")
return int(time())
class MyTime:
epoch = Epoch()
m = MyTime()
При вызове epoch
через экземпляр m
класса MyTime
:
>>> m.epoch
Self object <__main__.Epoch object at 0x7fcd70bf6950>
Instance object <__main__.MyTime object at 0x7fcd70bf6ad0>
Owner object <class '__main__.MyTime'>
1686562054
В Self был передан сам объект класса Epoch
(сам дескриптор - экземпляр дескриптора)
В Instance объект из которого произошло обращение к дескриптору (экземпляр класса MyTime
)
В Owner объект, класс собственник MyTime
Мы можем обращаться к атрибуту класса (epoch
) через сам класс (MyTime
):
>>> MyTime.epoch
Self object <__main__.Epoch object at 0x7fcd70bf6950>
Instance object None
Owner object <class '__main__.MyTime'>
1686563123
В Instance не был передан никакой объект - т.к. обращение произошло через класс (MyTime
) а не через экземпляр (m
)
Это напоминает работу @property
:
- Возвращать сам экземпляр класса (дескриптора), если обращение произошло через класс
- Возвращать значение свойств, если обращение произошло через экземпляр
class Person:
_name = 'Oleg'
@property
def name(self):
return self._name
# обращение через класс - вернул экземпляр класса
>>> Person.name
<property object at 0x7fcd70bea570>
# обращение через экземпляр - вернул значение свойства
>>> Person().name
'Oleg'
[!warning] Особенность хранения данных в дескипторах
Нельзя хранить данные в экземпляре дескриптора (реализующих методы get, set, delete), т.к. они делят общее состояние с экземплярами класса.
Имеется ввиду со стандартной реализацией set.
Для хранения данных в дескипторе можно определить словарь (dict) в инициализации и в методе __set__()
передавать в него значение по ключу
class MyDescriptor:
def __init__(self):
self._values = {}
def __set__(self, instance, value):
self._values[instance] = value
def __get__(self, instance, owner):
if instance is None:
return self
return self._values.get(instance)
class Vector:
x = MyDescriptor()
y = MyDescriptor()
v1 = Vector()
v2 = Vector()
Но такой вариант реализации допустит утечку памяти - при пересохранении значений будут сохраняться ссылки на значения.
Чтобы предотвратить утечку памяти, следует использовать слабые ссылки (weakref).
Для подсчета ссылок, можно использовать:
import sys
val = 'Oleg'
sys.getrefcount(val)
Либо с библиотекой ctypes
import ctypes
def ref_count(obj):
return ctypes.c_long.from_address(id(obj_id)).value
Описание утечки памяти из примера выше:
v1.x = 5
v2.x = 10
Vector.x._values
# {<__main__.Vector object at 0x7f621cddec10>: 5, <__main__.Vector object at 0x7f621cddec50>: 10}
v3 = Vector()
# вызываем верхнюю функцию для подсчёта ссылок
ref_count(v3)
# 1
v3.x = 5
ref_count(v3)
# 2
v3.y = 5
ref_count(v3)
# 3
del v3
ref_count(v3)
# 2
# тут и происходит утечка памяти - не все ссылки на объекты были удалены
Слабые ссылки (weakref)
Принцип работы слабых ссылок точно такой же, как и у сильных ссылок (strong ref), только они дополнительно учитываются сборщиком мусора
import weakref
class Person:
pass
p = Person()
w = weakref.ref(p)
print(w)
<weakref at 0x7f5b301ce610; to 'Person' at 0x7f5b300eed50>
>>> hex(id(p))
# '0x7f5b300eed50'
print(p)
<__main__.Person object at 0x7f5b300eed50>
# удаляем ссылку
del p
print(w)
<weakref at 0x7f5b301ce610; dead>
# статус слабой ссылки изменился на dead - она была удалена. Хотя сам объект пока остается в памяти
# но если попытаться вызвать объект по мертвой ссылке (dead)...
print(w())
# python вернет None - тоесть ничего
В библиотеки (weakref) для работы со слабыми ссылками есть специальный класс для сохранения ссылок в словарь (WeakKeyDictionary()
)
from weakref import WeakKeyDictionary
d = weakref.WeakKeyDictionary()
d[p] = 10
d[p]
# 10
# для просмотра ссылок
d.keyrefs()
# [<weakref at 0x7f5b2e61d760; to 'type' at 0x5654f6296d40 (Person)>]
del p
d.keyrefs()
# []
[!info] Про ключи для словарей
Ключами для словарей, могут быть только хешируемые объекты! Т.е. объекты, которые реализуют метод__hash__
.
Доступные дандер методы можно посмотреть в dir(obj)
Возвращаясь к примеру Data Descriptor, для устранения утечки памяти и работы дескриптора со слабыми ссылками, можно воспользоваться WeakKeyDictionary()
from weakref import WeakKeyDictionary
class MyDescriptor:
def __init__(self):
self._values = WeakKeyDictionary()
def __hash__(self):
return hash(self._values)
def __set__(self, instance, value):
self._values[instance] = value
def __get__(self, instance, owner):
if instance is None:
return self
return self._values.get(instance)
class Vector:
x = MyDescriptor()
y = MyDescriptor()
v1 = Vector()
v2 = Vector()
Проверим работу ссылок:
>>> v1.x = 10
>>> Vector.x._values.keyrefs()
[<weakref at 0x7f5e5c011b70; to 'Vector' at 0x7f5e5c0027d0>]
>>> del v1
>>> Vector.x._values.keyrefs()
[]
Метод __set_name__
Этот вызывается только один раз, когда создаётся дескриптор вызывается впервые (создаётся экземпляр дескриптора x = MyDescriptor()
) и в него передаётся имя атрибута (x
).
class ValidString:
def __set_name__(self, owner_class, propetry_name):
print(f"owner_class: {owner_class}")
print(f"propetry_name: {propetry_name}")
class Person:
my_new_name = ValidString()
При запуске этого кода, сразу будет запущен __set_name__()
. Сразу на этапе компиляции, даже если не создавать экземпляр класса Person
class ValidString:
def __set_name__(self, owner_class, propetry_name):
self.propetry_name = propetry_name
def __set__(self, instance, value):
if not isinstance(value, str):
raise ValueError(f"{self.propetry_name} must be a String, but {type(value)} was passed")
#key = "_" + self.propetry_name
#setattr(instance, key, value)
# аналогичная запись
instance.__dict__[self.propetry_name] = value
def __get__(self, instance, owner):
if instance is None:
return self
#key = "_" + self.propetry_name
#return getattr(instance, key, None)
# аналогичная запись
instance.__dict__.get(self.propetry_name, None)
class Person:
name = ValidString()
surname = ValidString()
p = Person()
Примечательно то, что у дескриптора нет метода __init__
- т.к. есть __set_name__
который делает то, что нужно
p.name = "Oleg"
p.__dict__
{"name": "Oleg"}
[!warning] Особенность записи имен в пространство имен с дескрипторами
Значениеname
запишется сразу в локальное пространство имен экземпляра классаp
, а не классаPerson
- это особенность работы дескриптора.
Если проделать подобное с обычным классом - name
запишется в сам класс (если в самом классе существует name
, она будет перезаписана):
>>> class Person:
... name = "Ivan"
...
>>> p = Person()
>>> p.name
'Ivan'
>>> p.__dict__
{}
>>> p.name = "Dima"
>>> p.name
'Dima'
>>> p.__dict__
{'name': 'Dima'}
>>> Person.name
'Ivan'
>>> Person.__dict__
mappingproxy({'__module__': '__main__', 'name': 'Ivan', '__dict__': <attribute '__dict__' of 'Person' objects>, '__weakref__': <attribute '__weakref__' of 'Person' objects>, '__doc__': None})
Чтение имени значения и его запись происходит всегда через дескриптор, поэтому, такой проблемы в дескрипторе нет.