Пять важных понятий, необходимых для быстрого старта в python ооп

Введение

Большинству python-разработчикам приходится регулярно писать такие классы:

Уже на этом примере видна избыточность. Идентификаторы title и author используются несколько раз. Реальный класс же будет ещё содержать переопределенные методы и .

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

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

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

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

Публичные и приватные

Java управляет доступом к методам и атрибутам, различая публичные и приватные данные.
В Java ожидается, что атрибуты будут объявлены как приватные (или защищенные — protected, если нужно обеспечить к ним доступ потомкам класса). Таким образом мы ограничиваем доступ к ним извне. Чтобы предоставить доступ к приватным атрибутам, мы объявляем публичные методы, которые устанавливают или получают эти данные (подробнее об этом – чуть позже).

Вспомним, что в нашем Java-коде переменная color была объявлена приватной. Следовательно, нижеприведенный код не скомпилируется:

Если не указать уровень доступа к атрибутам, то по умолчанию он будет установлен как package protected, что ограничивает доступ к классам в пределах пакета. Если же мы хотим, что вышеуказанный код заработал, то придется сделать атрибут публичным.

Однако, в Java не приветствуется объявление атрибутов публичными. Рекомендуется объявлять их приватными, а затем использовать публичные методы, наподобие getColor() и getModel(), как и было указано в тексте кода выше.

В противоположность, в Python отсутствуют понятия публичных и приватных данных. В Python всё – публичное. Этот питоновский код сработает на ура:

Вместо приватных переменных в Python имеется понятие непубличных (non-public) переменных экземпляра класса. Все переменные, названия которых начинаются с одинарного подчеркивания, считаются непубличными. Это соглашение об именах нисколько не мешает нам обратиться к переменной напрямую.

Добавим следующую строку в наш питоновский класс Car:

Мы можем получить доступ к переменной _cupholders напрямую:

Python позволяет получить доступ к такой переменной, правда, некоторые среды разработки вроде VS Code выдадут предупреждение:

Кроме этого, в Python для того, чтобы скрыть атрибут, используется двойное подчеркивание в начале названия переменной. Когда Python видит такую переменную, он автоматически меняет ее название, чтобы затруднить к ней прямой доступ. Однако, этот механизм всё равно не мешает нам обратиться к ней. Продемонстрируем это следующим примером:

Теперь если мы обратимся к переменной __cupholders, мы получим ошибку:

Так почему же атрибут __cupholders не существует?
Дело вот в чем. Когда Python видит атрибут с двойным подчеркиванием в самом начале, он меняет его, добавляя в начало имя класса с подчеркиванием. Для того чтобы обратиться к атрибуту напрямую, необходимо также изменить имя:

Теперь возникает вопрос: если атрибут Java-класса объявлен приватным и атрибуту Python-класса предшествует в имени двойное подчеркивание, то как достучаться до этих данных?

The self Parameter

The parameter is a reference to the
current instance of the class, and is used to access variables that belongs to the class.

It does not have to be named , you can
call it whatever you like, but it has to be the first parameter of any function
in the class:

Example

Use the words mysillyobject and abc instead of self:

class Person:  def __init__(mysillyobject, name, age):   
mysillyobject.name = name    mysillyobject.age = age  def myfunc(abc):   
print(«Hello my name is » + abc.name)p1 = Person(«John»,
36)p1.myfunc()

Python Syntax Tutorial
Class
Create Class
The Class __init__() Function
Object Methods
Modify Object Properties
Delete Object Properties
Delete Object
Class pass Statement

Изменение полей объекта

В Python объекту можно не только переопределять поля и методы, унаследованные от класса, также можно добавить новые, которых нет в классе:

>>> l.test = "hi"
>>> B.test
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
AttributeError: type object 'B' has no 
attribute 'test'
>>> l.test
'hi'

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

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

