Upgrade to Pro — share decks privately, control downloads, hide ads and more …

Python 3 Metaprogramming

Python 3 Metaprogramming

Tutorial. PyCon 2013. Santa Clara. Conference video at https://www.youtube.com/watch?v=sPiWg5jSoZI

David Beazley

March 14, 2013
Tweet

More Decks by David Beazley

Other Decks in Programming

Transcript

  1. Copyright (C) 2013, http://www.dabeaz.com Python 3 Metaprogramming 1 David Beazley

    @dabeaz http://www.dabeaz.com Presented at PyCon'2013, Santa Clara, CA March 14, 2013
  2. Copyright (C) 2013, http://www.dabeaz.com Requirements 2 • Python 3.3 or

    more recent • Don't even attempt on any earlier version • Support files: http://www.dabeaz.com/py3meta
  3. Copyright (C) 2013, http://www.dabeaz.com Welcome! • An advanced tutorial on

    two topics • Python 3 • Metaprogramming • Honestly, can you have too much of either? • No! 3
  4. Copyright (C) 2013, http://www.dabeaz.com Metaprogramming • In a nutshell: code

    that manipulates code • Common examples: • Decorators • Metaclasses • Descriptors • Essentially, it's doing things with code 4
  5. Copyright (C) 2013, http://www.dabeaz.com Why Would You Care? • Extensively

    used in frameworks and libraries • Better understanding of how Python works • It's fun • It solves a practical problem 5
  6. Copyright (C) 2013, http://www.dabeaz.com Don't Repeat Yourself • Highly repetitive

    code sucks • Tedious to write • Hard to read • Difficult to modify 9
  7. Copyright (C) 2013, http://www.dabeaz.com This Tutorial • A modern journey

    of metaprogramming • Highlight unique aspects of Python 3 • Explode your brain 10
  8. Copyright (C) 2013, http://www.dabeaz.com Reading 12 • Tutorial loosely based

    on content in "Python Cookbook, 3rd Ed." • Published May, 2013 • You'll find even more information in the book
  9. Copyright (C) 2013, http://www.dabeaz.com Caution • If you do everything

    in this tutorial all at once • You will either be fired • Or have permanent job security • It's a fine line 13
  10. Copyright (C) 2013, http://www.dabeaz.com Basic Building Blocks 15 def func(args):

    statement1 statement2 statement3 ... class A: def method1(self, args): statement1 statement2 def method2(self, args): statement1 statement2 ... statement1 statement2 statement3 ... Code
  11. Copyright (C) 2013, http://www.dabeaz.com Statements 16 statement1 statement2 statement3 ...

    • Perform the actual work of your program • Always execute in two scopes • globals - Module dictionary • locals - Enclosing function (if any) • exec(statements [, globals [, locals]])
  12. Copyright (C) 2013, http://www.dabeaz.com Functions 17 def func(x, y, z):

    statement1 statement2 statement3 ... • The fundamental unit of code in most programs • Module-level functions • Methods of classes
  13. Copyright (C) 2013, http://www.dabeaz.com Calling Conventions 18 def func(x, y,

    z): statement1 statement2 statement3 ... • Positional arguments func(1, 2, 3) • Keyword arguments func(x=1, z=3, y=2)
  14. Copyright (C) 2013, http://www.dabeaz.com Default Arguments 19 def func(x, debug=False,

    names=None): if names is None: names = [] ... func(1) func(1, names=['x', 'y']) • Default values set at definition time • Only use immutable values (e.g., None)
  15. Copyright (C) 2013, http://www.dabeaz.com *args and **kwargs 20 def func(*args,

    **kwargs): # args is tuple of position args # kwargs is dict of keyword args ... func(1, 2, x=3, y=4, z=5) args = (1, 2) kwargs = { 'x': 3, 'y': 4, 'z': 5 }
  16. Copyright (C) 2013, http://www.dabeaz.com *args and **kwargs 21 args =

    (1, 2) kwargs = { 'x': 3, 'y': 4, 'z': 5 } func(*args, **kwargs) func(1, 2, x=3, y=4, z=5) same as
  17. Copyright (C) 2013, http://www.dabeaz.com Keyword-Only Args 22 def recv(maxsize, *,

    block=True): ... def sum(*args, initial=0): ... • Named arguments appearing after '*' can only be passed by keyword recv(8192, block=False) # Ok recv(8192, False) # Error
  18. Copyright (C) 2013, http://www.dabeaz.com Closures 23 def make_adder(x, y): def

    add(): return x + y return add • You can make and return functions • Local variables are captured >>> a = make_adder(2, 3) >>> b = make_adder(10, 20) >>> a() 5 >>> b() 30 >>>
  19. Copyright (C) 2013, http://www.dabeaz.com Classes 24 class Spam: a =

    1 def __init__(self, b): self.b = b def imethod(self): pass >>> Spam.a # Class variable 1 >>> s = Spam(2) >>> s.b # Instance variable 2 >>> s.imethod() # Instance method >>>
  20. Copyright (C) 2013, http://www.dabeaz.com Different Method Types 25 class Spam:

    def imethod(self): pass @classmethod def cmethod(cls): pass @staticmethod def smethod(): pass s = Spam() s.imethod() Spam.cmethod() Spam.smethod() Usage
  21. Copyright (C) 2013, http://www.dabeaz.com Special Methods 26 class Array: def

    __getitem__(self, index): ... def __setitem__(self, index, value): ... def __delitem__(self, index): ... def __contains__(self, item): ... • Almost everything can be customized
  22. Copyright (C) 2013, http://www.dabeaz.com Inheritance 27 class Base: def spam(self):

    ... class Foo(Base): def spam(self): ... # Call method in base class r = super().spam()
  23. Copyright (C) 2013, http://www.dabeaz.com Dictionaries 28 class Spam: def __init__(self,

    x, y): self.x = x self.y = y def foo(self): pass • Objects are layered on dictionaries • Example: >>> s = Spam(2,3) >>> s.__dict__ {'y': 3, 'x': 2} >>> Spam.__dict__['foo'] <function Spam.foo at 0x10069fc20> >>>
  24. Copyright (C) 2013, http://www.dabeaz.com 29 "I love the smell of

    debugging in the morning." Metaprogramming Basics
  25. Copyright (C) 2013, http://www.dabeaz.com Problem: Debugging 30 • Will illustrate

    basics with a simple problem • Debugging • Not the only application, but simple enough to fit on slides
  26. Copyright (C) 2013, http://www.dabeaz.com Debugging with Print 31 def add(x,

    y): return x + y • A function • A function with debugging def add(x, y): print('add') return x + y • The one and only true way to debug...
  27. Copyright (C) 2013, http://www.dabeaz.com Many Functions w/ Debug 32 def

    add(x, y): print('add') return x + y def sub(x, y): print('sub') return x - y def mul(x, y): print('mul') return x * y def div(x, y): print('div') return x / y
  28. Copyright (C) 2013, http://www.dabeaz.com Many Functions w/ Debug 33 def

    add(x, y): print('add') return x + y def sub(x, y): print('sub') return x - y def mul(x, y): print('mul') return x * y def div(x, y): print('div') return x / y Bleah!
  29. Copyright (C) 2013, http://www.dabeaz.com Decorators • A decorator is a

    function that creates a wrapper around another function • The wrapper is a new function that works exactly like the original function (same arguments, same return value) except that some kind of extra processing is carried out 34
  30. Copyright (C) 2013, http://www.dabeaz.com A Debugging Decorator 35 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)
  31. Copyright (C) 2013, http://www.dabeaz.com A Debugging Decorator 36 from functools

    import wraps def debug(func): msg = func.__qualname__ @wraps(func) def wrapper(*args, **kwargs): print(msg) return func(*args, **kwargs) return wrapper A decorator creates a "wrapper" function
  32. Copyright (C) 2013, http://www.dabeaz.com A Debugging Decorator 37 from functools

    import wraps def debug(func): msg = func.__qualname__ @wraps(func) def wrapper(*args, **kwargs): print(msg) return func(*args, **kwargs) return wrapper A decorator creates a "wrapper" function A decorator creates a "wrapper" function Around a function that you provide
  33. Copyright (C) 2013, http://www.dabeaz.com Function Metadata 38 from functools import

    wraps def debug(func): msg = func.__qualname__ @wraps(func) def wrapper(*args, **kwargs): print(msg) return func(*args, **kwargs) return wrapper • @wraps copies metadata • Name and doc string • Function attributes
  34. Copyright (C) 2013, http://www.dabeaz.com The Metadata Problem • If you

    don't use @wraps, weird things happen 39 def add(x,y): "Adds x and y" return x+y add = debug(add) >>> add.__qualname__ 'wrapper' >>> add.__doc__ >>> help(add) Help on function wrapper in module __main__: wrapper(*args, **kwargs) >>>
  35. Copyright (C) 2013, http://www.dabeaz.com Decorator Syntax • The definition of

    a function and wrapping almost always occur together 40 • @decorator syntax performs the same steps def add(x,y): return x+y add = debug(add) @debug def add(x,y): return x+y
  36. Copyright (C) 2013, http://www.dabeaz.com Example Use 41 @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
  37. Copyright (C) 2013, http://www.dabeaz.com @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 Example Use 42 Each function is decorated, but there are no other implementation details
  38. Copyright (C) 2013, http://www.dabeaz.com Big Picture • Debugging code is

    isolated to single location • This makes it easy to change (or to disable) • User of a decorator doesn't worry about it • That's really the whole idea 43
  39. Copyright (C) 2013, http://www.dabeaz.com Variation: Logging 44 from functools import

    wraps import logging 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
  40. Copyright (C) 2013, http://www.dabeaz.com Variation: Optional Disable 45 from functools

    import wraps import os 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 • Key idea: Can change decorator independently of code that uses it
  41. Copyright (C) 2013, http://www.dabeaz.com Debugging with Print 46 • Everyone

    knows you really need a prefix • A function with debugging def add(x, y): print('add') return x + y def add(x, y): print('***add') return x + y • You know, for grepping...
  42. Copyright (C) 2013, http://www.dabeaz.com Decorators with Args 47 • Evaluates

    as • Calling convention @decorator(args) def func(): pass func = decorator(args)(func) • It's a little weird--two levels of calls
  43. Copyright (C) 2013, http://www.dabeaz.com Decorators with Args 48 from functools

    import wraps 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
  44. Copyright (C) 2013, http://www.dabeaz.com Decorators with Args 49 from functools

    import wraps 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 Outer function defines variables for use in regular decorator Normal decorator function
  45. Copyright (C) 2013, http://www.dabeaz.com A Reformulation 50 from functools import

    wraps, partial def debug(func=None, *, prefix=''): if func is None: return partial(debug, prefix=prefix) msg = prefix + func.__qualname__ @wraps(func) def wrapper(*args, **kwargs): print(msg) return func(*args, **kwargs) return wrapper • A test of your function calling skills...
  46. Copyright (C) 2013, http://www.dabeaz.com Usage 51 @debug def add(x, y):

    return x + y • Use as a simple decorator • Or as a decorator with optional configuration @debug(prefix='***') def add(x, y): return x + y
  47. Copyright (C) 2013, http://www.dabeaz.com Debug All Of This 52 class

    Spam: @debug def grok(self): pass @debug def bar(self): pass @debug def foo(self): pass • Debug all of the methods of a class • Can you decorate all methods at once?
  48. Copyright (C) 2013, http://www.dabeaz.com Class Decorator 53 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
  49. Copyright (C) 2013, http://www.dabeaz.com Example Use 54 @debugmethods class Spam:

    def grok(self): pass def bar(self): pass def foo(self): pass • One decorator application • Covers all definitions within the class • It even mostly works...
  50. Copyright (C) 2013, http://www.dabeaz.com Limitations 55 @debugmethods class BrokenSpam: @classmethod

    def grok(cls): # Not wrapped pass @staticmethod def bar(): # Not wrapped pass • Only instance methods get wrapped • Why? An exercise for the reader...
  51. Copyright (C) 2013, http://www.dabeaz.com Variation: Debug Access 56 def debugattr(cls):

    orig_getattribute = cls.__getattribute__ def __getattribute__(self, name): print('Get:', name) return orig_getattribute(self, name) cls.__getattribute__ = __getattribute__ return cls • Rewriting part of the class itself
  52. Copyright (C) 2013, http://www.dabeaz.com Example 57 @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 >>>
  53. Copyright (C) 2013, http://www.dabeaz.com Debug All The Classes 58 @debugmethods

    class Base: ... @debugmethods class Spam(Base): ... @debugmethods class Grok(Spam): ... @debugmethods class Mondo(Grok): ... • Many classes with debugging • Didn't we just solve this? • Bleah!!
  54. Copyright (C) 2013, http://www.dabeaz.com Solution: A Metaclass 59 class debugmeta(type):

    def __new__(cls, clsname, bases, clsdict): clsobj = super().__new__(cls, clsname, bases, clsdict) clsobj = debugmethods(clsobj) return clsobj class Base(metaclass=debugmeta): ... class Spam(Base): ... • Usage
  55. Copyright (C) 2013, http://www.dabeaz.com Solution: A Metaclass 60 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
  56. Copyright (C) 2013, http://www.dabeaz.com Solution: A Metaclass 61 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
  57. Copyright (C) 2013, http://www.dabeaz.com 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'> >>> 63
  58. Copyright (C) 2013, http://www.dabeaz.com Types and Classes • Classes define

    new types class Spam: pass >>> s = Spam() >>> type(s) <class '__main__.Spam'> >>> 64 • The class is the type of instances created • The class is a callable that creates instances
  59. Copyright (C) 2013, http://www.dabeaz.com Types of Classes • Classes are

    instances of types >>> type(int) <class 'type'> >>> type(list) <class 'type'> >>> type(Spam) <class 'type'> >>> isinstance(Spam, type) True >>> 65 • This requires some thought, but it should make some sense (classes are types)
  60. Copyright (C) 2013, http://www.dabeaz.com Creating Types • Types are their

    own class (builtin) class type: ... >>> type <class 'type'> >>> 66 • This class creates new "type" objects • Used when defining classes
  61. Copyright (C) 2013, http://www.dabeaz.com Classes Deconstructed • Consider a class:

    67 class Spam(Base): def __init__(self, name): self.name = name def bar(self): print "I'm Spam.bar" • What are its components? • Name ("Spam") • Base classes (Base,) • Functions (__init__,bar)
  62. Copyright (C) 2013, http://www.dabeaz.com Class Definition Process • What happens

    during class definition? • Step1: Body of class is isolated body = ''' def __init__(self, name): self.name = name def bar(self): print "I'm Spam.bar" ''' 68 class Spam(Base): def __init__(self, name): self.name = name def bar(self): print "I'm Spam.bar"
  63. Copyright (C) 2013, http://www.dabeaz.com Class Definition • Step 2: The

    class dictionary is created clsdict = type.__prepare__('Spam', (Base,)) • This dictionary serves as local namespace for statements in the class body • By default, it's a simple dictionary (more later) 69
  64. Copyright (C) 2013, http://www.dabeaz.com Class Definition • Step 3: Body

    is executed in returned dict exec(body, globals(), clsdict) • Afterwards, clsdict is populated >>> clsdict {'__init__': <function __init__ at 0x4da10>, 'bar': <function bar at 0x4dd70>} >>> 70
  65. Copyright (C) 2013, http://www.dabeaz.com 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 >>> 71
  66. Copyright (C) 2013, http://www.dabeaz.com Changing the Metaclass • metaclass keyword

    argument • Sets the class used for creating the type class Spam(metaclass=type): def __init__(self,name): self.name = name def bar(self): print "I'm Spam.bar" 72 • By default, it's set to 'type', but you can change it to something else
  67. Copyright (C) 2013, http://www.dabeaz.com Defining a New Metaclass • You

    typically inherit from type and redefine __new__ or __init__ class mytype(type): def __new__(cls, name, bases, clsdict): clsobj = super().__new__(cls, name, bases, clsdict) return clsobj 73 • To use class Spam(metaclass=mytype): ...
  68. Copyright (C) 2013, http://www.dabeaz.com Using a Metaclass • Metaclasses get

    information about class definitions at the time of definition • Can inspect this data • Can modify this data • Essentially, similar to a class decorator • Question: Why would you use one? 74
  69. Copyright (C) 2013, http://www.dabeaz.com Inheritance • Metaclasses propagate down hierarchies

    class Base(metaclass=mytype): ... class Spam(Base): # metaclass=mytype ... class Grok(Spam): # metaclass=mytype ... 75 • Think of it as a genetic mutation
  70. Copyright (C) 2013, http://www.dabeaz.com Solution: Reprise 76 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
  71. Copyright (C) 2013, http://www.dabeaz.com Debug The Universe 77 class Base(metaclass=debugmeta):

    ... class Spam(Base): ... class Grok(Spam): ... class Mondo(Grok): ... • Debugging gets applied across entire hierarchy • Implicitly applied in subclasses
  72. Copyright (C) 2013, http://www.dabeaz.com Big Picture • It's mostly about

    wrapping/rewriting • Decorators : Functions • Class Decorators: Classes • Metaclasses : Class hierarchies • You have the power to change things 78
  73. Copyright (C) 2013, http://www.dabeaz.com Journey So Far 80 • Have

    seen "classic" metaprogramming • Already widely used in Python 2 • Only a few Python 3 specific changes
  74. Copyright (C) 2013, http://www.dabeaz.com Journey to Come 81 • Let's

    build something more advanced • Using techniques discussed • And more...
  75. Copyright (C) 2013, http://www.dabeaz.com Problem : Structures 82 class Stock:

    def __init__(self, name, shares, price): self.name = name self.shares = shares self.price = price 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
  76. Copyright (C) 2013, http://www.dabeaz.com Problem : Structures 83 class Stock:

    def __init__(self, name, shares, price): self.name = name self.shares = shares self.price = price 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 Why must I keep writing these boilerplate init methods?
  77. Copyright (C) 2013, http://www.dabeaz.com A Solution : Inheritance 84 class

    Structure: _fields = [] def __init__(self, *args): if len(args) != self._fields: raise TypeError('Wrong # args') for name, val in zip(self._fields, args): setattr(self, name, val) class Stock(Structure): _fields = ['name', 'shares', 'price'] class Point(Structure): _fields = ['x', 'y'] class Host(Structure): _fields = ['address', 'port'] A generalized __init__()
  78. Copyright (C) 2013, http://www.dabeaz.com Usage 85 >>> 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 >>>
  79. Copyright (C) 2013, http://www.dabeaz.com Some Issues 86 >>> s =

    Stock('ACME', price=123.45, shares=50) Traceback (most recent call last): File "<stdin>", line 1, in <module> TypeError: __init__() got an unexpected keyword argument 'shares' >>> • No support for keyword args • Missing calling signatures >>> import inspect >>> print(inspect.signature(Stock)) (*args) >>>
  80. Copyright (C) 2013, http://www.dabeaz.com New Approach: Signatures 88 from inspect

    import Parameter, Signature fields = ['name', 'shares', 'price'] parms = [ Parameter(name, Parameter.POSITIONAL_OR_KEYWORD) for name in fields] sig = Signature(parms) • Build a function signature object • Signatures are more than just metadata
  81. Copyright (C) 2013, http://www.dabeaz.com Signature Binding 89 def func(*args, **kwargs):

    bound_args = sig.bind(*args, **kwargs) for name, val in bound_args.arguments.items(): print(name, '=', val) • Argument binding • sig.bind() binds positional/keyword args to signature • .arguments is an OrderedDict of passed values
  82. Copyright (C) 2013, http://www.dabeaz.com Signature Binding 90 • Example use:

    >>> func('ACME', 50, 91.1) name = ACME shares = 50 price = 91.1 >>> func('ACME', price=91.1, shares=50) name = ACME shares = 50 price = 91.1 • Notice: both positional/keyword args work
  83. Copyright (C) 2013, http://www.dabeaz.com Signature Binding 91 • Error handling

    >>> func('ACME', 50) Traceback (most recent call last): ... TypeError: 'price' parameter lacking default value >>> func('ACME', 50, 91.1, 92.3) Traceback (most recent call last): ... TypeError: too many positional arguments >>> • Binding: it just "works"
  84. Copyright (C) 2013, http://www.dabeaz.com Solution w/Signatures 92 from inspect import

    Parameter, Signature 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)
  85. Copyright (C) 2013, http://www.dabeaz.com Solution w/Signatures 93 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'])
  86. Copyright (C) 2013, http://www.dabeaz.com Solution w/Signatures 94 >>> s =

    Stock('ACME', shares=50, price=91.1) >>> s.name 'ACME' >>> s.shares 50 >>> s.price 91.1 >>> import inspect >>> print(inspect.signature(Stock)) (name, shares, price) >>>
  87. Copyright (C) 2013, http://www.dabeaz.com New Problem 95 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']) • This is rather annoying • Can't it be simplified in some way?
  88. Copyright (C) 2013, http://www.dabeaz.com Solutions 96 • Ah, a problem

    involving class definitions • Class decorators • Metaclasses • Which seems more appropriate? • Let's explore both options
  89. Copyright (C) 2013, http://www.dabeaz.com Class Decorators 97 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
  90. Copyright (C) 2013, http://www.dabeaz.com Metaclass Solution 98 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 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)
  91. Copyright (C) 2013, http://www.dabeaz.com Metaclass Solution 99 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 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) Read _fields attribute and make a proper signature out of it
  92. Copyright (C) 2013, http://www.dabeaz.com Usage 100 class Stock(Structure): _fields =

    ['name', 'shares', 'price'] class Point(Structure): _fields = ['x', 'y'] class Host(Structure): _fields = ['address', 'port'] • It's back to original 'simple' solution • Signatures are created behind scenes
  93. Copyright (C) 2013, http://www.dabeaz.com Considerations 101 class Structure(metaclass=StructMeta): _fields =

    [] ... def __repr__(self): args = ', '.join(repr(getattr(self, name)) for name in self._fields) return type(self).__name__ + \ '(' + args + ')' • How much will the Structure class be expanded? • Example: supporting methods • Is type checking important? isinstance(s, Structure)
  94. Copyright (C) 2013, http://www.dabeaz.com Advice 102 • Use a class

    decorator if the goal is to tweak classes that might be unrelated • Use a metaclass if you're trying to perform actions in combination with inheritance • Don't be so quick to dismiss techniques (e.g., 'metaclasses suck so .... blah blah') • All of the tools are meant to work together
  95. Copyright (C) 2013, http://www.dabeaz.com Owning the Dot 103 Q: "Who's

    in charge here?" A: "In charge? I don't know, man."
  96. Copyright (C) 2013, http://www.dabeaz.com Problem : Correctness 104 >>> s

    = Stock('ACME', 50, 91.1) >>> s.name = 42 >>> s.shares = 'a heck of a lot' >>> s.price = (23.45 + 2j) >>> • Types like a duck, rhymes with ... • Bah, real programmers use Haskell!
  97. Copyright (C) 2013, http://www.dabeaz.com Properties 105 class Stock(Structure): _fields =

    ['name', 'shares', 'price'] @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 • You can upgrade attributes to have checks (getter) (setter)
  98. Copyright (C) 2013, http://www.dabeaz.com Properties 106 >>> s = Stock('ACME',

    50, 91.1) >>> s.shares = 'a lot' Traceback (most recent call last): ... TypeError: expected int >>> s.shares = -10 Traceback (most recent call last): ... ValueError: Must be >= 0 >>> s.shares = 37 >>> s.shares 37 >>> • Example use:
  99. Copyright (C) 2013, http://www.dabeaz.com An Issue 107 @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 • It works, but it quickly gets annoying • Imagine writing same code for many attributes
  100. Copyright (C) 2013, http://www.dabeaz.com A Complexity 108 • Want to

    simplify, but how? • Two kinds of checking are intertwined • Type checking: int, float, str, etc. • Validation: >, >=, <, <=, !=, etc. • Question: How to structure it?
  101. Copyright (C) 2013, http://www.dabeaz.com Descriptor Protocol 109 • Properties are

    implemented via descriptors class Descriptor: def __get__(self, instance, cls): ... def __set__(self, instance, value): ... def __delete__(self, instance) ... • Customized processing of attribute access
  102. Copyright (C) 2013, http://www.dabeaz.com Descriptor Protocol 110 • Example: class

    Spam: x = Descriptor() s = Spam() s.x # x.__get__(s, Spam) s.x = value # x.__set__(s, value) del s.x # x.__delete__(s) • Customized handling of a specific attribute
  103. Copyright (C) 2013, http://www.dabeaz.com A Basic Descriptor 111 class Descriptor:

    def __init__(self, name=None): self.name = name def __get__(self, instance, cls): if instance is None: return self else: return instance.__dict__[self.name] def __set__(self, instance, value): instance.__dict__[self.name] = value def __delete__(self, instance): del instance.__dict__[self.name]
  104. Copyright (C) 2013, http://www.dabeaz.com A Basic Descriptor 112 class Descriptor:

    def __init__(self, name=None): self.name = name def __get__(self, instance, cls): if instance is None: return self else: return instance.__dict__[self.name] def __set__(self, instance, value): instance.__dict__[self.name] = value def __delete__(self, instance): del instance.__dict__[self.name] name of attribute being stored. A key in the instance dict.
  105. Copyright (C) 2013, http://www.dabeaz.com A Basic Descriptor 113 class Descriptor:

    def __init__(self, name=None): self.name = name def __get__(self, instance, cls): if instance is None: return self else: return instance.__dict__[self.name] def __set__(self, instance, value): instance.__dict__[self.name] = value def __delete__(self, instance): del instance.__dict__[self.name] Direct manipulation of the instance dict.
  106. Copyright (C) 2013, http://www.dabeaz.com A Simpler Descriptor 114 class Descriptor:

    def __init__(self, name=None): self.name = name def __set__(self, instance, value): instance.__dict__[self.name] = value def __delete__(self, instance): raise AttributeError("Can't delete") • You don't need __get__() if it merely returns the normal dictionary value
  107. Copyright (C) 2013, http://www.dabeaz.com Descriptor Usage 115 class Stock(Structure): _fields

    = ['name', 'shares', 'price'] name = Descriptor('name') shares = Descriptor('shares') price = Descriptor('price') • If it works, will capture set/delete operations >>> s = Stock('ACME', 50, 91.1) >>> s.shares 50 >>> s.shares = 50 # shares.__set__(s, 50) >>> del s.shares Traceback (most recent call last): ... AttributeError: Can't delete >>>
  108. Copyright (C) 2013, http://www.dabeaz.com Type Checking 116 class Typed(Descriptor): ty

    = object def __set__(self, instance, value): if not isinstance(value, self.ty): raise TypeError('Expected %s' % self.ty) super().__set__(instance, value) • Specialization class Integer(Typed): ty = int class Float(Typed): ty = float class String(Typed): ty = str
  109. Copyright (C) 2013, http://www.dabeaz.com Usage 117 • 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'> >>> class Stock(Structure): _fields = ['name', 'shares', 'price'] name = String('name') shares = Integer('shares') price = Float('price')
  110. Copyright (C) 2013, http://www.dabeaz.com Value Checking 118 class Positive(Descriptor): def

    __set__(self, instance, value): if value < 0: raise ValueError('Expected >= 0') super().__set__(instance, value) • Use as a mixin class class PosInteger(Integer, Positive): pass class PosFloat(Float, Positive): pass
  111. Copyright (C) 2013, http://www.dabeaz.com Usage 119 • 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'> >>> class Stock(Structure): _fields = ['name', 'shares', 'price'] name = String('name') shares = PosInteger('shares') price = PosFloat('price')
  112. Copyright (C) 2013, http://www.dabeaz.com Understanding the MRO 121 class PosInteger(Integer,

    Positive): pass >>> PosInteger.__mro__ (<class 'PosInteger'>, <class 'Integer'>, <class 'Typed'>, <class 'Positive'>, <class 'Descriptor'>, <class 'object'>) >>> This chain defines the order in which the value is checked by different __set__() methods • Base order matters (e.g., int before < 0)
  113. Copyright (C) 2013, http://www.dabeaz.com Length Checking 122 class Sized(Descriptor): def

    __init__(self, *args, maxlen, **kwargs): self.maxlen = maxlen super().__init__(*args, **kwargs) def __set__(self, instance, value): if len(value) > self.maxlen: raise ValueError('Too big') super().__set__(instance, value) • Use: class SizedString(String, Sized): pass
  114. Copyright (C) 2013, http://www.dabeaz.com Usage 123 • Example: >>> s

    = Stock('ACME', 50, 91.1) >>> s.name = 'ABRACADABRA' Traceback (most recent call last): ... ValueError: Too big >>> class Stock(Structure): _fields = ['name', 'shares', 'price'] name = SizedString('name', maxlen=8) shares = PosInteger('shares') price = PosFloat('price')
  115. Copyright (C) 2013, http://www.dabeaz.com Pattern Checking 124 import re class

    Regex(Descriptor): def __init__(self, *args, pat, **kwargs): self.pat = re.compile(pat) super().__init__(*args, **kwargs) def __set__(self, instance, value): if not self.pat.match(value): raise ValueError('Invalid string') super().__set__(instance, value) • Use: class SizedRegexString(String, Sized, Regex): pass
  116. Copyright (C) 2013, http://www.dabeaz.com Usage 125 • Example: >>> s

    = Stock('ACME', 50, 91.1) >>> s.name = 'Head Explodes!' Traceback (most recent call last): ... ValueError: Invalid string >>> class Stock(Structure): _fields = ['name', 'shares', 'price'] name = SizedRegexString('name', maxlen=8, pat='[A-Z]+$') shares = PosInteger('shares') price = PosFloat('price')
  117. Copyright (C) 2013, http://www.dabeaz.com Whoa, Whoa, Whoa 126 • Mixin

    classes with __init__() functions? class SizedRegexString(String, Sized, Regex): pass • Each with own unique signature a = String('name') b = Sized(maxlen=8) c = Regex(pat='[A-Z]+$') • This works, how?
  118. Copyright (C) 2013, http://www.dabeaz.com Keyword-only Args 127 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) SizedRegexString('name', maxlen=8, pat='[A-Z]+$')
  119. Copyright (C) 2013, http://www.dabeaz.com Keyword-only Args 128 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) SizedRegexString('name', maxlen=8, pat='[A-Z]+$') Keyword-only argument is isolated and removed from all other passed args
  120. Copyright (C) 2013, http://www.dabeaz.com Annoyance 130 • Still quite a

    bit of repetition • Signatures and type checking not unified • Maybe we can push it further class Stock(Structure): _fields = ['name', 'shares', 'price'] name = SizedRegexString('name', maxlen=8, pat='[A-Z]+$') shares = PosInteger('shares') price = PosFloat('price')
  121. Copyright (C) 2013, http://www.dabeaz.com A New Metaclass 132 from collections

    import OrderedDict class StructMeta(type): @classmethod def __prepare__(cls, name, bases): return OrderedDict() def __new__(cls, name, bases, clsdict): fields = [ key for key, val in clsdict.items() if isinstance(val, Descriptor) ] for name in fields: clsdict[name].name = name clsobj = super().__new__(cls, name, bases, dict(clsdict)) sig = make_signature(fields) setattr(clsobj, '__signature__', sig) return clsobj
  122. Copyright (C) 2013, http://www.dabeaz.com New Usage 133 • Oh, that's

    rather nice... class Stock(Structure): name = SizedRegexString(maxlen=8,pat='[A-Z]+$') shares = PosInteger() price = PosFloat()
  123. Copyright (C) 2013, http://www.dabeaz.com New Metaclass 134 from collections import

    OrderedDict class StructMeta(type): @classmethod def __prepare__(cls, name, bases): return OrderedDict() def __new__(cls, name, bases, clsdict): fields = [ key for key, val in clsdict.items() if isinstance(val, Descriptor) ] for name in fields: clsdict[name].name = name clsobj = super().__new__(cls, name, bases, dict(clsdict)) sig = make_signature(fields) setattr(clsobj, '__signature__', sig) return clsobj __prepare__() creates and returns dictionary to use for execution of the class body. An OrderedDict will preserve the definition order.
  124. Copyright (C) 2013, http://www.dabeaz.com Ordering of Definitions 135 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'>) )
  125. Copyright (C) 2013, http://www.dabeaz.com Duplicate Definitions 136 class NoDupOrderedDict(OrderedDict): def

    __setitem__(self, key, value): if key in self: raise NameError('%s already defined' % key) super().__setitem__(key, value) • If inclined, you could do even better • Make a new kind of dict • Use in place of OrderedDict
  126. Copyright (C) 2013, http://www.dabeaz.com Duplicate Definitions 137 class Stock(Structure): name

    = String() shares = PosInteger() price = PosFloat() shares = PosInteger() Traceback (most recent call last): File "<stdin>", line 1, in <module> File "<stdin>", line 5, in Stock File "./typestruct.py", line 107, in __setitem__ raise NameError('%s already defined' % key) NameError: shares already defined • Won't pursue further, but you get the idea
  127. Copyright (C) 2013, http://www.dabeaz.com New Metaclass 138 from collections import

    OrderedDict class StructMeta(type): @classmethod def __prepare__(cls, name, bases): return OrderedDict() def __new__(cls, name, bases, clsdict): fields = [ key for key, val in clsdict.items() if isinstance(val, Descriptor) ] for name in fields: clsdict[name].name = name clsobj = super().__new__(cls, name, bases, dict(clsdict)) sig = make_signature(fields) setattr(clsobj, '__signature__', sig) return clsobj Collect Descriptors and set their names
  128. Copyright (C) 2013, http://www.dabeaz.com Name Setting 139 class Stock(Structure): _fields

    = ['name', 'shares', 'price'] name = SizedRegexString('name', ...) shares = PosInteger('shares') price = PosFloat('price') • Old code • New Code class Stock(Structure): name = SizedRegexString(...) shares = PosInteger() price = PosFloat() Names are set from dict keys
  129. Copyright (C) 2013, http://www.dabeaz.com New Metaclass 140 from collections import

    OrderedDict class StructMeta(type): @classmethod def __prepare__(cls, name, bases): return OrderedDict() def __new__(cls, name, bases, clsdict): fields = [ key for key, val in clsdict.items() if isinstance(val, Descriptor) ] for name in fields: clsdict[name].name = name clsobj = super().__new__(cls, name, bases, dict(clsdict)) sig = make_signature(fields) setattr(clsobj, '__signature__', sig) return clsobj Make the class and signature exactly as before.
  130. Copyright (C) 2013, http://www.dabeaz.com New Metaclass 141 from collections import

    OrderedDict class StructMeta(type): @classmethod def __prepare__(cls, name, bases): return OrderedDict() def __new__(cls, name, bases, clsdict): fields = [ key for key, val in clsdict.items() if isinstance(val, Descriptor) ] for name in fields: clsdict[name].name = name clsobj = super().__new__(cls, name, bases, dict(clsdict)) sig = make_signature(fields) setattr(clsobj, '__signature__', sig) return clsobj A technicality: Must create a proper dict for class contents
  131. Copyright (C) 2013, http://www.dabeaz.com The Costs 143 • Option 1

    : Simple class Stock(Structure): name = SizedRegexString(...) shares = PosInteger() price = PosFloat() class Stock: def __init__(self, name, shares, price): self.name = name self.shares = shares self.price = price • Option 2 : Meta
  132. Copyright (C) 2013, http://www.dabeaz.com A Few Tests 144 • Instance

    creation s = Stock('ACME', 50, 91.1) • Attribute lookup s.price • Attribute assignment s.price = 10.0 • Attribute assignment s.name = 'ACME' 1.07s 91.8s 0.08s 0.08s 0.11s 3.40s 0.14s 8.14s Simple Meta (86x) (31x) (58x)
  133. Copyright (C) 2013, http://www.dabeaz.com A Few Tests 145 • Instance

    creation s = Stock('ACME', 50, 91.1) • Attribute lookup s.price • Attribute assignment s.price = 10.0 • Attribute assignment s.name = 'ACME' Simple Meta A bright spot 1.07s 91.8s 0.08s 0.08s 0.11s 3.40s 0.14s 8.14s (86x) (31x) (58x)
  134. Copyright (C) 2013, http://www.dabeaz.com Thoughts 146 • Several large bottlenecks

    • Signature enforcement • Multiple inheritance/super in descriptors • Can anything be done without a total rewrite?
  135. Copyright (C) 2013, http://www.dabeaz.com Code Generation 147 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 >>>
  136. Copyright (C) 2013, http://www.dabeaz.com Code Generation 148 class StructMeta(type): ...

    def __new__(cls, name, bases, clsdict): fields = [ key for key, val in clsdict.items() if isinstance(val, Descriptor) ] for name in fields: clsdict[name].name = name if fields: exec(_make_init(fields),globals(),clsdict) clsobj = super().__new__(cls, name, bases, dict(clsdict)) setattr(clsobj, '_fields', fields) return clsobj
  137. Copyright (C) 2013, http://www.dabeaz.com Code Generation 149 class StructMeta(type): ...

    def __new__(cls, name, bases, clsdict): fields = [ key for key, val in clsdict.items() if isinstance(val, Descriptor) ] for name in fields: clsdict[name].name = name if fields: exec(_make_init(fields),globals(),clsdict) clsobj = super().__new__(cls, name, bases, dict(clsdict)) setattr(clsobj, '_fields', fields) return clsobj No signature, but set _fields for code that wants it
  138. Copyright (C) 2013, http://www.dabeaz.com New Code 150 class Structure(metaclass=StructMeta): pass

    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
  139. Copyright (C) 2013, http://www.dabeaz.com New Thought 151 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) class Positive(Descriptor): def __set__(self, instance, value): if value < 0: raise ValueError('Expected >= 0') super().__set__(instance, value) Could you merge this code together?
  140. Copyright (C) 2013, http://www.dabeaz.com Reformulation 152 class Descriptor(metaclass=DescriptorMeta): def __init__(self,

    name=None): self.name = name @staticmethod def set_code(): return [ 'instance.__dict__[self.name] = value' ] def __delete__(self, instance): raise AttributeError("Can't delete") • Change __set__ to a method that returns source • Introduce a new metaclass (later)
  141. Copyright (C) 2013, http://www.dabeaz.com Reformulation 153 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")' ]
  142. Copyright (C) 2013, http://www.dabeaz.com Reformulation 154 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")' ]
  143. Copyright (C) 2013, http://www.dabeaz.com Reformulation 155 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")' ]
  144. Copyright (C) 2013, http://www.dabeaz.com Generating a Setter 156 def _make_setter(dcls):

    code = 'def __set__(self, instance, value):\n' for d in dcls.__mro__: if 'set_code' in d.__dict__: for line in d.set_code(): code += ' ' + line + '\n' return code • Takes a descriptor class as input • Walks its MRO and collects output of set_code() • Concatenate to make a __set__() method
  145. Copyright (C) 2013, http://www.dabeaz.com Example Setters 157 >>> 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 >>>
  146. Copyright (C) 2013, http://www.dabeaz.com Descriptor Metaclass 158 class DescriptorMeta(type): def

    __init__(self, clsname, bases, clsdict): if '__set__' not in clsdict: code = _make_setter(self) exec(code, globals(), clsdict) setattr(self, '__set__', clsdict['__set__']) else: raise TypeError('Define set_code()') class Descriptor(metaclass=DescriptorMeta): ... • For each Descriptor class, create setter code • exec() and drop result onto created class
  147. Copyright (C) 2013, http://www.dabeaz.com Just to be Clear 159 class

    Stock(Structure): name = SizedRegexString(...) shares = PosInteger() price = PosFloat() • User has no idea about this code generation • They're just using the same code as before • It's an implementation detail of descriptors
  148. Copyright (C) 2013, http://www.dabeaz.com New Performance 160 • Instance creation

    s = Stock('ACME',50,91.1) • Attribute lookup s.price • Attribute assignment s.price = 10.0 • Attribute assignment s.name = 'ACME' 1.07s 91.8s 7.19s 0.08s 0.08s 0.08s 0.11s 3.40s 1.11s 0.14s 8.14s 2.95s Simple Meta (86x) (6.7x) (31x) (10x) (58x) (21x) Exec
  149. Copyright (C) 2013, http://www.dabeaz.com Remaining Problem 162 class Stock(Structure): name

    = SizedRegexString(maxlen=8, pat='[A-Z]+$') shares = PosInteger() price = PosFloat() class Point(Structure): x = Integer() y = Integer() class Address(Structure): hostname = String() port = Integer() • Convincing a manager about all of this
  150. Copyright (C) 2013, http://www.dabeaz.com Solution: XML 163 <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>
  151. Copyright (C) 2013, http://www.dabeaz.com Solution: XML 164 <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> +5 extra credit Regex + XML
  152. Copyright (C) 2013, http://www.dabeaz.com XML to Classes 165 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 • XML Parsing • Continued...
  153. Copyright (C) 2013, http://www.dabeaz.com XML to Classes 166 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
  154. Copyright (C) 2013, http://www.dabeaz.com Example 167 >>> 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() >>>
  155. Copyright (C) 2013, http://www.dabeaz.com $$!!@!&!**!!! 168 • Now WHAT!?!? •

    Allow structure .xml files to be imported • Using the import statement • Yes!
  156. Copyright (C) 2013, http://www.dabeaz.com Import Hooks 169 >>> import sys

    >>> sys.meta_path [<class '_frozen_importlib.BuiltinImporter'>, <class '_frozen_importlib.FrozenImporter'>, <class '_frozen_importlib.PathFinder'>] >>> • sys.meta_path • A collection of importer/finder instances
  157. Copyright (C) 2013, http://www.dabeaz.com An Experiment 170 class MyImporter: def

    find_module(self, fullname, path=None): print('*** Looking for', fullname) return None >>> 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' >>> • Yes, you've plugged into the import statement
  158. Copyright (C) 2013, http://www.dabeaz.com Structure Importer 171 class StructImporter: def

    __init__(self, path): self._path = path def find_module(self, fullname, path=None): name = fullname.rpartition('.')[-1] if path is None: path = self._path for dn in path: filename = os.path.join(dn, name+'.xml') if os.path.exists(filename): return StructXmlLoader(filename) return None
  159. Copyright (C) 2013, http://www.dabeaz.com Structure Importer 172 class StructImporter: def

    __init__(self, path): self._path = path def find_module(self, fullname, path=None): name = fullname.rpartition('.')[-1] if path is None: path = self._path for dn in path: filename = os.path.join(dn, name+'.xml') if os.path.exists(filename): return StructXmlLoader(filename) return None Fully qualified module name Package path (if any)
  160. Copyright (C) 2013, http://www.dabeaz.com Structure Importer 173 class StructImporter: def

    __init__(self, path): self._path = path def find_module(self, fullname, path=None): name = fullname.rpartition('.')[-1] if path is None: path = self._path for dn in path: filename = os.path.join(dn, name+'.xml') if os.path.exists(filename): return StructXmlLoader(filename) return None Walk path, check for existence of .xml file and return a loader
  161. Copyright (C) 2013, http://www.dabeaz.com XML Module Loader 174 import imp

    class StructXMLLoader: def __init__(self, filename): self._filename = filename def load_module(self, fullname): mod = sys.modules.setdefault(fullname, imp.new_module(fullname)) mod.__file__ = self._filename mod.__loader__ = self code = _xml_to_code(self._filename) exec(code, mod.__dict__, mod.__dict__) return mod
  162. Copyright (C) 2013, http://www.dabeaz.com XML Module Loader 175 import imp

    class StructXMLLoader: def __init__(self, filename): self._filename = filename def load_module(self, fullname): mod = sys.modules.setdefault(fullname, imp.new_module(fullname)) mod.__file__ = self._filename mod.__loader__ = self code = _xml_to_code(self._filename) exec(code, mod.__dict__, mod.__dict__) return mod Create a new module and put in sys.modules
  163. Copyright (C) 2013, http://www.dabeaz.com XML Module Loader 176 import imp

    class StructXMLLoader: def __init__(self, filename): self._filename = filename def load_module(self, fullname): mod = sys.modules.setdefault(fullname, imp.new_module(fullname)) mod.__file__ = self._filename mod.__loader__ = self code = _xml_to_code(self._filename) exec(code, mod.__dict__, mod.__dict__) return mod Convert XML to code and exec() resulting source
  164. Copyright (C) 2013, http://www.dabeaz.com Installation and Use 177 • Add

    to sys.meta_path def install_importer(path=sys.path): sys.meta_path.append(StructImporter(path)) install_importer() • From this point, structure .xml files will import >>> import datadefs >>> s = datadefs.Stock('ACME', 50, 91.1) >>> s.name 'ACME' >>> datadefs <module 'datadefs' from './datadefs.xml'> >>>
  165. Copyright (C) 2013, http://www.dabeaz.com Look at the Source 178 >>>

    datadefs <module 'datadefs' from './datadefs.xml'> >>> >>> import inspect >>> print(inspect.getsource(datadefs)) <structures> <structure name="Stock"> <field type="SizedRegexString" maxlen="8" pat="'[A $'">name</field> <field type="PosInteger">shares</field> <field type="PosFloat">price</field> </structure> ...
  166. Copyright (C) 2013, http://www.dabeaz.com Extreme Power 180 class Stock(Structure): name

    = SizedRegexString(maxlen=8, pat='[A-Z]+$') shares = PosInteger() price = PosFloat() • Think about all of the neat things we did • Descriptors as building blocks • Hiding of annoying details (signatures, etc.) • Dynamic code generation • Even customizing import
  167. Copyright (C) 2013, http://www.dabeaz.com Hack or by Design? 181 •

    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.
  168. Copyright (C) 2013, http://www.dabeaz.com Python 3 FTW! 182 • Python

    3 makes a lot of little things easier • Example : Python 2 keyword-only args def __init__(self, *args, **kwargs): self.maxlen = kwargs.pop('maxlen') ... • In Python 3 def __init__(self, *args, maxlen, **kwargs): self.maxlen = maxlen ... • There are a lot of little things like this
  169. Copyright (C) 2013, http://www.dabeaz.com Just the Start 183 • We've

    only scratched the surface • Function annotations def add(x:int, y:int) -> int: return x + y • Non-local variables def outer(): x = 0 def inner(): nonlocal x x = newvalue ...
  170. Copyright (C) 2013, http://www.dabeaz.com Just the Start 184 • Context

    managers with m: ... • Frame-hacks import sys f = sys._getframe(1) • Parsing/AST-manipulation import ast
  171. Copyright (C) 2013, http://www.dabeaz.com You Can, But Should You? 185

    • Metaprogramming is not for "normal" coding • Frameworks/libraries are a different story • If using a framework, you may be using this features without knowing it • You can do a lot of cool stuff • OTOH: Keeping it simple is not a bad strategy
  172. Copyright (C) 2013, http://www.dabeaz.com That is All! 186 • Thanks

    for listening • Hope you learned a few new things • Buy the "Python Cookbook, 3rd Ed." (O'Reilly) • Twitter: @dabeaz