Tinkoff Python
Episode 3
Что скрывает python?
(cpython, memory, gc)
52645-22327
Homework
В предыдущей серии...
Collections
namedtuple
>>> Point = namedtuple('Point', ['x', 'y'])
>>> p = Point(11, y=22) # instantiate with positional or keyword arguments
>>> p[0] + p[1] # indexable like the plain tuple (11, 22)
33
>>> x, y = p # unpack like a regular tuple
>>> x, y
(11, 22)
>>> p.x + p.y # fields also accessible by name
33
>>> p # readable __repr__ with a name=value style
Point(x=11, y=22)
defaultdict
>>> s = [('yellow', 1), ('blue', 2), ('yellow', 3), ('blue', 4), ('red', 1)]
>>> d = defaultdict(list)
>>> for k, v in s:
... d[k].append(v)
...
>>> sorted(d.items())
[('blue', [2, 4]), ('red', [1]), ('yellow', [1, 3])]
etc.
- deque list-like container with fast appends and pops on either end
- ChainMap dict-like class for creating a single view of multiple mappings
- Counter dict subclass for counting hashable objects
- OrderedDict dict subclass that remembers the order entries were added
- UserDict wrapper around dictionary objects for easier dict subclassing
- UserList wrapper around list objects for easier list subclassing
- UserString wrapper around string objects for easier string subclassing
О чем будем говорить?
- Какой путь проходит наш код внутри интерпретатора
- объектная система
- работа с памятью
- сборка мусора
Зачем нам это знать?
Проще диагностировать проблемы
Меньше магии
Всегда знаем куда пойти посмотреть
![](https://s3.amazonaws.com/media-p.slid.es/uploads/995652/images/5799350/pasted-from-clipboard.png)
CPython 3.7
![](https://s3.amazonaws.com/media-p.slid.es/uploads/995652/images/5797888/pasted-from-clipboard.png)
Что есть что в репо?
- Grammar - граматика питона
- Include - .h файлы для C кода (здесь же объявляются почти все основные структуры)
- Lib - стандартная библиотека на питоне
- Modules - стандартная библиотека на C
- Objects - объектная система питона, встроенные структуры данных
- Parser - парсинг исходного кода (до ast)
- Python - интерпретатор, компилятор байт кода
Что происходит на самом деле?
$ python my_code.py
code.py
tokenizer
parser
compiler
VM
![](https://s3.amazonaws.com/media-p.slid.es/uploads/995652/images/5802154/pasted-from-clipboard.png)
$ echo "print('Hello world')" > my_code.py
$ python3 -m tokenize my_code.py
0,0-0,0: ENCODING 'utf-8'
1,0-1,5: NAME 'print'
1,5-1,6: OP '('
1,6-1,19: STRING "'Hello world'"
1,19-1,20: OP ')'
1,20-1,21: NEWLINE '\n'
2,0-2,0: ENDMARKER ''
Разбиваем код на токены
Можно получить ошибки несоответствия грамматике
...
comp_op: '<'|'>'|'=='|'>='|'<='|'<>'|'!='|'in'|'not' 'in'|'is'|'is' 'not'
star_expr: '*' expr
expr: xor_expr ('|' xor_expr)*
xor_expr: and_expr ('^' and_expr)*
and_expr: shift_expr ('&' shift_expr)*
shift_expr: arith_expr (('<<'|'>>') arith_expr)*
arith_expr: term (('+'|'-') term)*
...
Грамматика языка
Строим AST
![](https://s3.amazonaws.com/media-p.slid.es/uploads/995652/images/5798411/pasted-from-clipboard.png)
Можно получить синтаксические ошибки
Строим AST
In [0]: import ast
In [1]: tree = ast.parse('print("Hello world")')
In [2]: tree
Out[2]: <_ast.Module at 0x10a3ba0b8>
Компилируем в байт-код
Получаем набор низкоуровневых команд (opcode) для последующей интерпретации
Зачем?
- Компактное представление
- Ограниченный набор команд
- Можно сразу интерпретировать без дополнительных шагов
Компилируем в байт-код
In [1]: import dis
In [2]: def x():
...: a = 2 ** 3
...: return a
...:
...:
In [3]: dis.dis(x)
2 0 LOAD_CONST 1 (8)
2 STORE_FAST 0 (a)
3 4 LOAD_FAST 0 (a)
6 RETURN_VALUE
In [4]: x.__code__.co_code
Out[4]: b'd\x01}\x00|\x00S\x00'
Байт код кэшируется в .pyc файлах
Затем наш байт-код отдается на выполнение VM
Как это выглядит в коде?
#ifdef MS_WINDOWS
int
wmain(int argc, wchar_t **argv)
{
return Py_Main(argc, argv);
}
#else
int
main(int argc, char **argv)
{
return _Py_UnixMain(argc, argv);
}
#endif
Точка входа
static int
pymain_main(_PyMain *pymain)
{
PyInterpreterState *interp;
pymain_init(pymain, &interp);
// ...
pymain_run_python(pymain, interp) // выполнение кода
// ...
pymain_free(pymain);
// ... обработка ошибок, сигналов
return pymain->status;
}
pymain_main
static int
pymain_run_python(_PyMain *pymain, PyInterpreterState *interp)
{
// ... проверяем настройки
// в зависимости от способа запуска
if (pymain->filename != NULL) { // stdin, module, etc.
pymain_run_file(pymain, config, &cf); // наш случай
}
// ...
pymain_repl(pymain, config, &cf); # тут REPL
// ...
return res;
}
Выбираем режим выполнения
int
PyRun_SimpleFileExFlags(FILE *fp, const char *filename, int closeit,
PyCompilerFlags *flags)
{
// ...
if (maybe_pyc_file(fp, filename, ext, closeit)) {
// ...
v = run_pyc_file(pyc_fp, filename, d, d, flags);
} else {
// ...
v = PyRun_FileExFlags(fp, filename, Py_file_input, d, d,
closeit, flags);
}
// ...
return ret;
}
Проверяем наличие .pyc
PyObject *
PyRun_FileExFlags(FILE *fp, const char *filename_str, int start, PyObject *globals,
PyObject *locals, int closeit, PyCompilerFlags *flags)
{
// ...
mod = PyParser_ASTFromFileObject(fp, filename, NULL, start, 0, 0,
flags, NULL, arena); # получаем AST
// ...
ret = run_mod(mod, filename, globals, locals, flags, arena); # выполняем код
// ...
return ret;
}
Парсим файл
static PyObject *
run_mod(mod_ty mod, PyObject *filename, PyObject *globals, PyObject *locals,
PyCompilerFlags *flags, PyArena *arena)
{
PyCodeObject *co;
PyObject *v;
co = PyAST_CompileObject(mod, filename, flags, -1, arena); // получаем байт-код
if (co == NULL)
return NULL;
v = run_eval_code_obj(co, globals, locals); // выполняем байт-код
Py_DECREF(co);
return v;
}
Выполняем код
Как выполняется байт-код?
Фрэймы
Модуль
main
sort
sort_file
current frame
code, args, kwargs, const, etc.
code, args, kwargs, const, etc.
code, args, kwargs, const, etc.
Получение текущего фрэйма
In [9]: import inspect
In [10]: current_frame = inspect.currentframe()
In [11]: current_frame
Out[11]: <frame at 0x1068357a8, file '<...>', line 1, code <module>>
In [12]: current_frame.f_back
Out[12]: <frame at 0x7fa91b874df8, file './.py', line 2981, code run_code>
Стэктрейс строится из цепочки фрэймов
import traceback
traceback.print_stack()
Выполняем код фрэйма
(_PyEval_EvalFrameDefault)
Создаем фрейм и последовательно выполняем его opcode`ы
main_loop
main_loop:
for (;;) {
// ...
switch (opcode) {
case TARGET(LOAD_FAST): {
PyObject *value = GETLOCAL(oparg);
Py_INCREF(value);
PUSH(value);
FAST_DISPATCH();
}
case TARGET(LOAD_CONST): {
PREDICTED(LOAD_CONST);
PyObject *value = GETITEM(consts, oparg);
Py_INCREF(value);
PUSH(value);
FAST_DISPATCH();
}
// ...
Вызывая функцию, мы создаем новый фрейм и начинаем выполнять его
Как выполняются opcode`ы ?
Python VM - это стековая машина
Каждый фрейм имеет стек c данными, над которым совершаются операции, закодированные в opcode
case TARGET(BINARY_SUBTRACT): {
PyObject *right = POP();
PyObject *left = TOP();
PyObject *diff = PyNumber_Subtract(left, right);
Py_DECREF(right);
Py_DECREF(left);
SET_TOP(diff);
if (diff == NULL)
goto error;
DISPATCH();
}
Объектная модель питона
![](https://s3.amazonaws.com/media-p.slid.es/uploads/995652/images/5802037/pasted-from-clipboard.png)
PyObject
typedef struct _object {
Py_ssize_t ob_refcnt;
struct _typeobject *ob_type;
} PyObject;
Так выглядит float object
typedef struct {
PyObject ob_base;
double ob_fval;
} PyFloatObject;
А так function object
typedef struct {
PyObject ob_base;
PyObject *func_code; /* A code object, the __code__ attribute */
PyObject *func_globals; /* A dictionary (other mappings won't do) */
PyObject *func_defaults; /* NULL or a tuple */
PyObject *func_kwdefaults; /* NULL or a dict */
PyObject *func_closure; /* NULL or a tuple of cell objects */
PyObject *func_doc; /* The __doc__ attribute, can be anything */
PyObject *func_name; /* The __name__ attribute, a string object */
PyObject *func_dict; /* The __dict__ attribute, a dict or NULL */
PyObject *func_weakreflist; /* List of weak references */
PyObject *func_module; /* The __module__ attribute, can be anything */
PyObject *func_annotations; /* Annotations, a dict or NULL */
PyObject *func_qualname; /* The qualified name */
} PyFunctionObject;
Типы обернуты в PyObject и потому занимают больше места в памяти
Lookup problem
class X:
def method(self, y):
print(i)
x = X()
method = x.method
for i in range(1000):
method(i)
Lookup problem
class X:
def method(self, y):
print(i)
x = X()
method = x.method
for i in range(1000):
method(i)
Перерыв?
Memory management
Для объектов, которым нужно больше 512B используем malloc/free
Остальные объекты хранятся в блоках
блок - это область памяти с фиксированным размером кратным 8
Объект получает блок с минимально необходимым размером
Например: если нужно выделить 13Б, берем блок на 16Б
![](https://s3.amazonaws.com/media-p.slid.es/uploads/995652/images/5798809/pasted-from-clipboard.png)
Блоки сгруппированы в пулы (pool) по 4кБ
- Пул состоит из блоков одинакового размера (помогает избежать фрагментации)
- Пул помнит какие блоки в нем заняты, а какие нет.
- Если объект уничтожается, занимаемый им блок помечается как свободный и может быть использован заново.
pool_header
struct pool_header {
union { block *_padding;
uint count; } ref; /* number of allocated blocks */
block *freeblock; /* pool's free list head */
struct pool_header *nextpool; /* next pool of this size class */
struct pool_header *prevpool; /* previous pool "" */
uint arenaindex; /* index into arenas of base adr */
uint szidx; /* block size class index */
uint nextoffset; /* bytes to virgin block */
uint maxnextoffset; /* largest valid nextoffset */
};
![](https://s3.amazonaws.com/media-p.slid.es/uploads/995652/images/5798817/pasted-from-clipboard.png)
Пулы группируются в арены (arena) по 256кБ, каждая арена содержит в себе 64 пула
![](https://s3.amazonaws.com/media-p.slid.es/uploads/995652/images/5798837/pasted-from-clipboard.png)
struct arena_object {
uintptr_t address;
block* pool_address;
uint nfreepools;
uint ntotalpools;
struct pool_header* freepools;
struct arena_object* nextarena;
struct arena_object* prevarena;
};
arena_object
Python запрашивает память у ОС только целыми аренами, чтобы уменьшить кол-во выделений памяти
Освобождается ли память обратно?
Только если все блоки всех пулов арены пусты
![](https://s3.amazonaws.com/media-p.slid.es/uploads/995652/images/5802115/pasted-from-clipboard.png)
sys._debugmallocstats()
class size num pools blocks in use avail blocks
----- ---- --------- ------------- ------------
0 8 2 801 211
1 16 2 394 112
2 24 4 509 163
3 32 48 6024 24
4 40 99 9944 55
5 48 69 5686 110
...
# arenas allocated total = 170
...
76 arenas * 262144 bytes/arena = 19,922,944
...
Total = 19,922,944
14 free PyCFunctionObjects * 48 bytes each = 672
72 free PyDictObjects * 48 bytes each = 3,456
3 free PyFloatObjects * 24 bytes each = 72
8 free PyFrameObjects * 368 bytes each = 2,944
...
sys.getsizeof
In [1]: import sys
In [2]: sys.getsizeof([])
Out[2]: 64
In [3]: sys.getsizeof([1,2,3])
Out[3]: 88
In [4]: sys.getsizeof({})
Out[4]: 240
__dict__
In [15]: class X:
...: pass
...:
...:
In [16]: X.__dict__
Out[16]:
mappingproxy({'__module__': '__main__',
'__dict__': <attribute '__dict__' of 'X' objects>,
'__weakref__': <attribute '__weakref__' of 'X' objects>,
'__doc__': None})
In [17]: X().__dict__
Out[17]: {}
В памяти питона словари повсюду!
__slots__ позволяет сэкономить немного памяти
In [1]: class X:
...: __slots__ = ('a', )
...: def __init__(self, a):
...: self.a = a
...:
...:
In [2]: X(1).__dict__
...
AttributeError: 'X' object has no attribute '__dict__'
Небольшие хитрости
In [19]: a = 256
In [20]: b = 256
In [21]: a is b
Out[21]: True
In [22]: a = 257
In [23]: b = 257
In [24]: a is b
Out[24]: False
Все целые числа от -5 до 256 создаются 1 раз при старте интерпретатора
Небольшие хитрости
$ python3 -c "a = 'abc' * 20; b = 'abc' * 20; print(a is b)"
True
$ python3 -c "a = input(); b = input(); print(a is b)"
a
a
True
$ python3 -c "a = input(); b = input(); print(a is b)"
ab
ab
False
sys.intern
import sys
a = sys.intern(input())
b = sys.intern(input())
print(a is b)
Сборка мусора
![](https://s3.amazonaws.com/media-p.slid.es/uploads/995652/images/5799499/pasted-from-clipboard.png)
Ref counting
PyObject
typedef struct _object {
Py_ssize_t ob_refcnt; # счетчик ссылок
struct _typeobject *ob_type;
} PyObject;
Счетчик увеличивается
- Создается переменная, указывающая на объект
- Объект записывается в атрибут
- Объект передается как аргумент в функцию
- Объект кладется в коллекцию
- etc.
Счетчик уменьшается
- Удаляется переменная/атрибут
- Заканчивается выполнение функции
- Удаляется коллекция
- etc.
Объект удаляется из памяти, когда счетчик ссылок опустится до 0
Удаление одного объекта может каскадно привести к удалению других
sys.getrefcount
foo = []
# 2 references, 1 from the foo var and 1 from getrefcount
print(sys.getrefcount(foo))
def bar(a):
# 4 references
# from the foo var, function argument,
# getrefcount and Python's function stack
print(sys.getrefcount(a))
bar(foo)
# 2 references, the function scope is destroyed
print(sys.getrefcount(foo))
Циклические ссылки
object_1 = {}
object_2 = {}
object_1['obj2'] = object_2
object_2['obj1'] = object_1
del object_1, object_2
gc
gc запускается периодически и делает stop the world
![](https://s3.amazonaws.com/media-p.slid.es/uploads/995652/images/5801852/pasted-from-clipboard.png)
Generations
Все объекты делятся на три поколения
Новые объекты попадают в первое
У каждого поколения есть счетчик, который увеличивается при добавлении объекта в поколение и уменьшается при удалении
После создания любого контейнерного объекта проверяется достиг ли счетчик некоторого порогового значения (700, 10, 10 по умолчанию) и запускается сборка для соответствующего поколения
Пороги можно менять
gc.get_threshold
gc.set_threshold
Объекты, пережившие сборку мусора, переходят в следующее поколение. После чего счетчик для поколения обнуляется.
Переход объектов в следующее поколение может сразу же вызвать сборку и в нем
Как gc находит циклические ссылки?
Можно вручную запустить gc через gc.collect()
Отладка gc
import gc
gc.set_debug(gc.DEBUG_SAVEALL)
print(gc.get_count())
lst = []
lst.append(lst)
list_id = id(lst)
del lst
gc.collect()
for item in gc.garbage:
print(item)
assert list_id == id(item)
Подсчет ссылок не отключаем
Для всего остального есть gc.disable()
__del__
In [1]: class X:
...: def __del__(self):
...: print('del X')
...:
In [2]: x = X()
In [3]: del x
del X
weakref
In [10]: x = X()
In [11]: ref = weakref.ref(x)
In [12]: print(ref())
<__main__.X object at 0x102bdd668>
In [13]: del x
del X
In [14]: print(ref())
None
Интернирование строк тоже не учитывается в счетчике ссылок
memory_profiler
Line # Mem usage Increment Line Contents
==============================================
3 @profile
4 5.97 MB 0.00 MB def my_func():
5 13.61 MB 7.64 MB a = [1] * (10 ** 6)
6 166.20 MB 152.59 MB b = [2] * (2 * 10 ** 7)
7 13.61 MB -152.59 MB del b
8 13.61 MB 0.00 MB return a
ДЗ
Вопросы?
Tinkoff Python 3
By Afonasev Evgeniy
Tinkoff Python 3
- 743