Declarative Metaclass Pattern

Presenter Notes

+Łukasz Rekucki

Presenter Notes

What the hell are you talking about ?

Presenter Notes

Django Forms

class UserCreationForm(forms.ModelForm):
    """
    A form that creates a user, with no privileges, 
    from the given username and password.
    """
    username = forms.RegexField(
                            label=_("Username"), 
                            max_length=30, 
                            regex=r'^[\w.@+-]+$')

    password1 = forms.CharField(
                            label=_("Password"), 
                            widget=forms.PasswordInput)

    password2 = forms.CharField(
                            label=_("Password confirmation"), 
                            widget=forms.PasswordInput)

    class Meta: # Meta, WTF ?
        model = User
        fields = ("username",)

Presenter Notes

Django Models

class Comment(BaseCommentAbstractModel):
    """
    A user comment about some object.
    """
    user        = models.ForeignKey(User, verbose_name=_('user'),
                    blank=True, null=True, 
                    related_name="%(class)s_comments")

    user_name   = models.CharField(_("user's name"), 
                    max_length=50, blank=True)
    user_email  = models.EmailField(_("user's email address"), 
                    blank=True)
    user_url    = models.URLField(_("user's URL"), blank=True)

    comment = models.TextField(_('comment'), 
                    max_length=COMMENT_MAX_LENGTH)

    # Metadata about the comment
    submit_date = models.DateTimeField(_('date/time submitted'), 
                    default=None)
    ip_address  = models.IPAddressField(_('IP address'),
                    blank=True, null=True)

Presenter Notes

Django Filters (by Alex Gaynor)

class ProductFilter(django_filters.FilterSet):

    price = django_filters.NumberFilter(
                lookup_type='lt')

    class Meta:
        model = Product
        fields = ['price', 'release_date']

Presenter Notes

SQLAlchemy

from sqlalchemy.ext.declarative import declarative_base

Base = declarative_base()

class Person(Base):
    __tablename__ = 'people'

    id = Column(Integer, primary_key=True)
    discriminator = Column('type', String(50))
    __mapper_args__ = {'polymorphic_on': discriminator}

class Engineer(Person):
    __tablename__ = 'engineers'
    __mapper_args__ = {'polymorphic_identity': 'engineer'}

    id = Column(Integer, ForeignKey('people.id'), 
        primary_key=True)
    primary_language = Column(String(50))

Presenter Notes

PyStruct

@inpacket(0x01)
class WelcomePacket(GaduPacket):
    seed = numeric.IntField(0)

@inpacket(0x05)
class MessageAckPacket(GaduPacket): #SendMsgAck
    MSG_STATUS = Enum({
        'BLOCKED': 0x0001, 'DELIVERED': 0x0002,
        'QUEUED': 0x0003, 'MBOXFULL': 0x0004,
        'NOT_DELIVERED': 0x0006
    })

    msg_status  = numeric.IntField(0)
    recipient   = numeric.IntField(1)
    seq         = numeric.IntField(2)

Presenter Notes

