View on GitHub

Methodics of Python Intro

Practical materials for learning Python

В современных ЯПах (особенно, компилируемых высокоуровневых) в какой-то момент появилась возможность штатно запрашивать данные самого объекта — наличие полей, тип объекта итд

В Питоне эта интроспекция идёт из коробки

>>> # знаем про dir, __dict__, vars
>>> 
>>> # Есть методы проверки того, чем являются объекты (callable, hasattr)
>>> 
>>> # Поддерживаются аннотации. След.лекция будет как раз про неё как про основной инструмент статичес\
кой типизации
>>> 
>>> # Наличие аннотации не меняет «игнорирование» типов питоном из коробки.
>>> 

>>> ## Модуль inspect — делает исследование объектовудобнее и подробнее.
>>> class C:  
...     """Documentation string"""
...     __slots__ = "a", 'b', 'c'
...     def fun(self, a):
...         """C method"""
...         return a + self.a
...
>>> import inspect  
>>> inspect.getmembers(C)  
[('__class__', <class 'type'>), ('__delattr__', <slot wrapper '__delattr__' of 'object' objects>), ('__  
dir__', <method '__dir__' of 'object' objects>), ('__doc__', 'Documentation string'), ('__eq__', <slot  
wrapper '__eq__' of 'object' objects>), ('__firstlineno__', 1), ('__format__', <method '__format__' of  
'object' objects>), ('__ge__', <slot wrapper '__ge__' of 'object' objects>), ('__getattribute__', <slot  
wrapper '__getattribute__' of 'object' objects>), ('__getstate__', <method '__getstate__' of 'object'  
objects>), ('__gt__', <slot wrapper '__gt__' of 'object' objects>), ('__hash__', <slot wrapper '__hash_  
_' of 'object' objects>), ('__init__', <slot wrapper '__init__' of 'object' objects>), ('__init_subclas  
s__', <built-in method __init_subclass__ of type object at 0x559923f33c40>), ('__le__', <slot wrapper '  
__le__' of 'object' objects>), ('__lt__', <slot wrapper '__lt__' of 'object' objects>), ('__module__',  
'__main__'), ('__ne__', <slot wrapper '__ne__' of 'object' objects>), ('__new__', <built-in method __ne  
w__ of type object at 0x7f5098ee3480>), ('__reduce__', <method '__reduce__' of 'object' objects>), ('__  
reduce_ex__', <method '__reduce_ex__' of 'object' objects>), ('__repr__', <slot wrapper '__repr__' of '  
object' objects>), ('__setattr__', <slot wrapper '__setattr__' of 'object' objects>), ('__sizeof__', <m  
ethod '__sizeof__' of 'object' objects>), ('__slots__', ('a', 'b', 'c')), ('__static_attributes__', ())  
, ('__str__', <slot wrapper '__str__' of 'object' objects>), ('__subclasshook__', <built-in method __su  
bclasshook__ of type object at 0x559923f33c40>), ('a', <member 'a' of 'C' objects>), ('b', <member 'b'  
of 'C' objects>), ('c', <member 'c' of 'C' objects>), ('fun', <function C.fun at 0x7f5097dc1800>)]  
>>> # Прокаченный dir получается  
>>>
>>> dir(C)  
['__class__', '__delattr__', '__dir__', '__doc__', '__eq__', '__firstlineno__', '__format__', '__ge__',  
'__getattribute__', '__getstate__', '__gt__', '__hash__', '__init__', '__init_subclass__', '__le__', '  
__lt__', '__module__', '__ne__', '__new__', '__reduce__', '__reduce_ex__', '__repr__', '__setattr__', '  
__sizeof__', '__slots__', '__static_attributes__', '__str__', '__subclasshook__', 'a', 'b', 'c', 'fun']  
>>>
>>> # Можно задавать фильтры  
>>>
>>> inspect.getmembers(C, inspect.isfunction())  
Traceback (most recent call last):  
 File "<python-input-20>", line 1, in <module>
   inspect.getmembers(C, inspect.isfunction())
                         ~~~~~~~~~~~~~~~~~~^^
TypeError: isfunction() missing 1 required positional argument: 'object'  
>>> inspect.getmembers(C, inspect.isfunction)  
[('fun', <function C.fun at 0x7f5097dc1800>)]  
>>> inspect.getmembers(C, inspect.ismethod)  
[]  
>>> c = C()  
>>> inspect.getmembers(c, inspect.ismethod)  
[('fun', <bound method C.fun of <__main__.C object at 0x7f5097d982c0>>)]  
>>>
>>> inspect.getdoc(C)  
'Documentation string'  
>>> inspect.getsource(C)  
"# Important: don't add things to this module, as they will end up in the REPL's\n"  
>>> print(inspect.getsource(inspect.getdoc))  
def getdoc(object):  
   """Get the documentation string for an object.
  
   All tabs are expanded to spaces.  To clean up docstrings that are
   indented to line up with blocks of code, any whitespace than can be
   uniformly removed from the second line onwards is removed."""
   try:
       doc = object.__doc__
   except AttributeError:
       return None
   if doc is None:
       try:
           doc = _finddoc(object)
       except (AttributeError, TypeError):
           return None
   if not isinstance(doc, str):
       return None
   return cleandoc(doc)
  
>>>
>>> # Некрасивый и красивый выводы документации  
>>> doc = """Get the documentation string for an object.
...
...    All tabs are expanded to spaces.  To clean up docstrings that are
...    indented to line up with blocks of code, any whitespace than can be
...    uniformly removed from the second line onwards is removed."""
>>> print(doc)  
Get the documentation string for an object.

   All tabs are expanded to spaces.  To clean up docstrings that are
   indented to line up with blocks of code, any whitespace than can be
   uniformly removed from the second line onwards is removed.
>>> print(inspect.cleandoc(doc))  
Get the documentation string for an object.

All tabs are expanded to spaces.  To clean up docstrings that are
indented to line up with blocks of code, any whitespace than can be
uniformly removed from the second line onwards is removed.  
>>>

Интроспекция функций

>>> def fun(a, aa=123, /, b=1, *, c=34):  
...     print(a, aa, b, c)
...
>>> fun(1)  
1 123 1 34  
>>> fun(1, 2)  
1 2 1 34  
>>> fun(1, 2, 3)  
1 2 3 34  
>>> fun(1, 2, b=33)  
1 2 33 34  
>>> fun(1, c=100500)  
1 123 1 100500  
>>>
>>> inspect.signature(fun)  
<Signature (a, aa=123, /, b=1, *, c=34)>  
>>> sign = inspect.signature(fun)  
>>> sign.parameters  
mappingproxy(OrderedDict({'a': <Parameter "a">, 'aa': <Parameter "aa=123">, 'b': <Parameter "b=1">, 'c'  
: <Parameter "c=34">}))  
>>> par=sign.parameters  
>>> par['a']  
<Parameter "a">  
>>> par['aa']  
<Parameter "aa=123">  
>>> par['aa'].kind  
<_ParameterKind.POSITIONAL_ONLY: 0>  
>>> par['b'].kind  
<_ParameterKind.POSITIONAL_OR_KEYWORD: 1>  
>>> par['c'].kind  
<_ParameterKind.KEYWORD_ONLY: 3>  
>>>
>>> # есть другой формат  
>>>
>>> inspect.getfullargspec(fun)  
FullArgSpec(args=['a', 'aa', 'b'], varargs=None, varkw=None, defaults=(123, 1), kwonlyargs=['c'], kwonl  
ydefaults={'c': 34}, annotations={})  
>>>
>>> # Такая непростая структура идёт от того, что в питоне изначально тянется неоднозначность понимани\  
я, что за параметры по типу перед нами  
>>> # Аналогичная история с замыканиями  
>>>
>>> y = 1  
>>> def funf(a):  
...     def fun(x):
...         return a + x + y
...     return fun
...
>>> f = funf(42)  
>>> inspect.getclosurevars(f)  
ClosureVars(nonlocals={'a': 42}, globals={'y': 1}, builtins={}, unbound=set())  
>>>
>>> inspect.getclasstree((SyntaxError,))  
[(<class 'Exception'>, (<class 'BaseException'>,)), [(<class 'SyntaxError'>, (<class 'Exception'>,))]]  

>>> # Дальше—лучше. Работаем с контекстом вызовов  
>>>
>>> def fun(a):  
...     return fun2(a)
...
>>> def fun2(a):  
...     return fun3(a)
...
>>> def fun3(a):  
...     raise RuntimeError
...
>>> fun(100500)  
Traceback (most recent call last):  
 File "<python-input-71>", line 1, in <module>
   fun(100500)
   ~~~^^^^^^^^
 File "<python-input-68>", line 2, in fun
   return fun2(a)
 File "<python-input-69>", line 2, in fun2
   return fun3(a)
 File "<python-input-70>", line 2, in fun3
   raise RuntimeError
RuntimeError  
>>>
>>> # ПРи обработке исключений, да и вообще в контекст прилетает стек вызовов!  
>>>
>>> def caller(a, b):  
...     global F
...     guesser(b, a * 2)
...
... def guesser(x, y):  
...     Me = inspect.stack()[0]
...     print(Me.function)
...     print(Me.frame.f_globals[Me.function])
...     print(*Me.frame.f_locals)
...     print(inspect.signature(Me.frame.f_globals[Me.function]))
...     print(inspect.stack()[1].function)
...     # print(inspect.stack()
...
... caller(1, 10)  
...
guesser  
<function guesser at 0x7f5097dc1e40>  
x y Me  
(x, y)  
caller


Интерпретация исходного текста

Питон разрабатывался не как исполнитель строк в памяти. Питоновский байткод, как результат анализатора, это стек-машина.

Поговорим про синтаксический анализатор. Он на Си, но есть прикладной модуль ast, который умеет парсить что угодно

>>> import ast  
>>>
>>> ast.parse('print("qq")')  
<ast.Module object at 0x7f5097dbc9d0>  
>>> res = ast.parse('print("qq")')  
>>> res.body  
[<ast.Expr object at 0x7f5097d5d3d0>]  
>>> ast.  
KeyboardInterrupt  
>>> print(ast.dump(res, intent=2))  
Traceback (most recent call last):  
 File "<python-input-82>", line 1, in <module>
   print(ast.dump(res, intent=2))
         ~~~~~~~~^^^^^^^^^^^^^^^
TypeError: dump() got an unexpected keyword argument 'intent'. Did you mean 'indent'?  
>>> print(ast.dump(res, indent=2))  
Module(  
 body=[
   Expr(
     value=Call(
       func=Name(id='print', ctx=Load()),
       args=[
         Constant(value='qq')]))])
>>>
>>>
>>> tree = ast.parse("""  
... k = 1  
... for i in range(10):  
...     print(i, k)
... """)  
>>> print(ast.dump(tree, indent=2))  
Module(  
 body=[
   Assign(
     targets=[
       Name(id='k', ctx=Store())],
     value=Constant(value=1)),
   For(
     target=Name(id='i', ctx=Store()),
     iter=Call(
       func=Name(id='range', ctx=Load()),
       args=[
         Constant(value=10)]),
     body=[
       Expr(
         value=Call(
           func=Name(id='print', ctx=Load()),
           args=[
             Name(id='i', ctx=Load()),
             Name(id='k', ctx=Load())]))])])
>>>

Зачем работать с ast ­— абстрактным синтаксическим деревом? Проверить, что что это вообще питон-код (если АСД не собирается, код не пиотновский. Собирается — мб норм код)

Это хороший инструмент для проверки плагиата.

Также в такой штуке можно написать свой интерпретатор поверх питоновского.

>>> code = ast.unparse(tree)  
>>> print(code)  
k = 1  
for i in range(10):  
   print(i, k)
>>>
>>> for obj in ast.walk(tree):  
...   print(obj)
...
<ast.Module object at 0x7f5097d5dad0>  
<ast.Assign object at 0x7f5097c11290>  
<ast.For object at 0x7f5097c10f10>  
<ast.Name object at 0x7f5097c10e10>  
<ast.Constant object at 0x7f5097c112d0>  
<ast.Name object at 0x7f5097c11510>  
<ast.Call object at 0x7f5097c10dd0>  
<ast.Expr object at 0x7f5097c11190>  
<ast.Store object at 0x7f5098039b50>  
<ast.Store object at 0x7f5098039b50>  
<ast.Name object at 0x7f5097c11010>  
<ast.Constant object at 0x7f5097c10d10>  
<ast.Call object at 0x7f5097c11150>  
<ast.Load object at 0x7f5098039ad0>  
<ast.Name object at 0x7f5097c10fd0>  
<ast.Name object at 0x7f5097c10e90>  
<ast.Name object at 0x7f5097c11210>  
<ast.Load object at 0x7f5098039ad0>  
<ast.Load object at 0x7f5098039ad0>  
<ast.Load object at 0x7f5098039ad0>




compile

Компиляция дерева в байткод

>>> a, b = 1, 2  
>>> compile('a + b', "IN", 'eval')  
<code object <module> at 0x7f5097f7f9f0, file "IN", line 1>  
>>> code = compile('a + b', "IN", 'eval')  
>>> import dis  
>>> dis.dis(code)  
 0           RESUME                   0
  
 1           LOAD_NAME                0 (a)
             LOAD_NAME                1 (b)
             BINARY_OP                0 (+)
             RETURN_VALUE
>>> code = compile('a + b', "IN", 'exec')  
>>> dis.dis(code)  
 0           RESUME                   0
  
 1           LOAD_NAME                0 (a)
             LOAD_NAME                1 (b)
             BINARY_OP                0 (+)
             POP_TOP
             RETURN_CONST             0 (None)
>>>
>>> code.co_code  
b'\x95\x00\\\x00\\\x01-\x00\x00\x00 \x00g\x00'  
>>> compile('tree', 'tree', 'exec').co_code  
b'\x95\x00\\\x00 \x00g\x00'  
>>> compile(tree, 'tree', 'exec').co_code  
b'\x95\x00S\x00r\x00\\\x01"\x00S\x015\x01\x00\x00\x00\x00\x00\x00\x13\x00H\x0c\x00\x00r\x02\\\x03"\x00\  
\\x02\\\x005\x02\x00\x00\x00\x00\x00\x00 \x00M\x0e\x00\x00\x0b\x00 \x00g\x02'  

>>> text = '''N = 100500  
... for i in range(5):  
...   print(N)'''
>>> code = compile(text, 'text', 'exec')  
>>> dis.dis(code)  
 0           RESUME                   0
  
 1           LOAD_CONST               0 (100500)
             STORE_NAME               0 (N)
  
 2           LOAD_NAME                1 (range)
             PUSH_NULL
             LOAD_CONST               1 (5)
             CALL                     1
             GET_ITER
     L1:     FOR_ITER                11 (to L2)
             STORE_NAME               2 (i)
  
 3           LOAD_NAME                3 (print)
             PUSH_NULL
             LOAD_NAME                0 (N)
             CALL                     1
             POP_TOP
             JUMP_BACKWARD           13 (to L1)
  
 2   L2:     END_FOR
             POP_TOP
             RETURN_CONST             2 (None)
>>>
>>> dis.dis(compile("1 + 2", "<пример>", "eval"))  
 0           RESUME                   0
  
 1           RETURN_CONST             0 (3)
>>> dis.dis(compile("a + 2", "<пример>", "eval"))  
 0           RESUME                   0
  
 1           LOAD_NAME                0 (a)
             LOAD_CONST               0 (2)
             BINARY_OP                0 (+)
             RETURN_VALUE
>>> dis.dis(compile("a + 2", "<пример>", "exec"))  
 0           RESUME                   0
  
 1           LOAD_NAME                0 (a)
             LOAD_CONST               0 (2)
             BINARY_OP                0 (+)
             POP_TOP
             RETURN_CONST             1 (None)
>>> dis.dis(compile("a + 2", "<пример>", "single"))  
 0           RESUME                   0
  
 1           LOAD_NAME                0 (a)
             LOAD_CONST               0 (2)
             BINARY_OP                0 (+)
             CALL_INTRINSIC_1         1 (INTRINSIC_PRINT)
             POP_TOP
             RETURN_CONST             1 (None)
>>>

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

>>> c1 = compile('a, b = 3, 4', 'in', 'exec')  
>>> c2 = compile('a = 3; b = 4', 'in', 'exec')  
>>> dis.dis(c1)  
 0           RESUME                   0
  
 1           LOAD_CONST               0 ((3, 4))
             UNPACK_SEQUENCE          2
             STORE_NAME               0 (a)
             STORE_NAME               1 (b)
             RETURN_CONST             1 (None)
>>> dis.dis(c2)  
 0           RESUME                   0
  
 1           LOAD_CONST               0 (3)
             STORE_NAME               0 (a)
             LOAD_CONST               1 (4)
             STORE_NAME               1 (b)
             RETURN_CONST             2 (None)
>>>
>>> print(dis.code_info(c1))  
Name:              <module>
Filename:          in
Argument count:    0
Positional-only arguments: 0  
Kw-only arguments: 0  
Number of locals:  0
Stack size:        2
Flags:             0x0
Constants:  
  0: (3, 4)
  1: None
Names:  
  0: a
  1: b
>>> print(dis.code_info(c2))  
Name:              <module>
Filename:          in
Argument count:    0
Positional-only arguments: 0  
Kw-only arguments: 0  
Number of locals:  0
Stack size:        1
Flags:             0x0
Constants:  
  0: 3
  1: 4
  2: None
Names:  
  0: a
  1: b
>>>