Заметки по ООП в Python
Это краткий конспект по классам, который не является исчерпывающим руководством. Также я не даю определений таким понятиям как ООП, класс, объект и т.п. Все эти определения вы можете легко найти в сети.
Процедурный подход
Представьте, что вы пишите веб-сервис и перед вами встала следующая задача: "Как представить пользователя системы в программе?". Давайте договоримся, что у каждого пользователя должны быть следующие атрибуты:
- имя (username)
- адрес электронной почты (email)
- пароль (password)
Можно представить пользователя в виде нескольких переменных:
username = 'bob'
email = '[email protected]'
password = 'foobar'
У такого подхода есть несколько недостатков:
- Мы пониманием, что переменные логически должны быть связаны между собой, но мы эту связь никак не показали. Другими словами, переменная
username
может содержать имя одного пользователя,email
относиться ко второму пользователю, аpassword
к третьему. - Если нам необходимо одновременно взаимодействовать с несколькими пользователями, то для каждого из них нужно создавать по три таких переменных.
Как показать, что существует логическая связь между этими переменными? Мы можем использовать любую подходящую структуру: список, словарь, кортеж, именованный кортеж. Как пример будем использовать словарь (именованный кортеж, т.е. namedtuple
, было бы использовать нечестно):
# Представление отдельно взятого пользователя
user = {
'username': 'bob',
'email': '[email protected]',
'password': 'foobar'
}
# А так мы теперь можем представить список пользователей
users_list = [
{'username': 'bob', 'email': '[email protected]', 'password': 'foobar'},
{'username':'joe', 'email': '[email protected]', 'password': 'barfoo'},
]
Таким образом, объединив несколько значений (имя, адрес электронной почты и пароль) в контейнер, мы попытались показать, что существует логическая связь между этими значениями.
Теперь напишем простую функцию, которая возвращает имя пользователя:
def get_username(user):
return user['username']
>>> get_username(user)
'bob'
Так как пользователь представлен словарем, то мы возвращаем значение по ключу username
, которое соответствует имени.
Одним из недостатков такого подхода является то, что мы не показали логическую связь между данными о пользователе и той функцией, которая должна с ними работать.
В решении этой проблемы нам могут помочь классы, задача которых объединить данные и методы работы с ними.
Создание простого класса
Начнем с создания простого класса:
class User:
pass
Теперь создадим новый объект класса пользователь:
>>> u = User()
Атрибуты хранятся в специальном словаре (подробнее про модель данных в Python можно почитать тут):
>>> u.__dict__
{}
Так как мы не создали еще ни одного атрибута, то и словарь будет пустым. Давайте добавим несколько атрибутов (Python позволяет динамически привязывать новые атрибуты к объекту, в конце концов это просто словарь):
>>> u.username = 'bob'
>>> u.password = '[email protected]'
>>> u.email = 'foobar'
>>> u.__dict__
{'username': 'bob', 'password': '[email protected]', 'email': 'foobar'}
# Следующие два выражения в нашем примере эквиваленты
>>> u.username
'bob'
>>> u.__dict__['username']
'bob'
Что будет, если мы обратимся к атрибуту, которого не существует?
>>> u.created_at
Traceback (most recent call last):
File "<input>", line 1, in <module>
u.created_at
AttributeError: 'User' object has no attribute 'created_at'
Работать с атрибутами можно с помощью следующих функций:
hasattr(obj, attr_name)
- проверить наличие атрибутаattr_name
в объектеobj
. Если атрибут присутствует, то функция возвращаетTrue
, иначеFalse
.getattr(obj, attr_name[, default_value])
- получить значение атрибута. Можно указать значение по умолчаниюdefault_value
, которое будет возвращено, если атрибута не существует.setattr(obj, attr_name, value)
- изменить значение атрибутаattr_name
наvalue
. Если атрибут не существовал, то он будет создан.
>>> hasattr(u, 'created_at')
False
>>> hasattr(u, 'username')
True
>>> getattr(u, 'created_at')
Traceback (most recent call last):
File "<input>", line 1, in <module>
getattr(u, 'created_at')
AttributeError: 'User' object has no attribute 'created_at'
import datetime
>>> getattr(u, 'created_at', datetime.datetime.now())
datetime.datetime(2017, 4, 11, 16, 45, 36, 757869)
>>> setattr(u, 'created_at', datetime.datetime.now())
datetime.datetime(2017, 4, 11, 16, 45, 36, 757869)
Функция setattr
может оказаться полезной, когда нам необходимо добавить в объект множество атрибутов, хранящихся в каком-нибудь контейнере:
u = User()
attrs = {'username': 'bob', 'email': '[email protected]', 'password': 'foobar'}
for k, v in attrs.items():
setattr(u, k, v)
Добавим теперь функцию получения имени пользователя в ранее созданный объект:
def get_username(user):
return user.username
>>> u.get_username = get_username
>>> u.__dict__
{'username': 'bob', 'password': '[email protected]', 'email': 'foobar', 'get_username': <function get_username at 0x1038256a8>}
>>> u.get_username(u)
'bob'
Обратите внимание, что мы вызываем функцию get_username
у объекта u
и в качестве аргумента передаем сам объект u
. Выглядит странно и некрасиво.
>>> from types import MethodType
>>> u.get_username = MethodType(get_username, u)
>>> u.get_username()
'bob'
>>> u.__dict__
{'username': 'bob', 'password': '[email protected]', 'email': 'foobar', 'get_username': <bound method get_username of <__console__.U
ser object at 0x103839d30>>}
Так как каждый объект класса Пользователь должен содержать одинаковый набор атрибутов, но с разными значениями, то было бы логичным вынести процесс создания атрибутов в отдельный метод. В Python таким методом является __init__
:
class User:
def __init__(self, username, email, password):
self.username = username
self.email = email
self.password = password
def get_username(self):
return self.username
>>> u = User('bob', '[email protected]', 'foobar')
>>> u.email
'[email protected]'
>>> u.get_username()
'bob'
Возникает вопрос "Откуда взялся self
в качестве первого аргумента у метода __init__()
?". Упрощенно процесс создания и инициализации нового объекта можно описать следующими шагами:
u = User('bob', '[email protected]', 'foobar')
- Вызывается конструктор объекта
__new__()
, который возвращает "пустой" объект. - Созданный объект передается в инициализатор
__init__()
в качестве первого аргумента с именемself
(такое имя не является обязательным, но используется по соглашению), за ним передаются все остальные аргументы указанные при вызове класса ('bob', '[email protected]', 'foobar'
). - У объекта создаются все требуемые атрибуты, например
self.username = username
. - Инициализированный объект возвращается на место вызова класса, в примере переменная
u
связывается с созданным объектом.
Вы заметили, что в метод get_username()
также передается self
? А также то, что при вызове этого метода мы не передаем никаких аргументов? Давайте рассмотрим два следующих выражения:
>>> u.get_username()
'bob'
>>> User.get_username(u)
'bob'
В Python классы также являются объектами (в Python все является объектом) и методы, в отличие от атрибутов, принадлежат классу, а не объекту. Поэтому метод get_username()
вызывается не у объекта, а у класса, а объект передается в качестве аргумента (отсюда следует, что объект должен передаваться в качестве первого аргумента во все методы класса и по соглашению его называют self
). Таким образом, выражение u.get_username()
является просто синтаксическим сахаром (упрощенной формой записи) по отношению к User.get_username(u)
.
>>> User.get_username
< function User.get_username at 0x103a06268 >
>>> u.get_username
< bound method User.get_username of < __console__.User object at 0x1039a8eb8>>
В первом случае мы имеем функцию, а во втором - связный метод, другими словами есть разница в поведении в зависимости от того вызываем мы
get_username
у класса или у объекта. Такое поведение реализовано с помощью дескрипторов, о которых мы будем говорить ниже, а пока можно почитать вот эту интересную статью.
Пример: создание простой ORM
Что такое ORM? Вот пояснение с сайта Full Stack Python:
An object-relational mapper (ORM) is a code library that automates the transfer of data stored in relational databases tables into objects that are more commonly used in application code.
В этом примере (полностью основанном на этом коде) мы рассмотрим пример создания примитивной ORM для SQLite базы данных, которая имеет встроенную поддержку в Python.
Создадим БД с таблицей Пользователи и добавим туда несколько записей:
import sqlite3
# Создание нового соединения с БД
conn = sqlite3.connect('users_db.sqlite3')
# Курсор это объект, который позволяет выполнять запросы к БД
cursor = conn.cursor()
# Создание таблицы пользователей
cursor.execute('CREATE TABLE users (id, username, email, password)')
# Добавление новых записей
users = [
(1, 'john', '[email protected]', 'foobar'),
(2, 'paul', '[email protected]', 'barfoo'),
(3, 'ringo', '[email protected]', 'foobaz'),
(4, 'george', '[email protected]', 'bazfoo')
]
cursor.executemany('INSERT INTO users VALUES (?,?,?,?)', users)
conn.commit()
# Вывод всех записей
for row in cursor.execute('SELECT * FROM users'):
print(row)
В результате вы должны увидеть следующие записи:
(1, 'john', '[email protected]', 'foobar')
(2, 'paul', '[email protected]', 'barfoo')
(3, 'ringo', '[email protected]', 'foobaz')
(4, 'george', '[email protected]', 'bazfoo')
Теперь перейдем к ORM:
import sqlite3
class DataBase:
def __init__(self, db='db'):
self.conn = sqlite3.connect(f"{db}.sqlite3")
self.cursor = self.conn.cursor()
def get_columns(self, tbl_name):
self.sql_rows = f"SELECT * FROM {tbl_name}"
columns = f"PRAGMA table_info({tbl_name})"
self.cursor.execute(columns)
return [row[1] for row in self.cursor.fetchall()]
def Table(self, tbl_name):
columns = self.get_columns(tbl_name)
return Query(self.cursor, self.sql_rows, columns, tbl_name)
class Query:
def __init__(self, cursor, rows, columns, tbl_name):
self.cursor = cursor
self.sql_rows = rows
self.columns = columns
self.tbl_name = tbl_name
def filter(self, criteria):
key_word = "AND" if "WHERE" in self.sql_rows else "WHERE"
sql = f"{self.sql_rows} {key_word} {criteria}"
return Query(self.cursor, sql, self.columns, self.tbl_name)
def order_by(self, criteria):
return Query(self.cursor, f"{self.sql_rows} ORDER BY {criteria}", self.columns, self.tbl_name)
def group_by(self, criteria):
return Query(self.cursor, f"{self.sql_rows} GROUP BY {criteria}", self.columns, self.tbl_name)
@property
def rows(self):
print(self.sql_rows)
self.cursor.execute(self.sql_rows)
return [Row(zip(self.columns, fields), self.tbl_name) for fields in self.cursor.fetchall()]
class Row:
def __init__(self, fields, table_name):
self.__class__.__name__ = table_name + "_Row"
for name, value in fields:
setattr(self, name, value)
def __repr__(self):
attrs = ', '.join([f"{attr}={value}" for attr, value in self.__dict__.items()])
return f"{self.__class__.__name__}({attrs})"
Класс DataBase
отвечает за создание соединения, класс Query
за формирование запроса к БД, класс Row
представляет одну запись в таблице.
Метод
__repr__
переопределен, чтобы выводить чуть больше полезной информации об объекте, чем просто его адрес в памяти. Узнать больше про магические методы в питоне можно прочитав статью на Хабре.
Ниже приведен пример использования:
>>> db = DataBase('users_db')
>>> db.get_columns('users')
['id', 'username', 'email', 'password']
>>> db.Table('users').rows:
SELECT * FROM users
[users_Row(id=1, username=john, [email protected], password=foobar),
users_Row(id=2, username=paul, [email protected], password=barfoo),
users_Row(id=3, username=ringo, [email protected], password=foobaz),
users_Row(id=4, username=george, [email protected], password=bazfoo)]
>>> db.Table('users').filter('id > 2').rows
SELECT * FROM users WHERE id > 2
[users_Row(id=3, username=ringo, [email protected], password=foobaz),
users_Row(id=4, username=george, [email protected], password=bazfoo)]
>>> db.Table('users').order_by('username DESC').rows
SELECT * FROM users ORDER BY username DESC
[users_Row(id=3, username=ringo, [email protected], password=foobaz),
users_Row(id=2, username=paul, [email protected], password=barfoo),
users_Row(id=1, username=john, [email protected], password=foobar),
users_Row(id=4, username=george, [email protected], password=bazfoo)]
>>> user = db.Table('users').rows[0]
SELECT * FROM users
>>> user.id
1
>>> user.username
'john'
Задания:
- Вы должны были заметить, что мы получаем объекты класса
users_Row
, а не классаUser
. Попробуйте внести изменения, чтобы мы получали объекты классаUser
:>>> user = db.Table('users').rows[0] >>> type(user) <class '__main__.User'> >>> user.get_username() 'john'
- Добавьте метод
limit(N)
в классDataBase
, который позволяет получить не больше N записей. - Добавьте метод
insert(obj)
, который создает в БД новую запись об объектеobj
.
"Приватные" поля класса
Допустим у нас имеется следующее определение класса, в котором есть методы для изменения и проверки пароля, а также адреса электронной почты:
import hashlib
import random
import string
import re
class User:
def __init__(self, username, email, password):
self.username = username
self.set_email(email)
self.set_password(password)
def set_email(self, email):
match = re.match('^[_a-z0-9-]+(\.[_a-z0-9-]+)*@[a-z0-9-]+(\.[a-z0-9-]+)*(\.[a-z]{2,4})$', email)
if not match:
raise ValueError("Invalid email")
self.email = email
# http://pythoncentral.io/hashing-strings-with-python/
# https://docs.python.org/3.5/library/hashlib.html
def make_salt(self):
salt = ""
for i in range(5):
salt = salt + random.choice(string.ascii_letters)
return salt
def set_password(self, pw, salt=None):
if salt == None:
salt = self.make_salt()
self.password = hashlib.sha256(pw.encode() + salt.encode()).hexdigest() + "," + salt
def check_password(self, user_password):
password, salt = self.password.split(',')
return password == hashlib.sha256(user_password.encode() + salt.encode()).hexdigest()
Давайте посмотрим на работу с этими методами:
>>> u = User('bob', '[email protected]', 'foobar')
>>> u.username
'bob'
>>> u.email
'[email protected]'
>>> u.password
'fd6c85ddaccb0d13e8b4d45b6e3bcbcc18d3b737a10aef21d297c861d770da6d,PDrGK'
>>> u.set_email('bob-new-email')
Traceback (most recent call last):
File "<input>", line 1, in <module>
u.set_email('bob-new-eamil')
File "<input>", line 12, in set_email
raise ValueError("Invalid email")
ValueError: Invalid email
>>> u.set_email('[email protected]')
>>> u.email
'[email protected]'
>>> u.check_password('barfoo')
False
>>> u.check_password('foobar')
True
Допустим, что мы хотим изменить пароль (или адрес электронной почты) и делаем это напрямую обращаясь к атрибуту:
>>> u.password = 'barfoo'
>>> u.check_password('barfoo')
False
Почему пароль не прошел проверку? Мы изменили значение атрибута напрямую, не используя функцию set_password()
, таким образом, мы сохранили пароль в открытом виде. В свою очередь функция check_password()
хеширует переданный ей пароль в качестве аргумента и затем сравнивает его с паролем, который хранился в атрибуте password
.
Очевидно, что нужно менять значение пароля или адреса электронной почты с помощью методов set_password()
и set_email()
, чтобы избежать подобного рода ошибок. А прямое обращение к полям password
и email
нужно ограничить.
В языке Python сложно что-то запретить, в частности обращение к полям класса, но есть соглашения. Например, если имя атрибута начинается с одного нижнего подчеркивания, то он считается (считается != является) приватным (private), другими словами, указывая нижнее подчеркивание перед именем атрибута мы "говорим" пользователям нашего класса "Не нужно обращаться к этому полю напрямую, иначе можно нарушить логику работы". Таким образом, в примере приведенном выше, имена всех атрибутов (пароль, адрес электронной почты и имя пользователя) следовало бы начинать с нижнего подчеркивания, а для взаимодействия с ними использовать соответствующие методы, которые обычно называют "сеттерами" и "геттерами" (от set/get, например, set_username
и get_username
).
Свойства (property)
Давайте рассмотрим следующий пример: пусть у нас есть класс "Профиль пользователя" и поле дата рождения,
class UserProfile:
def __init__(self, user, first_name='', sur_name='', bdate=None):
assert isinstance(user, User), '`user` field must be a User class instance'
self._user = user
self.first_name = first_name
self.sur_name = sur_name
self.bdate = bdate
self._age = None
self._age_last_recalculated = None
self._recalculate_age()
def _recalculate_age(self):
today = datetime.date.today()
age = today.year - self.bdate.year
if today < datetime.date(today.year, self.bdate.month, self.bdate.day):
age -= 1
self._age = age
self._age_last_recalculated = today
def age(self):
if (datetime.date.today() > self._age_last_recalculated):
self._recalculate_age()
return self._age
class User:
def __init__(self, username, email, password):
...
self.profile = UserProfile(self)
...
class UserProfile:
...
@property
def age(self):
if (datetime.date.today() > self._age_last_recalculated):
self._recalculate_age()
return self._age
class UserProfile:
...
@property
def fullname(self):
return '{} {}'.format(self.first_name, self.sur_name).title()
@fullname.setter
def fullname(self, value):
name, surname = value.split(" ", maxsplit=1)
self.first_name = name
self.sur_name = surname
@fullname.deleter
def fullname(self):
self.first_name = ''
self.sur_name = ''
Дескрипторы
Методы класса (@classmethod и @staticmethod)
Наследование
class TimestampedModel:
def __init__(self, created_at=None, updated_at=None):
self.created_at = created_at or datetime.datetime.now()
self.updated_at = updated_at or created_at
def update(self):
self.updated_at = datetime.datetime.now()
class User(TimestampedModel):
def __init__(self, username, email, password):
self.username = username
self.email = email
self.password = password
class User(TimestampedModel):
def __init__(self, username, email, password):
self.username = username
self.email = email
self.password = password
super().__init__()
@property
def username(self):
return self._username
@username.setter
def username(self, new_username):
self._username = new_username
self.update()
import json
import attr
import enum
import datetime
class JSONSerializerMixin:
@staticmethod
def to_serializable(value):
if isinstance(value, datetime.datetime):
return value.isoformat() + "Z"
elif isinstance(value, enum.Enum):
return value.value
elif attr.has(value.__class__):
return attr.asdict(value)
elif isinstance(value, Exception):
return {
"error": value.__class__.__name__,
"args": value.args,
}
return str(value)
def toJSON(self):
return json.dumps(self.__dict__, default=JSONSerializerMixin.to_serializable)
@classmethod
def fromJSON(cls, data):
def datetime_parser(json_dict):
for k,v in json_dict.items():
try:
json_dict[k] = datetime.datetime.strptime(v, "%Y-%m-%d").date()
except:
pass
return json_dict
return cls(**json.loads(data, object_hook=datetime_parser))
class User(TimestampedModel, JSONSerializerMixin):
...
Множественное наследование и полиморфизм
Метаклассы
Таким же образом, как классы контролируют создание экземпляров и позволяют задавать поведение методов, метаклассы в Python могут делать все это для классов. Понять, что такое метаклассы, можно определив их как классы классов.
Самый часто используемый метакласс - type
т.к. это метакласс для всех классов по умолчанию, все остальные метаклассы должны наследоваться от него.
- Тип type
В Python всё является объектом и должно иметь тип. Вкратце, тип - это сущность, которая знает как создать экземпляр. Например, тип числа 1
- Int
, а тип Int
- type
>>> type(1)
<class 'int'>
>>> type(int)
<class 'type'>
Существует модуль types
, который предоставляет стандартный набор функций для работы с типами и определения типов, которые интерпретатор использует по умолчанию. Это может оказаться кстати, например, если вам нужно проверить является ли данный объект функцией или модулем.
>>> import types
>>> def func():
... pass
>>> isinstance(func, types.FunctionType)
True
>>> isinstance(func, types.ModuleType)
False
Теперь, когда type
тоже является объектом, какой тогда его тип? Получается, что типом типа type
является... type
:
>>> type(type)
<class 'type'>
Все создаваемые пользователем новые классы (new-style classes
) также имеют тип type
:
>>> class A(object):
... pass
>>> type(A)
<class 'type'>
Это происходит с 3 версии Python, однако в случае использования Python 2 вам необходимо помнить, что все классы следует наследовать от object
. Иначе, они будут иметь тип classobj
:
# Python 2.x
>>> class B:
... pass
>>> type(B)
<type 'classobj'>
В конце концов, если вызвать type
с тремя аргументами, он будет вести себя как конструктор - так вы сможете создавать типы "на лету". Этими аргументами являются имя нового типа, кортеж родительских классов и словарь класса (который содержит все то, что вы обычно размещаете в теле класса)
class A(object):
pass
def f(self, x):
return x + 1
Type = type('Type', (A,), {'x': 42, 'f': f})
instance = Type()
>>> issubclass(Type, A)
True
>>> isinstance(instance, Type)
True
>>> Type.x
42
>>> instance.f(1)
2
Мы вернёмся к этому позже, когда будем разбирать конструктор метаклассов.
- Связываем метаклассы
Так как все метаклассы наследуются от type
, самая простая реализация метакласса выглядит следующим образом:
class M(type):
pass
Теперь любой класс, чьим метаклассом является M
, будет иметь тип M
(но также являться экземпляром типа type
, потому что M
наследуется от type
)
Как мы можем связать метакласс и класс? Увы, это ещё одна вещь, реализованная по-разному в Python 2 и Python 3.
В Python 2, метакласс устанавливается в специальное поле класса - __metaclass__
# Python 2.x only
class A(object):
__metaclass__ = M
В Python 3 метакласс передаётся в качестве аргумента:
# Python 3.x only
class A(object, metaclass=M):
pass
Одним из способов унифицировать этот процесс является использование библиотеки six
, предоставляющей единый способ связывания метаклассов с основным классом:
# Python 2 and 3
import six
# metaclass
class M(type):
pass
# base class
class A(object):
pass
class B(six.with_metaclass(M, A)):
pass
@six.add_metaclass(M)
class C(A):
pass
assert issubclass(B, A)
assert type(B) is M
assert issubclass(C, A)
assert type(C) is M
- Перехватываем конструкторы типов
Как мы рассмотрели выше, сигнатура конструктора у type
следующая:
type(name, bases, classdict)
Тогда определение классов с помощью ключевого слова class
можно рассматривать как "синтаксический сахар", позволяющий не вызывать конструктор type
напрямую.
Определим 2 класса:
class A(object):
x = 1
class B(A):
y = 2
@classmethod
def f(cls, v):
return v + 1
def g(self, v):
return v + 2
Мы можем переписать код выше с использованием type
напрямую:
clsdict_a = {
'x': 1
}
A = type('A', (object,), clsdict_a)
def f(cls, v):
return v + 1
def g(self, v):
return v + 2
clsdict_b = {
'y': 2,
'f': classmethod(f),
'g': g
}
B = type('B', (A,), clsdict_b)
Всё самое интересное происходит внутри конструктора type
, а все определяемые пользователем метаклассы могут варьировать список аргументов, которые он получает. Другими словами, вы можете перехватить создание класса сразу после того, как интерпретатор считал определение класса, и ровно до того момента, когда произойдёт обращение к type
. Для этого необходимо переопределить метод __new__
.
Давайте посмотрим на пример, в котором мы хотим, чтобы во всех наследниках конкретного класса автоматически увеличивалось значение id
в случае, если track
установлен в True
в теле класса; также мы хотим хранить все отслеживаемые классы в общем поле classes
:
import six
class Meta(type):
classes = []
def __new__(meta, name, bases, clsdict):
clsdict['id'] = None
track = clsdict.pop('track', False)
clsdict['id'] = len(meta.classes) if track else None
cls = super(Meta, meta).__new__(meta, name, bases, clsdict)
if track:
meta.classes.append(cls)
return cls
@six.add_metaclass(Meta)
class Trackable(object):
pass
assert Trackable.classes == []
assert Meta.classes == []
assert Trackable.id is None
class A(Trackable):
track = True
assert A.id == 0
assert A.classes == Trackable.classes == [A]
assert not hasattr(A, 'track')
class B(A):
pass
assert B.classes == A.classes == [A]
assert B.id is None
class C(Trackable):
track = True
assert C.classes == B.classes == [A, C]
assert C.id == 1
assert Trackable.classes[C.id] is C
Сперва мы наследуем метакласс Meta
от type
, в котором переопределяем метод __new__
и создаём переменную classes
уровня класса, которая отражает текущее состояние.
Сигнатура метода __new__
совпадает с сигнатурой конструктора type
- она тоже получает имя класса, кортеж родительских типов и словарь свойств класса; она также должна вернуть полностью сформированный тип. В этом методе мы проверяем наличие поля track
в словаре класса и его значение. Если поле есть и значение установлено в True, то мы присваиваем значение id
классового уровня соответственно, иначе - None
.
В завершении, мы вызываем конструктор основного метакласса для формирования типа, при желании сохраняем его в списке отслеживаемых классов и возвращаем сформированный тип. Стоит упомянуть, что мы могли использовать метод type.__new__
вместо super(Meta, meta).__new__
, но в целом это хорошая практика избегать жёстко закодированные основные типы, которые иногда приводят к неожиданному поведению при наследовании.
Обратите внимание, что все переменные классового уровня (такие как classes
) в этом примере могут также быть доступны наследникам. Похожим образом экземпляры методов метаклассов доступны как методы класса одновременно для классов и их экземпляров.
- Метакласс как реестр
Типичный случай применения метаклассов - отслеживание созданных классов для последующего доступа к ним во время выполнения по имени или идентификатору.
import six
class RegistryMeta(type):
def __getitem__(meta, key):
return meta._registry[key]
@six.add_metaclass(RegistryMeta)
class Registry(type):
_registry = {}
def __new__(meta, name, bases, clsdict):
cls = super(Registry, meta).__new__(meta, name, bases, clsdict)
if not clsdict.pop('__base__', False):
meta._registry[name] = cls
if 'alias' in clsdict:
meta._registry[cls.alias] = cls
return cls
class Base(six.with_metaclass(Registry)):
__base__ = True
class A(Base):
pass
class B(Base):
alias = 'foo'
assert Registry['A'] is A # lookup by class name
assert Registry['B'] is B
assert Registry['foo'] is B # or by alias
# Base is not in registry
try:
Registry['Base']
except Exception as e:
assert isinstance(e, KeyError)
В примере выше, все наследники Base
будут отслеживаться метаклассом Registry
, и к ним можно будет обратиться позже по имени класса или значению поля alias
при его наличии.
Перед тем, как зарегистрировать класс, мы проверяем наличие поля __base__
, чтобы учитывать только прямых потомков Base
.
Для того, чтобы сделать класс Registry
индексируемым, мы должны связать метакласс RegistryMeta
с ним, ведь в нем мы реализовали "магический" метод __getitem__
. Насколько страшно звучит фраза "метакласс метакласса", настолько же мы в действительности следуем такой логике, реализуя метод для экземпляров в теле класса. Если вам нужен экземпляр для чего-либо, вы реализуете методы в его типе, дабы последний знал, как связать реализованную функциональность с объектом во время его создания.
Подытожим иерархию метакласса следующим примером:
a = A()
assert type(a) is A
assert type(A) is Registry
assert type(Registry) is RegistryMeta
assert type(RegistryMeta) is type
- Singleton-паттерн
Использование глобальных объектов - априори плохая затея, но если вы твердо решили идти этим путем, вам следует убедиться, что это Singleton, в котором существует один единственный экземпляр какого-либо класса в любой момент выполнения, от которого невозможно наследоваться.
Как мы уже убедились, если метакласс связан с основным классом, всякий раз, когда от основного класса происходит наследование, вызывается метод __new__
. Это позволяет с легкостью обрабатывать неперехватываемую часть. Если по какой-либо причине вы хотите предотвратить возможность наследования от метакласса, вам следует реализовать метакласс для метаклассов.
Когда речь идёт о контролировании создаваемых экземпляров метакласса, переопределение метода __new__
больше недостаточно: вместо него мы должны переопределить __call__
. Легко запомнить: __new__
срабатывает при создании нового класса, __call__
- при создании нового экземпляра класса. Это работает точно так же и в случае реализации вышеуказанных методов в обычных классах. Таким образом экземпляры этих классов можно будет вызывать.
Чтобы разобраться, как эти методы работают в связке, взглянем на следующий фрагмент:
from __future__ import print_function
import six
class M(type):
def __new__(meta, *a, **kw):
print('metaclass::new')
return super(M, meta).__new__(meta, *a, **kw)
def __call__(cls, *a, **kw):
print('metaclass::call', a, kw)
return super(M, cls).__call__(*a, **kw)
print('---')
class C(six.with_metaclass(M)):
def __new__(cls, *a, **kw):
print('class::new', a, kw)
return super(C, cls).__new__(cls, *a, **kw)
def __init__(self, *a, **kw):
print('class::init', a, kw)
def __call__(self, *a, **kw):
print('class::call', a, kw)
print('---')
instance = C('foo', x=1)
print('---')
instance('bar', y=2)
Вывод будет следующим:
metaclass::new
---
metaclass::call ('foo',) {'x': 1}
class::new ('foo',) {'x': 1}
class::init ('foo',) {'x': 1}
---
class::call ('bar',) {'y': 2}
Возвращаясь к singleton-классу, мы должны переопределить __call__
в метаклассе для перехвата конструктора экземпляров и возвращения существующих экземпляров, которые могли быть ранее сохранены прямо в классе. Если экземпляр не существует, мы можем создать его, вызвав super
, который в свою очередь вызовет конструктор класса, если он определён.
import six
class Singleton(type):
def __new__(meta, name, bases, clsdict):
if any(isinstance(cls, meta) for cls in bases):
raise TypeError('Cannot inherit from singleton class')
clsdict['_instance'] = None
return super(Singleton, meta).__new__(meta, name, bases, clsdict)
def __call__(cls, *args, **kwargs):
if not isinstance(cls._instance, cls):
cls._instance = super(Singleton, cls).__call__(*args, **kwargs)
return cls._instance
@six.add_metaclass(Singleton)
class A(object):
pass
a = A()
b = A()
assert a is b # all new instances point to the same object
try:
class B(A):
pass
except Exception as e:
assert isinstance(e, TypeError) # cannot inherit from singleton
Реализация весьма проста и прозрачна, однако здесь ещё есть место для улучшений. К примеру, __call__
обрабатывает полученные аргументы только в первый раз; во всех остальных вызовах он лишь безоговорочно возвращает сохранённое значение.
- Генерируем дескрипторы
Ещё один частый случай использования метаклассов - автоматизация создания дескрипторов. В Python дескриптором является любой объект, который реализует хотя бы один из следующих методов: __get__
, __set__
или __delete__
Дескрипторы контролируют доступ к свойствам, а также задают поведение для получения, установки или удаления свойства из словаря объекта. Рассмотрим простую реализацию:
class Descriptor(object):
def __init__(self, name):
self.name = name
def __get__(self, instance, cls=None):
return instance.__dict__[self.name]
def __set__(self, instance, value):
instance.__dict__[self.name] = value
def __delete__(self, instance):
del instance.__dict__[self.name]
class A(object):
x = Descriptor('x')
a = A()
assert not hasattr(a, 'x')
a.x = 1
assert a.x == 1
del a.x
assert not hasattr(a, 'x')
Это может выглядеть как излишество, так как мы бы могли напрямую работать с свойствами экземпляров. Тем не менее, дескрипторы полезны в случаях, когда мы хотим добавить дополнительную логику в один из методов get, set или delete. Например, мы можем полностью переписать встроенные свойства/методы с использованием дескрипторов.
Зачем нам нужно было передавать строку x
в Descriptor
явно? Ответ крайне прост - с одной стороны, дескриптор должен знать имя атрибута для поиска в словаре объекта, с другой стороны - на момент инициализации он не может ссылаться на словарь объекта, потому что тот пока что не существует. Так, например, в момент присваивания a = A()
невозможно сообщить классу A
о том, куда его экземпляр будет присвоен.
Здесь в дело вступают метаклассы. Как раз до того, как будет создан тип, у нас есть полный словарь класса для наших нужд, и мы можем заменять конкретные поля, используя дескрипторы.
В следующем примере мы создаём основной класс, наследники которого могут использовать специальный синтаксис для генерации свойств типа с значениями по умолчанию.
class A(Typed):
x = int
y = str, 'foo'
Мы хотим, чтобы x
был числовым свойством с стандартным значением None
, а y
- строкой со стандартным значением foo
. Когда значения по умолчанию присвоены, оба свойства попытаются привести установленные значения к своему типу.
Возможная реализация дескриптора:
import six
class Descriptor(object):
def __init__(self, name, cls, default=None):
self.name = name
self.cls = cls
self.default = default
def __set__(self, instance, value):
# convert the value to `cls` and write to instance dict
instance.__dict__[self.name] = self.cls(value)
def __get__(self, instance, cls):
# retrieve the value from instance dict
return instance.__dict__.get(self.name, self.default)
class Meta(type):
_types = [int, str]
def __new__(meta, name, bases, clsdict):
for k, v in clsdict.copy().items():
if v in meta._types:
# a type with no default
clsdict[k] = Descriptor(k, v)
elif isinstance(v, tuple):
if len(v) == 2 and v[0] in meta._types:
# a type and a default value
clsdict[k] = Descriptor(k, v[0], v[1])
return super(Meta, meta).__new__(meta, name, bases, clsdict)
class Typed(six.with_metaclass(Meta)):
pass
class A(Typed):
x = int
y = str, 'foo'
a = A()
assert a.x is None
a.x = '42'
assert a.x == 42
assert a.y == 'foo'
a.y = 42
assert a.y == '42'
Заметьте, что мы до сих пор должны передавать имена полей в Descriptor
, но на этот раз это было сделано метаклассом автоматически, а имена свойств попросту соответствовали ключам в словаре. Это основное преимущество использования метаклассов - реализовывать всю тяжёлую работу в метаклассе, чтобы пользовательский код выглядел проще.
Источник: http://ivansmirnov.io/python-metaclasses/ Статья может быть доступна по ссылке http://archive.li/35l34
Абстрактные классы
Продолжим рассматривать пример с классом Пользователь. Мы можем разделить пользователей на два типа: анонимные пользователи, то есть те пользователи, которые не зарегистрированы или не вошли в систему под своим логином и паролем, и авторизованные пользователи.
class AbstractUser:
def set_password(self, raw_password):
raise NotImplementedError()
def check_password(self, raw_password):
raise NotImplementedError()
def is_anonymous(self):
raise NotImplementedError()
class AnonymousUser(AbstractUser):
def is_anonymous(self):
return True
class User(AbstractUser):
def set_password(self, raw_password):
...
def check_password(self, raw_password):
...
def is_anonymous(self):
return False
from abc import ABCMeta, abstractmethod
class AbstractUser(metaclass=ABCMeta):
def set_password(self, raw_password):
raise NotImplementedError()
def check_password(self, raw_password):
raise NotImplementedError()
@abstractmethod
def is_anonymous(self):
pass