>>> class User:
...     def setName(self, n):
...             self.name = n
...     def getName(self):
...             try:
...                     return self.name
...             except:
...                     print("No name")
... 
>>> first = User()
>>> second = User()
>>> first.setName("Bob")
>>> first.getName()
'Bob'
>>> second.getName()
No name

Подобные методы называют сеттерами (set – установить) и геттерами (get – получить).

Простая функция

Python Функция это блок кода, который начинается с ключевого слова def, с дальнейшим названием функции. Функция может принимать от нуля и более аргументов, ключевые аргументы или сочетание этих аргументов. Функция всегда выдает результат. Если вы не определили, что именно она должна выдавать, она выдаст None. Вот очень простой пример функции, которая выдает строку:

Python

# -*- coding: utf-8 -*-
def a_function():
«»»Обычная функция»»»
return «1+1»

if __name__ == «__main__»:
value = a_function()
print(value)

1
2
3
4
5
6
7
8

# -*- coding: utf-8 -*-

defa_function()

«»»Обычная функция»»»

return»1+1″

if__name__==»__main__»

value=a_function()

print(value)

Все что мы сделали в этом коде, это вызвали функцию и указали значение выдачи. Давайте создадим другую функцию:

Python

# -*- coding: utf-8 -*-
def another_function(func):
«»»
Функция которая принимает другую функцию.
«»»

def other_func():
val = «Результат от %s это %s» % (func(),
eval(func())
)
return val

return other_func

1
2
3
4
5
6
7
8
9
10
11
12
13

# -*- coding: utf-8 -*-

defanother_function(func)

«»»

    Функция которая принимает другую функцию.
    «»»

defother_func()

val=»Результат от %s это %s»%(func(),

eval(func())

)

returnval

returnother_func

Эта функция принимает один аргумент и этот аргумент должен быть функцией или вызываемой. По факту, ее стоит вызывать, используя определенную в прошлом функцию. Вы заметите, что это функция содержит вложенную внутрь функцию, которую мы называем other_func. Она принимает результат переданной функции, её выражение и создает строку, которая говорит нам о том, что мы сделали, после чего она возвращается.

Давайте взглянем на полную версию данного кода:

Python

# -*- coding: utf-8 -*-
def another_function(func):
«»»
Функция которая принимает другую функцию.
«»»

def other_func():
val = «Результат от %s это %s» % (func(),
eval(func())
)

return val
return other_func

def a_function():
«»»Обычная функция»»»
return «1+1»

if __name__ == «__main__»:
value = a_function()
print(value)
decorator = another_function(a_function)
print(decorator())

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# -*- coding: utf-8 -*-

defanother_function(func)

«»»

    Функция которая принимает другую функцию.
    «»»

defother_func()

val=»Результат от %s это %s»%(func(),

eval(func())

)

returnval

returnother_func

defa_function()

«»»Обычная функция»»»

return»1+1″

if__name__==»__main__»

value=a_function()

print(value)

decorator=another_function(a_function)

print(decorator())

Так и работает декоратор. Мы создали одну функцию и передали её другой второй функции. Вторая функция является функцией декоратора. Декоратор модифицирует или усиливает функцию, которая была передана и возвращает модификацию. Если вы запустите этот код, вы увидите следующий выход в stdout:

Python

1+1
Результат от 1+1 это 2

1
2

1+1
Результат от 1+1 это 2

Давайте немного изменим код, чтобы превратить another_function в декоратор:

Python

# -*- coding: utf-8 -*-
def another_function(func):
«»»
Функция которая принимает другую функцию.
«»»

def other_func():
val = «Результат от %s это %s» % (func(),
eval(func())
)
return val

return other_func

@another_function
def a_function():
«»»Обычная функция»»»
return «1+1»

if __name__ == «__main__»:
value = a_function()
print(value)

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24

# -*- coding: utf-8 -*-

defanother_function(func)

«»»

    Функция которая принимает другую функцию.
    «»»

defother_func()

val=»Результат от %s это %s»%(func(),

eval(func())

)

