# Declarative Metaclass Pattern
----
# +Łukasz Rekucki
----
# What the hell are you talking about ?
----
# Django Forms
!python
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",)
----
# Django Models
!python
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)
----
# Django Filters (by Alex Gaynor)
!python
class ProductFilter(django_filters.FilterSet):
price = django_filters.NumberFilter(
lookup_type='lt')
class Meta:
model = Product
fields = ['price', 'release_date']
----
# SQLAlchemy
!python
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))
----
# PyStruct
!python
@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)
----
# 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.
----
# Jak to działa ?
----
# Definicja klasy
----
## Składnia
.fx: partial_start
!python
# Python 2
class A(object):
x = 10
# Python 3
class A:
x = 10
# Co się właściwie stanie ?
----
## Rozwinięcie
.fx: partial
!python
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)
----
# Dlaczego ``type`` ?
----
# 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``.
!python
def meta(*args):
print(args)
return type(*args)
# Python 2
class B(A):
__metaclass__ = meta
# Python 3
class B(A, metaclass=meta):
pass
----
# Wybór metaklasy
Metaklasa jest wybierana na podstawie prostego
algorytmu:
!python
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
----
# Tworzenie instancji przez (meta)klasę
!python
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.
----
# 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.
!python
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
----
# Metaklasa (Python 2)
!python
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
----
# Metaklasa (Python 3)
!python
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
----
# Klasa ``Meta``
!python
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.
----
# Po co właściwie to wszystko ?
----
# Co można zrobić lepiej w Py3k ?
----
## Pola z nadklasy
!python
# 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"),))
----
## Pola z nadklasy
!python
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"),)
----
## Pozbyć się ``Meta``
!python
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
----
## Pozbyć się ``Meta``
!python
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)
----
# Przykład: [https://gist.github.com/1368346](https://gist.github.com/1368346)
----
# Pomysły ?