Cechy wspólne

  • Klasa zawiera definicję "pól" w swojej treści. Opis danych, zamiast opisu zachowania.
  • Nie budujemy formularza. Podajemy tylko jakie właściwości ma on mieć.
  • Kolejność definicji ma znaczenie. Kolumny w tabeli zostaną utworzone w podanej kolejności. Tak samo ze struktrami w PyStruct.
  • Nazwy specjalne: __tablename__, `class Meta``, które są tylko w definicji.

Presenter Notes

Jak to działa ?

Presenter Notes

Definicja klasy

Presenter Notes

Składnia

# Python 2
class A(object):
    x = 10

# Python 3
class A:
    x = 10

# Co się właściwie stanie ?

Presenter Notes

Rozwinięcie

name = "A"
bases = (object,)

def _A():
  x = 10

# Python 2
body = {}

# Python 3
body = type.__prepare__(name, bases)

# i dalej:
exec(_A.__code__, body, globals())
A = type(name, bases, body)

Presenter Notes

Dlaczego type ?

Presenter Notes

Metaklasa

Formalnie jest to dowolny obiekt wywoływalny (ang. callable) przyjmujący wymienione trzy argumenty: name, bases i body.

(Py3k) Jeśli obiekt ma atrybut __prepare__, to jest wywoływany z argumentami name i bases do stworzenia body.

def meta(*args):
    print(args)
    return type(*args)

# Python 2
class B(A):
    __metaclass__ = meta

# Python 3
class B(A, metaclass=meta):
    pass

Presenter Notes

Wybór metaklasy

Metaklasa jest wybierana na podstawie prostego algorytmu:

if meta:
   isclass = issubclass(meta, type)
else:
   if not bases:
       meta = type
   else:
       meta = bases[0].__class__
   isclass = True

if isclass:
    for base in bases:
        if issubclass(meta, base):
            continue
        if issubclass(base, meta):
            meta = base
            continue
        raise TypeError("Konflikt")
return meta


Presenter Notes

Tworzenie instancji przez (meta)klasę

A = meta(name, bases, body)

# znów rozwijamy:
A = meta.__call__(name, bases, body)

# jeśli ``meta`` jest klasą
A = meta.__new__(name, bases, body)
if isinstance(A, meta):
    meta.__init__(A, name, bases, body)

Będziemy więc nadpisywać __new__ albo __init__ w zależności od potrzeb.

Presenter Notes

Klasa dla pól

Potrzebujemy osobnej klasy bazowej dla pól (może być abstrakcyjna, patrz moduł abc), aby móc rozróżnić je od zwykłych metod.

Musimy też rozwiązać jakoś problem kolejności, gdyż domyślnie nazwy zdefiniowane w ciele klasy są wkładane do słownika.

class BaseField(object):
    _creation_counter = 0

    def __new__(cls, *args, **kwargs):
        instance = super(BaseField, cls).__new__(cls, *args, **kwargs)
        instance._creation_counter = BaseField._creation_counter
        BaseField._creation_counter += 1

Presenter Notes

Metaklasa (Python 2)

class DMeta(type):

    def __new__(mcls, name, bases, body):
        fields = []
        for name, field in body.items():
            if not isinstance(field, BaseField):
                continue
            body.pop(name) # opcjonalne
            fields.append((name, field))
        fields.sort(key=lambda f: f[1]._creation_counter)

        cls = super(DMeta, mcls).__new__(mcls, name, bases, body)

        # Zsumuj definicję
        base_fields = []
        for base in bases:
            base_fields.extend(base._fields.items())
        cls._fields = collections.OrderedDict(base_fields + fields)

        for name, field in cls._fields.items():
            field.contribute_to_class(name, cls)
        return cls

Presenter Notes

Metaklasa (Python 3)

class DMeta(type):

    @classmethod
    def __prepare__(mcls, name, bases):
        return collections.OrderedDict()

    def __new__(mcls, name, bases, body):
        fields = []
        for name, field in body.items():
            if not hasattr("contribute_to_class", field):
                continue
            body.pop(name) # opcjonalne
            fields.append((name, field))

        cls = super().__new__(mcls, name, bases, body)

        # Zsumuj definicję
        base_fields = []
        for base in bases:
            base_fields.extend(base._fields.items())
        cls._fields = collections.OrderedDict(base_fields + fields)

        for name, field in cls._fields.items():
            field.contribute_to_class(name, cls)
        return cls

Presenter Notes

Klasa Meta

class Struct(metaclass=DMeta):
    one = Field()
    two = Field()

    class Meta:
        unique_together = [('one', 'two')]

Pozwala definiować dodatkowe własności nie związane z konkretnym polem unikająć kolizji z nazwami pól, bez używania wszędzie __ i nie zaśmiecając klasy.

Presenter Notes

Po co właściwie to wszystko ?

Presenter Notes

Co można zrobić lepiej w Py3k ?

Presenter Notes

Pola z nadklasy

# Formularze Django:

class ArticleForm(forms.Form):
    title = forms.CharField()
    category = forms.CharField(max_length=1, 
                    choices=(("N", "News"), ("R", "Review"))

# Teraz chcemy stworzyć specjalny formularz dla News, więc
# musimy ograniczyć category

# Nie zadziała:
class NewsForm(ArticleForm):
    category.choices = ((1, "News"),)

# Można tak, ale musimy powtórzyć 
# wszystkie inne opcje
class NewsForm(ArticleForm):
    category = forms.CharField(max_length=1, 
                    choices=(("N", "News"),))

Presenter Notes

Pola z nadklasy

class ExtMeta(DMeta):

    @classmethod
    def __prepare__(mcls, name, bases):
        d = super().__prepare__(name, bases)
        for base in bases:
            for name, field in base._fields.items():
                d[name] = field.clone()
        return d

class NewsForm(ArticleForm, metaclass=ExtMeta):
    category.choices = ((1, "News"),)


Presenter Notes

Pozbyć się Meta

class Options(object):

    def __init__(self, *base_options):
        self.unique_tuples = []
        for base in base_options:
            self.unique_tuples.extend(base.unique_tuples)

    def add_unique(self, *fields):
        self.unique_tuples.append(fields)

class OptsMeta(ExtMeta):

    @classmethod
    def __prepare__(mcls, name, bases, **kwargs):
        d = super().__prepare__(name, bases)
        d["Meta"] = Options(*[base._options for base in bases])
        return d

    def __new__(mcls, name, bases, body, **kwargs):
        options = body.pop("Meta")
        cls = super().__new__(mcls, name, bases, body)
        cls._options = options
        return cls

    def __init__(cls, name, bases, body, abstract=False):
        cls.abstract = abstract

Presenter Notes

Pozbyć się Meta

class Field:
    def contribute_to_class(self, name, cls):
        pass

    def clone(self):
        return copy.copy(self)

class Base(metaclass=OptsMeta):
    pass

class A(Base, abstract=True):
    a = Field()

class B(A):
    b = Field()

    Meta.add_unique(a, b)

Presenter Notes

Presenter Notes

Pomysły ?

Presenter Notes