returnval

returnother_func
 
 

@another_function

defa_function()

«»»Обычная функция»»»

return»1+1″

if__name__==»__main__»

value=a_function()

print(value)

Обратите внимание на то, что декоратор начинается с символа @, за которым следует название функции, которую мы собираемся «декорировать». Для получения декоратора python, вам нужно только разместить его в строке перед определением функции

Теперь, когда мы вызываем **a_function, она будет декорирована, и мы получим следующий результат:

Python

Результат от 1+1 это 2

1 Результатот1+1это2

Давайте создадим декоратор, который будет делать что-нибудь полезное.

Управление доступом

В Java мы получаем доступ к приватным атрибутам при помощи сеттеров (setters) и геттеров (getters). Для того чтобы пользователь перекрасил-таки свою машину, добавим следующий кусок кода в Java-класс:

Поскольку методы getColor() и setColor() – публичные, то любой пользователь может вызвать их и получить / изменить цвет машины. Использование приватных атрибутов, к которым мы получаем доступ публичными геттерами и сеттерами, — одна из причин большей «многословности» Java в сравнении с Python.

Как было показано выше, в Python мы можем получить доступ к атрибутам напрямую. Поскольку всё – публичное, мы может достучаться к чему угодно, когда угодно и откуда угодно. Мы можем получать и устанавливать значения атрибутов напрямую, обращаясь по их имени. В Python мы можем даже удалять атрибуты, что немыслимо в Java:

Однако бывает и так, что мы хотим контролировать доступ к атрибутам. В таком случае нам на помощь приходят Python-свойства (properties).

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

Работу свойств можно увидеть на следующем примере класса Car:

В данном примере мы расширяем понятие класса Car, включая электромобили. В строке 6 объявляется атрибут _voltage, чтобы хранить в нем напряжение батареи.

В строках 9 и 10 для контролируемого доступа мы создаем функцию voltage() и возвращаем значение приватной переменной. Используя декоратор @property, мы превращаем его в геттер, к которому теперь любой пользователь получает доступ.

В строках 13-15 мы определяем функцию, так же носящую название voltage(). Однако, мы ее декорируем по-другому: voltage.setter. Наконец, в строках 18-20 мы декорируем функцию voltage() при помощи voltage.deleter и можем при необходимости удалить атрибут _voltage.

Декорируемые функции носят одинаковые имена, указывая на то, что они управляют доступом к одному и тому же атрибуту. Эти имена функций также становятся именами атрибутов, используемых для получения их значений. Вот как это работает:

Обратите внимание, что мы используем voltage, а не _ voltage. Так мы указываем Python-у на то, что следует применять свойства, которые только что определили:

  • Когда в 4-й строке выводим значение my_car.voltage, Python вызывает функцию voltage(), декорированную @property.
  • Когда в 7-й строке присваиваем значение my_car.voltage, Python вызывает функцию voltage(), декорированную voltage.setter.
  • Когда в 13-й строке удаляем my_car.voltage, Python вызывает функцию voltage(), декорированную voltage.deleter.

Вышеприведенные декораторы дают нам возможность контролировать доступ к атрибутам без использования различных методов. Можно даже сделать атрибут свойством только для чтения (read-only), убрав декорированные функции @.setter и @.deleter.

Defining a Class in Python

Like function definitions begin with the keyword in Python, class definitions begin with a keyword.

The first string inside the class is called docstring and has a brief description about the class. Although not mandatory, this is highly recommended.

Here is a simple class definition.

A class creates a new local namespace where all its attributes are defined. Attributes may be data or functions.

There are also special attributes in it that begins with double underscores . For example, gives us the docstring of that class.

As soon as we define a class, a new class object is created with the same name. This class object allows us to access the different attributes as well as to instantiate new objects of that class.

Output

10
<function Person.greet at 0x7fc78c6e8160>
This is a person class

dataclass как вызываемый декоратор

