[Tutor] question about metaclasses

Peter Otten __peter__ at web.de
Wed Jan 10 13:29:58 EST 2018


Albert-Jan Roskam wrote:

> Why does following the line (in #3) 

> # 3-------------------------------------------------
> class Meta(type):
>     def __new__(cls, name, bases, attrs):
>         for attr, obj in attrs.items():
>             if attr.startswith('_'):
>                 continue
>             elif not isinstance(obj, property):
>                 import pdb;pdb.set_trace()
>                 #setattr(cls, attr, property(lambda self: obj))  #
>                 #incorrect!
>                 raise ValueError("Only properties allowed")
>         return super().__new__(cls, name, bases, attrs)
> 
> class MyReadOnlyConst(metaclass=Meta):
>     __metaclass__ = Meta
>     YES = property(lambda self: 1)
>     NO = property(lambda self: 0)
>     DUNNO = property(lambda self: 42)
>     THROWS_ERROR = 666
> 
> 
> c2 = MyReadOnlyConst()
> print(c2.THROWS_ERROR)
> #c2.THROWS_ERROR = 777
> #print(c2.THROWS_ERROR)

> not convert the normal attribute into
> a property?
> 
> setattr(cls, attr, property(lambda self: obj))  # incorrect!

cls is Meta itself, not MyReadOnlyConst (which is an instance of Meta).
When the code in Meta.__new__() executes MyReadOnlyConst does not yet exist,
but future attributes are already there, in the form of the attrs dict.
Thus to convert the integer value into a read-only property you can 
manipulate that dict (or the return value of super().__new__()):

class Meta(type):
    def __new__(cls, name, bases, attrs):
        for attr, obj in attrs.items():
            if attr.startswith('_'):
                continue
            elif not isinstance(obj, property):
                attrs[attr] = property(lambda self, obj=obj: obj)
                
        return super().__new__(cls, name, bases, attrs)

class MyReadOnlyConst(metaclass=Meta):
    YES = property(lambda self: 1)
    NO = property(lambda self: 0)
    DUNNO = property(lambda self: 42)
    THROWS_ERROR = 666

c = MyReadOnlyConst()
try:
    c.THROWS_ERROR = 42
except AttributeError:
    pass
else:
    assert False
assert c.THROWS_ERROR == 666

PS: If you don't remember why the obj=obj is necessary:
Python uses late binding; without that trick all lambda functions would 
return the value bound to the obj name when the for loop has completed.
A simplified example:

>>> fs = [lambda: x for x in "abc"]
>>> fs[0](), fs[1](), fs[2]()
('c', 'c', 'c')
>>> fs = [lambda x=x: x for x in "abc"]
>>> fs[0](), fs[1](), fs[2]()
('a', 'b', 'c')




More information about the Tutor mailing list