Python Metaprogramming
Python Metaprogramming
Metaprogramming
David Beazley
@dabeaz
http://www.dabeaz.com
Requirements
Metaprogramming
In a nutshell: code that manipulates code
Common examples:
Decorators
Metaclasses
Descriptors
Essentially, it's doing things with code
Copyright (C) 2013, http://www.dabeaz.com 4
Why Would You Care?
DRY
Copyright (C) 2013, http://www.dabeaz.com 6
DRY Don't Repeat Yourself
This Tutorial
Framework/library builders
Anyone who wants to know how things work
Programmers wishing to increase "job security"
Reading
Tutorial loosely based
on content in "Python
Cookbook, 3rd Ed."
Published May, 2013
You'll find even more
information in the
book
class A:
def method1(self, args):
statement1
statement2
def method2(self, args):
statement1
statement2
...
Copyright (C) 2013, http://www.dabeaz.com 14
Statements
statement1
statement2
statement3
...
Functions
def func(x, y, z):
statement1
statement2
statement3
...
Positional arguments
func(1, 2, 3)
Keyword arguments
func(x=1, z=3, y=2)
Default Arguments
def func(x, debug=False, names=None):
if names is None:
names = []
...
func(1)
func(1, names=['x', 'y'])
func(*args, **kwargs)
same as
func(1, 2, x=3, y=4, z=5)
Closures
You can make and return functions
def make_adder(x, y):
def add():
return x + y
return add
@classmethod
def cmethod(cls): Spam.cmethod()
pass
@staticmethod
def smethod(): Spam.smethod()
pass
Inheritance
class Base:
def spam(self):
...
class Foo(Base):
def spam(self):
...
# Call method in base class
r = super().spam()
Example:
>>> s = Spam(2,3)
>>> s.__dict__
{'y': 3, 'x': 2}
>>> Spam.__dict__['foo']
<function Spam.foo at 0x10069fc20>
>>>
Copyright (C) 2013, http://www.dabeaz.com 27
Metaprogramming Basics
A Debugging Decorator
from functools import wraps
def debug(func):
msg = func.__qualname__
@wraps(func)
def wrapper(*args, **kwargs):
print(msg)
return func(*args, **kwargs)
return wrapper
Application (wrapping)
func = debug(func)
def debug(func):
msg = func.__qualname__
@wraps(func)
def wrapper(*args, **kwargs):
print(msg)
return func(*args, **kwargs)
return wrapper
A Debugging Decorator
from functools import wraps
def debug(func):
msg = func.__qualname__
@wraps(func)
def wrapper(*args, **kwargs):
print(msg)
return func(*args, **kwargs)
return wrapper
def debug(func):
msg = func.__qualname__
@wraps(func)
def wrapper(*args, **kwargs):
print(msg)
return func(*args, **kwargs)
return wrapper
>>> add.__qualname__
'wrapper'
>>> add.__doc__
>>> help(add)
Help on function wrapper in module
__main__:
wrapper(*args, **kwargs)
>>>
Copyright (C) 2013, http://www.dabeaz.com 38
Decorator Syntax
The definition of a function and wrapping
almost always occur together
def add(x,y):
return x+y
add = debug(add)
Example Use
@debug
def add(x, y):
return x + y
@debug
def sub(x, y):
return x - y
@debug
def mul(x, y):
return x * y
@debug
def div(x, y):
return x / y
Big Picture
def debug(func):
log = logging.getLogger(func.__module__)
msg = func.__qualname__
@wraps(func)
def wrapper(*args, **kwargs):
log.debug(msg)
return func(*args, **kwargs)
return wrapper
def debug(func):
if 'DEBUG' not in os.environ:
return func
msg = func.__qualname__
@wraps(func)
def wrapper(*args, **kwargs):
print(msg)
return func(*args, **kwargs)
return wrapper
Evaluates as
func = decorator(args)(func)
def debug(prefix=''):
def decorate(func):
msg = prefix + func.__qualname__
@wraps(func)
def wrapper(*args, **kwargs):
print(msg)
return func(*args, **kwargs)
return wrapper
return decorate
Usage
@debug(prefix='***')
def add(x,y):
return x+y
Copyright (C) 2013, http://www.dabeaz.com 47
def debug(prefix=''):
def decorate(func):
msg = prefix + func.__qualname__
@wraps(func)
def wrapper(*args, **kwargs):
print(msg)
return func(*args, **kwargs)
return wrapper
return decorate
Usage
Use as a simple decorator
@debug
def add(x, y):
return x + y
Class Decorator
def debugmethods(cls):
for name, val in vars(cls).items():
if callable(val):
setattr(cls, name, debug(val))
return cls
Idea:
Walk through class dictionary
Identify callables (e.g., methods)
Wrap with a decorator
Copyright (C) 2013, http://www.dabeaz.com 52
Example Use
@debugmethods
class Spam:
def grok(self):
pass
def bar(self):
pass
def foo(self):
pass
Limitations
@debugmethods
class BrokenSpam:
@classmethod
def grok(cls): # Not wrapped
pass
@staticmethod
def bar(): # Not wrapped
pass
return cls
Example
@debugattr
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
>>> p = Point(2, 3)
>>> p.x
Get: x
2
>>> p.y
Get: y
3
>>>
@debugmethods
class Grok(Spam):
...
@debugmethods
class Mondo(Grok):
...
Solution: A Metaclass
class debugmeta(type):
def __new__(cls, clsname, bases, clsdict):
clsobj = super().__new__(cls, clsname,
bases, clsdict)
clsobj = debugmethods(clsobj)
return clsobj
Usage
class Base(metaclass=debugmeta):
...
class Spam(Base):
...
Idea
Class gets created normally
Solution: A Metaclass
class debugmeta(type):
def __new__(cls, clsname, bases, clsdict):
clsobj = super().__new__(cls, clsname,
bases, clsdict)
clsobj = debugmethods(clsobj)
return clsobj
Idea
Class gets created normally
Immediately wrapped by class decorator
Types
All values in Python have a type
Example:
>>> x = 42
>>> type(x)
<type 'int'>
>>> s = "Hello"
>>> type(s)
<type 'str'>
>>> items = [1,2,3]
>>> type(items)
<type 'list'>
>>>
>>> s = Spam()
>>> type(s)
<class '__main__.Spam'>
>>>
Types of Classes
Classes are instances of types
>>> type(int)
<class 'int'>
>>> type(list)
<class 'list'>
>>> type(Spam)
<class '__main__.Spam'>
>>> isinstance(Spam, type)
True
>>>
>>> type
<class 'type'>
>>>
Classes Deconstructed
Consider a class:
class Spam(Base):
def __init__(self, name):
self.name = name
def bar(self):
print "I'm Spam.bar"
Class Definition
Step 2: The class dictionary is created
clsdict = type.__prepare__('Spam', (Base,))
Class Definition
Step 4: Class is constructed from its name,
base classes, and the dictionary
>>> Spam = type('Spam', (Base,), clsdict)
>>> Spam
<class '__main__.Spam'>
>>> s = Spam('Guido')
>>> s.bar()
I'm Spam.bar
>>>
To use
class Spam(metaclass=mytype):
...
Inheritance
Metaclasses propagate down hierarchies
class Base(metaclass=mytype):
...
Idea
Class gets created normally
Immediately wrapped by class decorator
class Spam(Base):
... Debugging gets applied
across entire hierarchy
class Grok(Spam):
... Implicitly applied in
class Mondo(Grok):
subclasses
...
Interlude
Journey to Come
class Point:
def __init__(self, x, y):
self.x = x
self.y = y
class Host:
def __init__(self, address, port):
self.address = address
self.port = port
Problem : Structures
class Stock:
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
Why must I keep
class Point: writing these
def __init__(self, x, y):
self.x = x
boilerplate init
self.y = y methods?
class Host:
def __init__(self, address, port):
self.address = address
self.port = port
class Stock(Structure):
_fields = ['name', 'shares', 'price']
class Point(Structure):
_fields = ['x', 'y']
class Host(Structure):
_fields = ['address', 'port']
Copyright (C) 2013, http://www.dabeaz.com 83
Usage
>>> s = Stock('ACME', 50, 123.45)
>>> s.name
'ACME'
>>> s.shares
50
>>> s.price
123.45
>>> p = Point(4, 5)
>>> p.x
4
>>> p.y
5
>>>
Put a Signature on It
Signature Binding
Argument binding
def func(*args, **kwargs):
bound_args = sig.bind(*args, **kwargs)
for name, val in bound_args.arguments.items():
print(name, '=', val)
Signature Binding
Error handling
>>> func('ACME', 50)
Traceback (most recent call last):
...
TypeError: 'price' parameter lacking default value
def make_signature(names):
return Signature(
Parameter(name,
Parameter.POSITIONAL_OR_KEYWORD)
for name in names)
class Structure:
__signature__ = make_signature([])
def __init__(self, *args, **kwargs):
bound = self.__signature__.bind(
*args, **kwargs)
for name, val in bound.arguments.items():
setattr(self, name, val)
Solution w/Signatures
class Stock(Structure):
__signature__ = make_signature(
['name','shares','price'])
class Point(Structure):
__signature__ = make_signature(['x', 'y'])
class Host(Structure):
__signature__ = make_signature(
['address', 'port'])
New Problem
This is rather annoying
class Stock(Structure):
__signature__ = make_signature(
['name','shares','price'])
class Point(Structure):
__signature__ = make_signature(['x', 'y'])
class Host(Structure):
__signature__ = make_signature(
['address', 'port'])
Class Decorators
def add_signature(*names):
def decorate(cls):
cls.__signature__ = make_signature(names)
return cls
return decorate
Usage:
@add_signature('name','shares','price')
class Stock(Structure):
pass
@add_signature('x','y')
class Point(Structure):
pass
class Structure(metaclass=StructMeta):
_fields = []
def __init__(self, *args, **kwargs):
bound = self.__signature__.bind(
*args, **kwargs)
for name, val in bound.arguments.items():
setattr(self, name, val)
Metaclass Solution
class StructMeta(type):
def __new__(cls, name, bases, clsdict):
clsobj = super().__new__(cls, name,
bases, clsdict)
sig = make_signature(clsobj._fields)
setattr(clsobj, '__signature__', sig)
return clsobj
class Point(Structure):
_fields = ['x', 'y']
class Host(Structure):
_fields = ['address', 'port']
Considerations
How much will the Structure class be expanded?
Example: supporting methods
class Structure(metaclass=StructMeta):
_fields = []
...
def __repr__(self):
args = ', '.join(repr(getattr(self, name))
for name in self._fields)
return type(self).__name__ + \
'(' + args + ')'
Properties
You can upgrade attributes to have checks
class Stock(Structure):
_fields = ['name', 'shares', 'price']
@property
def shares(self):
(getter) return self._shares
@shares.setter
def shares(self, value):
if not isinstance(value, int):
raise TypeError('expected int')
(setter) if value < 0:
raise ValueError('Must be >= 0')
self._shares = value
An Issue
It works, but it quickly gets annoying
@property
def shares(self):
return self._shares
@shares.setter
def shares(self, value):
if not isinstance(value, int):
raise TypeError('expected int')
if value < 0:
raise ValueError('Must be >= 0')
self._shares = value
Descriptor Protocol
Properties are implemented via descriptors
class Descriptor:
def __get__(self, instance, cls):
...
def __set__(self, instance, value):
...
def __delete__(self, instance)
...
A Basic Descriptor
class Descriptor:
def __init__(self, name=None):
self.name = name
A Basic Descriptor
class Descriptor:
def __init__(self, name=None):
self.name = name
Descriptor Usage
class Stock(Structure):
_fields = ['name', 'shares', 'price']
name = Descriptor('name')
shares = Descriptor('shares')
price = Descriptor('price')
Specialization
class Integer(Typed):
ty = int
class Float(Typed):
ty = float
class String(Typed):
ty = str
Usage
class Stock(Structure):
_fields = ['name', 'shares', 'price']
name = String('name')
shares = Integer('shares')
price = Float('price')
Example:
>>> s = Stock('ACME', 50, 91.1)
>>> s.shares = 'a lot'
Traceback (most recent call last):
...
TypeError: Expected <class 'int'>
>>> s.name = 42
Traceback (most recent call last):
...
TypeError: Expected <class 'str'>
>>>
Copyright (C) 2013, http://www.dabeaz.com 116
Value Checking
class Positive(Descriptor):
def __set__(self, instance, value):
if value < 0:
raise ValueError('Expected >= 0')
super().__set__(instance, value)
Usage
class Stock(Structure):
_fields = ['name', 'shares', 'price']
name = String('name')
shares = PosInteger('shares')
price = PosFloat('price')
Example:
>>> s = Stock('ACME', 50, 91.1)
>>> s.shares = -10
Traceback (most recent call last):
...
ValueError: Expected >= 0
>>> s.shares = 'a lot'
Traceback (most recent call last):
...
TypeError: Expected <class 'int'>
>>>
Copyright (C) 2013, http://www.dabeaz.com 118
Building Blocks!
class PosInteger(Integer, Positive):
pass
super()
>>> PosInteger.__mro__
(<class 'PosInteger'>, This chain defines the
<class 'Integer'>, order in which the
<class 'Typed'>,
<class 'Positive'>, value is checked by
<class 'Descriptor'>, different __set__()
<class 'object'>) methods
>>>
Use:
class SizedString(String, Sized):
pass
Usage
class Stock(Structure):
_fields = ['name', 'shares', 'price']
name = SizedString('name', maxlen=8)
shares = PosInteger('shares')
price = PosFloat('price')
Example:
>>> s = Stock('ACME', 50, 91.1)
>>> s.name = 'ABRACADABRA'
Traceback (most recent call last):
...
ValueError: Too big
>>>
Use:
class SizedRegexString(String, Sized, Regex):
pass
Usage
class Stock(Structure):
_fields = ['name', 'shares', 'price']
name = SizedRegexString('name', maxlen=8,
pat='[A-Z]+$')
shares = PosInteger('shares')
price = PosFloat('price')
Example:
>>> s = Stock('ACME', 50, 91.1)
>>> s.name = 'Head Explodes!'
Traceback (most recent call last):
...
ValueError: Invalid string
>>>
Keyword-only Args
SizedRegexString('name', maxlen=8, pat='[A-Z]+$')
class Descriptor:
def __init__(self, name=None):
...
class Sized(Descriptor):
def __init__(self, *args, maxlen, **kwargs):
...
super().__init__(*args, **kwargs)
class Regex(Descriptor):
def __init__(self, *args, pat, **kwargs):
...
super().__init__(*args, **kwargs)
class Descriptor:
def __init__(self, name=None):
...
class Sized(Descriptor):
def __init__(self, *args, maxlen, **kwargs):
...
super().__init__(*args, **kwargs)
class Regex(Descriptor):
Keyword-only argument is
def __init__(self, *args, pat, **kwargs):
isolated
... and removed from
all other passed args
super().__init__(*args, **kwargs)
"Awesome, man!"
Copyright (C) 2013, http://www.dabeaz.com 128
Annoyance
class Stock(Structure):
_fields = ['name', 'shares', 'price']
name = SizedRegexString('name', maxlen=8,
pat='[A-Z]+$')
shares = PosInteger('shares')
price = PosFloat('price')
New Usage
class Stock(Structure):
name = SizedRegexString(maxlen=8,pat='[A-Z]+$')
shares = PosInteger()
price = PosFloat()
__prepare__()
def __new__(cls, name, bases, clsdict): creates
fields = [ key for key, val in clsdict.items()
and returns
if isinstance(val, dictionary]
Descriptor)
for name in fields: to use for execution of
clsdict[name].name = name
the class body.
clsobj = super().__new__(cls, name, bases,
An OrderedDict will
dict(clsdict))
sig = make_signature(fields)
preserve the definition
setattr(clsobj, '__signature__', sig)
return clsobj order.
Copyright (C) 2013, http://www.dabeaz.com 133
Ordering of Definitions
class Stock(Structure):
name = SizedRegexString(maxlen=8,pat='[A-Z]+$')
shares = PosInteger()
price = PosFloat()
clsdict = OrderedDict(
('name', <class 'SizedRegexString'>),
('shares', <class 'PosInteger'>),
('price', <class 'PosFloat'>)
)
Duplicate Definitions
class Stock(Structure):
name = String()
shares = PosInteger()
price = PosFloat()
shares = PosInteger()
Name Setting
Old code
class Stock(Structure):
_fields = ['name', 'shares', 'price']
name = SizedRegexString('name', ...)
shares = PosInteger('shares')
price = PosFloat('price')
New Code
class Stock(Structure):
name = SizedRegexString(...)
shares = PosInteger()
price = PosFloat()
New Metaclass
from collections import OrderedDict
class StructMeta(type):
@classmethod
def __prepare__(cls, name, bases):
return OrderedDict()
The Costs
Option 1 : Simple
class Stock:
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
Option 2 : Meta
class Stock(Structure):
name = SizedRegexString(...)
shares = PosInteger()
price = PosFloat()
Attribute assignment
s.price = 10.0 0.11s 3.40s
(31x)
Attribute assignment
s.name = 'ACME' 0.14s 8.14s
(58x)
A Few Tests
Instance creation Simple Meta
s = Stock('ACME', 50, 91.1) 1.07s 91.8s
(86x)
Attribute lookup
s.price 0.08s 0.08s
Code Generation
def _make_init(fields):
code = 'def __init__(self, %s):\n' % \
', '.join(fields)
for name in fields:
code += ' self.%s = %s\n' % (name, name)
return code
Example:
>>> code = _make_init(['name','shares','price'])
>>> print(code)
def __init__(self, name, shares, price):
self.name = name
self.shares = shares
self.price = price
>>>
Copyright (C) 2013, http://www.dabeaz.com 146
Code Generation
class StructMeta(type):
...
def __new__(cls, name, bases, clsdict):
fields = [ key for key, val in clsdict.items()
if isinstance(val, Descriptor) ]
if fields:
exec(_make_init(fields),globals(),clsdict)
Code Generation
class StructMeta(type):
...
def __new__(cls, name, bases, clsdict):
fields = [ key for key, val in clsdict.items()
if isinstance(val, Descriptor) ]
if fields:
exec(_make_init(fields),globals(),clsdict)
class Stock(Structure):
name = SizedRegexString(...)
shares = PosInteger()
price = PosFloat()
Instance creation:
Simple 1.1s
Old Meta (w/signatures) 91.8s
New Meta (w/exec) 17. 6s
New Thought
class Descriptor:
...
def __set__(self, instance, value):
instance.__dict__[self.name] = value
class Typed(Descriptor):
def __set__(self, instance, value):
if not isinstance(value, self.ty):
raise TypeError('Expected %s' % self.ty)
super().__set__(instance, value)
Could you merge
class Positive(Descriptor):
this code together?
def __set__(self, instance, value):
if value < 0:
raise ValueError('Expected >= 0')
super().__set__(instance, value)
Copyright (C) 2013, http://www.dabeaz.com 150
Reformulation
class Descriptor(metaclass=DescriptorMeta):
def __init__(self, name=None):
self.name = name
@staticmethod
def set_code():
return [
'instance.__dict__[self.name] = value'
]
Reformulation
class Typed(Descriptor):
ty = object
@staticmethod
def set_code():
return [
'if not isinstance(value, self.ty):',
' raise TypeError("Expected %s"%self.ty)'
]
class Positive(Descriptor):
@staticmethod
def set_code(self):
return [
'if value < 0:',
' raise ValueError("Expected >= 0")'
]
Copyright (C) 2013, http://www.dabeaz.com 152
Reformulation
class Sized(Descriptor):
def __init__(self, *args, maxlen, **kwargs):
! self.maxlen = maxlen
super().__init__(*args, **kwargs)
@staticmethod
def set_code():
! return [
'if len(value) > self.maxlen:',
' raise ValueError("Too big")'
]
Reformulation
import re
class RegexPattern(Descriptor):
def __init__(self, *args, pat, **kwargs):
self.pat = re.compile(pat)
super().__init__(*args, **kwargs)
@staticmethod
def set_code():
return [
'if not self.pat.match(value):',
' raise ValueError("Invalid string")'
]
Example Setters
>>> print(_make_setter(Descriptor))
def __set__(self, instance, value):
instance.__dict__[self.name] = value
>>> print(_make_setter(PosInteger))
def __set__(self, instance, value):
if not isinstance(value, self.ty):
raise TypeError("Expected %s" % self.ty)
if value < 0:
raise ValueError("Expected >= 0")
instance.__dict__[self.name] = value
>>>
class Descriptor(metaclass=DescriptorMeta):
...
Just to be Clear
class Stock(Structure):
name = SizedRegexString(...)
shares = PosInteger()
price = PosFloat()
Attribute lookup
(86x) (6.7x)
Attribute assignment
s.price = 10.0 0.11s 3.40s 1.11s
(31x) (10x)
Attribute assignment
s.name = 'ACME' 0.14s 8.14s 2.95s
(58x) (21x)
Copyright (C) 2013, http://www.dabeaz.com 159
@alex_gaynor
class Point(Structure):
x = Integer()
y = Integer()
class Address(Structure):
hostname = String()
port = Integer()
Solution: XML
<structures>
<structure name="Stock">
<field type="SizedRegexString" maxlen="8"
pat="'[A-Z]+$'">name</field>
<field type="PosInteger">shares</field>
<field type="PosFloat">price</field>
</structure>
<structure name="Point">
<field type="Integer">x</field>
<field type="Integer">y</field>
</structure>
<structure name="Address">
<field type="String">hostname</field>
<field type="Integer">port</field>
</structure>
</structures>
Copyright (C) 2013, http://www.dabeaz.com 162
Solution: XML
<structures>
<structure name="Stock">
<field type="SizedRegexString" maxlen="8"
pat="'[A-Z]+$'">name</field>
<field type="PosInteger">shares</field>
<field type="PosFloat">price</field>
+5 extra credit
</structure>
Regex
<structure + XML
name="Point">
<field type="Integer">x</field>
<field type="Integer">y</field>
</structure>
<structure name="Address">
<field type="String">hostname</field>
<field type="Integer">port</field>
</structure>
</structures>
Copyright (C) 2013, http://www.dabeaz.com 163
XML to Classes
XML Parsing
from xml.etree.ElementTree import parse
def _xml_to_code(filename):
doc = parse(filename)
code = 'import typestruct as _ts\n'
for st in doc.findall('structure'):
code += _xml_struct_code(st)
return code
Continued...
Copyright (C) 2013, http://www.dabeaz.com 164
XML to Classes
def _xml_struct_code(st):
stname = st.get('name')
code = 'class %s(_ts.Structure):\n' % stname
for field in st.findall('field'):
name = field.text.strip()
dtype = '_ts.' + field.get('type')
kwargs = ', '.join('%s=%s' % (key, val)
for key, val in field.items()
if key != 'type')
code += ' %s = %s(%s)\n' % \
(name, dtype, kwargs)
return code
Example
>>> code = _xml_to_code('data.xml')
>>> print(code)
import typestruct as _ts
class Stock(_ts.Structure):
name = _ts.SizedRegexString(maxlen=8, pat='[A-Z]+$')
shares = _ts.PosInteger()
price = _ts.PosFloat()
class Point(_ts.Structure):
x = _ts.Integer()
y = _ts.Integer()
class Address(_ts.Structure):
hostname = _ts.String()
port = _ts.Integer()
>>>
Now WHAT!?!?
Allow structure .xml files to be imported
Using the import statement
Yes!
Import Hooks
sys.meta_path
>>> import sys
>>> sys.meta_path
[<class '_frozen_importlib.BuiltinImporter'>,
<class '_frozen_importlib.FrozenImporter'>,
<class '_frozen_importlib.PathFinder'>]
>>>
>>> sys.meta_path.append(MyImporter())
>>> import foo
*** Looking for foo
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
ImportError: No module named 'foo'
>>>
Structure Importer
class StructImporter:
def __init__(self, path):
self._path = path
Structure Importer
class StructImporter:
def __init__(self, path):
self._path = path
Walk path=None):
def find_module(self, fullname, path, check for
existence of .xml file
name = fullname.rpartition('.')[-1]
if path is None:
path = self._path
and return a loader
for dn in path:
filename = os.path.join(dn, name+'.xml')
if os.path.exists(filename):
return StructXmlLoader(filename)
return None
install_importer()
Final Thoughts
Hack or by Design?
Python 3 is designed to do this sort of stuff
More advanced metaclasses (e.g., __prepare__)
Signatures
Import hooks
Keyword-only args
Observe: I didn't do any mind-twisting "hacks" to
work around a language limitation.
Non-local variables
def outer():
x = 0
def inner():
nonlocal x
x = newvalue
...
Frame-hacks
import sys
f = sys._getframe(1)
Parsing/AST-manipulation
import ast