Не всегда желательно, что бы были определены все методы. Возможно вам понадобится только хранение значений и проверка равенства. Таким образом, вам нужны только определенные методы __init__ и __eq__. Если бы мы могли сказать декоратору не генерировать другие методы, это уменьшило бы некоторые накладные расходы, и у нас были бы только нужные операции над объектом данных.

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

Из официальных декоратор может использоваться как вызываемый со следующими аргументами:

@dataclass(init=True, repr=True, eq=True, order=False, unsafe_hash=False, frozen=False)
class C:
 …
  1. : По умолчанию будет создан метод __init__. Если ему передано значение False, у класса не будет метода __init__.
  2. : Метод __repr__ генерируется по умолчанию. Если ему передано значение False, у класса не будет метода __repr__.
  3. : По умолчанию будет создан метод __eq__. Если ему передано значение False, метод __eq__ не будет добавлен классом данных, но по умолчанию все равное будет object.__eq__.
  4. : По умолчанию генерируются методы __gt__, __ge__, __lt__, __le__. Если будет False, они не будут заданы.

Аргумент frozen мы обсудим чуть позже. Аргумент unsafe_hash заслуживает отдельного поста из-за его сложных вариантов использования.

Сейчас вернемся к нашему примеру использования, вот что нам нужно:

1. 2. 

Эти функции генерируются по умолчанию, поэтому нам не нужно генерировать другие функции. Как нам это сделать? Просто передайте соответствующим аргументам значение False.

@dataclass(repr = False) # order, unsafe_hash and frozen are False
class Number:
    val: int = 0

>>> a = Number(1)

>>> a
>>> <__main__.Number object at 0x7ff395afe898>

>>> b = Number(2)

>>> c = Number(1)

>>> a == b
>>> False

>>> a < b
>>> Traceback (most recent call last):
 File “<stdin>”, line 1, in <module>
TypeError: ‘<’ not supported between instances of ‘Number’ and ‘Number’

Наследование

Когда вы используете декоратор , он проходит по всем родительским классам начиная с object и для каждого найденного класса данных сохраняет поля в упорядоченный словарь (ordered mapping), затем добавляя свойства обрабатываемого класса. Все сгенерированные методы используют поля из полученного упорядоченного словаря.

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

Так как упорядоченный словарь хранит значения в порядке вставки, то для следующих классов

будет сгенерирован метод с такой сигнатурой:

Типы данных и полиморфизм

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

Реализуем зарядку устройства для наших Java-объектов при помощи создания метода charge(), который принимает в качестве параметра переменную типа Device. Любой объект, реализующий интерфейс Device, может быть передан методу charge().

Создадим следующий класс в файле под названием Rhino.java:

Теперь создадим файл Main.java с методом charge() и посмотрим, чем отличаются объекты классов Car и Rhino.

Поскольку в классе Rhino не реализован интерфейс Device, его нельзя передать в качестве параметра в charge().

В отличие от статической типизации (в оригинале — strict variable typing, то есть строгая типизация, но Python тоже относится к языкам со строгой типизацией) переменных, принятой в Java, в Python используется концепция утиной типизации, которая в общем виде звучит так: если переменная «ходит как утка и крякает как утка, то это и есть утка» (на самом деле звучит немного иначе: «если нечто выглядит как утка, плавает как утка и крякает как утка, то это, вероятно, и есть утка» – прим. переводчика). Вместо идентификации объектов по типу, Python проверяет их поведение.

Лучше понять утиную типизацию поможет следующий аналогичный пример зарядки устройства на Python:

charge() проверяет существование в объекте атрибута _voltage. Поскольку в классе Device имеется такой атрибут, то и в любом его классе-наследнике (Car и Phone) тоже будет этот атрибут, и, следовательно, этот класс выведет сообщение о зарядке. У классов, которые не унаследовались от Device (как Rhino), не будет этого атрибута, и они не будут заряжаться, что хорошо, поскольку для жизни носорога (rhino) электрическая зарядка смертельно опасна.

deque

В соответствии с документацией Python, deque – это обобщение стеков и очередей. Они являются контейнером замен для списка Python. Они защищены от потоков и поддерживают эффективность соединения памяти, а также сноски с обеих сторон deque. Список оптимизирован под быстрые операции с фиксированной длиной. За всеми подробностями можете обратиться к . Наш deque поддерживает аргумент maxlen, который устанавливает границы для deque. В противном случае deque будет расти до произвольных размеров. Когда ограниченный deque заполнен, любые новые объекты, которые были добавлены, вызовут такое же количество элементов, которые выскочат с другого конца. Вот основное правило: если вам нужно что-то быстро дописать или вытащить, используйте deque. Если вам нужен быстрый случайный доступ, используйте list. Давайте уделим пару минут, и посмотрим, как мы можем создавать и использовать deque.

Python

from collections import deque
import string

d = deque(string.ascii_lowercase)
for letter in d:
print(letter)

1
2
3
4
5
6

fromcollectionsimportdeque

importstring

d=deque(string.ascii_lowercase)

forletter ind

print(letter)

Здесь мы импортируем deque из нашего модуля collections, а также модуль string. Для того, чтобы создать экземпляр deque, нам нужно передать его итерируемой. В данном случае, мы передали его string.ascii_lowercase, и получили список всех строчных букв в алфавите. Наконец, мы сделали цикл над deque и вывели каждый объект. Теперь давайте взглянем на несколько методов, которыми обладает deque.

Python

d.append(‘bork’)
print(d)

# deque()

d.appendleft(‘test’)
print(d)

# deque()

d.rotate(1)
print(d)

# deque()

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17

d.append(‘bork’)

print(d)

 
# deque()
 

d.appendleft(‘test’)

print(d)

 
# deque()
 

d.rotate(1)

print(d)

 
# deque()

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

Python

from collections import deque

def get_last(filename, n=5):
«»»
Возвращаем последние N кол-во строк из файла
«»»
try:
with open(filename) as f:
return deque(f, n)
except OSError:
print(«Файл не открывается: {}».format(filename))
raise

1
2
3
4
5
6
7
8
9
10
11
12
13

fromcollectionsimportdeque

defget_last(filename,n=5)

«»»

    Возвращаем последние N кол-во строк из файла
    «»»

try

withopen(filename)asf

returndeque(f,n)

exceptOSError

print(«Файл не открывается: {}».format(filename))

raise

Этот код работает по схожему принципу с программой-хвостом Linux. Здесь мы передаем имя файла нашему скрипту вместе с n количеством строк, которые мы хотим вернуть. Наш deque ограничен той или иной цифрой, которую мы указываем как n. Это значит, что как только deque заполнится, когда новые строки прочитаны и добавлены в deque, старые строки выпадают из другого конца и отбрасываются. Я также завернул открываемый в операторе файл в простой обработчик исключений, так как очень легко выполнить передачу по неверному пути. Таким образом, мы поймаем несуществующие файлы, к примеру. Теперь мы готовы идти дальше и приступить к изучению namedtuple.

9.4. Random Remarks¶

If the same attribute name occurs in both an instance and in a class,
then attribute lookup prioritizes the instance:

>>> class Warehouse
        purpose = 'storage'
        region = 'west'

>>> w1 = Warehouse()
>>> print(w1.purpose, w1.region)
storage west
>>> w2 = Warehouse()
>>> w2.region = 'east'
>>> print(w2.purpose, w2.region)
storage east

Data attributes may be referenced by methods as well as by ordinary users
(“clients”) of an object. In other words, classes are not usable to implement
pure abstract data types. In fact, nothing in Python makes it possible to
enforce data hiding — it is all based upon convention. (On the other hand,
the Python implementation, written in C, can completely hide implementation
details and control access to an object if necessary; this can be used by
extensions to Python written in C.)

Clients should use data attributes with care — clients may mess up invariants
maintained by the methods by stamping on their data attributes. Note that
clients may add data attributes of their own to an instance object without
affecting the validity of the methods, as long as name conflicts are avoided —
again, a naming convention can save a lot of headaches here.

There is no shorthand for referencing data attributes (or other methods!) from
within methods. I find that this actually increases the readability of methods:
there is no chance of confusing local variables and instance variables when
glancing through a method.

Often, the first argument of a method is called . This is nothing more
than a convention: the name has absolutely no special meaning to
Python. Note, however, that by not following the convention your code may be
less readable to other Python programmers, and it is also conceivable that a
class browser program might be written that relies upon such a convention.

Any function object that is a class attribute defines a method for instances of
that class. It is not necessary that the function definition is textually
enclosed in the class definition: assigning a function object to a local
variable in the class is also ok. For example:

# Function defined outside the class
def f1(self, x, y):
    return min(x, x+y)

class C
    f = f1

    def g(self):
        return 'hello world'

    h = g

Now , and are all attributes of class that refer to
function objects, and consequently they are all methods of instances of
— being exactly equivalent to . Note that this practice
usually only serves to confuse the reader of a program.

Methods may call other methods by using method attributes of the
argument:

class Bag
    def __init__(self):
        self.data = []

    def add(self, x):
        self.data.append(x)

    def addtwice(self, x):
        self.add(x)
        self.add(x)

Methods may reference global names in the same way as ordinary functions. The
global scope associated with a method is the module containing its
definition. (A class is never used as a global scope.) While one
rarely encounters a good reason for using global data in a method, there are
many legitimate uses of the global scope: for one thing, functions and modules
imported into the global scope can be used by methods, as well as functions and
classes defined in it. Usually, the class containing the method is itself
defined in this global scope, and in the next section we’ll find some good
reasons why a method would want to reference its own class.

Представление

Представление объекта — это значимое строковое представление объекта, которое очень полезно при отладке.

Представление объектов Python по умолчанию не особо понятно и читабельно, обычно это что типа такого object at 0x7ff395b2ccc0:

class Number:
    def __init__(self, val = 0):
    self.val = val
 
>>> a = Number(1)
>>> a
>>> <__main__.Number object at 0x7ff395b2ccc0>

Это не дает нам понимание о полезности объекта и приводит к сложности при отладки.

Значимое представление может быть реализовано путем определения метода __repr__ в определении класса.

def __repr__(self):
    return self.val

Теперь мы получаем читаемое представление объекта:

>>> a = Number(1)
>>> a
>>> 1

dataclass автоматически добавляет функцию __repr__, поэтому нам не нужно ее реализовывать вручную.

@dataclass
class Number:
    val: int = 0
>>> a = Number(1)
>>> a
>>> Number(val = 1)

Support for automatically setting __slots__?

At least for the initial release, __slots__ will not be supported.
__slots__ needs to be added at class creation time. The Data
Class decorator is called after the class is created, so in order to
add __slots__ the decorator would have to create a new class, set
__slots__, and return it. Because this behavior is somewhat
surprising, the initial version of Data Classes will not support
automatically setting __slots__. There are a number of
workarounds:

  • Manually add __slots__ in the class definition.
  • Write a function (which could be used as a decorator) that inspects
    the class using fields() and creates a new class with
    __slots__ set.

Python Objects and Classes

Python is an object oriented programming language. Unlike procedure oriented programming, where the main emphasis is on functions, object oriented programming stresses on objects.

An object is simply a collection of data (variables) and methods (functions) that act on those data. Similarly, a class is a blueprint for that object.

We can think of class as a sketch (prototype) of a house. It contains all the details about the floors, doors, windows etc. Based on these descriptions we build the house. House is the object.

As many houses can be made from a house’s blueprint, we can create many objects from a class. An object is also called an instance of a class and the process of creating this object is called instantiation.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *

Adblock
detector