jamadagni
jamadagni

Reputation: 1264

Trying to create read-only slots using metaclasses

I wrote some code based on what I read here, here, and here.

#! /usr/bin/env python3

class ROSlotsType(type):
    def __new__(cls, name, bases, namespace, **kwds):
        roprops = namespace.pop("__roslots__")
        namespace["__slots__"] = tuple(("_" + propname) for propname in roprops)
        for propname in roprops:
            namespace[propname] = property(lambda self: getattr(self, "_" + propname)) # can't use self.__dict__ since it doesn't exist
        return type.__new__(cls, name, bases, namespace)

class Location(metaclass = ROSlotsType):
    __roslots__ = ["lat", "lon", "alt"]
    def __init__(self, lat, lon, alt = 0):
        self._lat = lat ; self._lon = lon ; self._alt = alt
    def __repr__(self):
        return "Location({}, {}, {})".format(self._lat, self._lon, self._alt)

place = Location(25.282, 82.956, 77.0)
print("Created object {}".format(place))

print("Accessing its attributes:", place.lat, place.lon, place.alt)

print("Trying to access its __dict__...")
try: place.__dict__
except:
    print("Caught exception; object has only __slots__: {}".format(place.__slots__))

print("Trying to set new property...")
try: place.name = "Varanasi"
except:
    print("Caught exception; cannot add new property")

print("Trying to modify read-only property...")
try: place.alt += 1
except:
    print("Caught exception; cannot modify read-only property")

Executing the above gives:

Created object Location(25.282, 82.956, 77.0)
Accessing its attributes: 77.0 77.0 77.0
Trying to access its __dict__...
Caught exception; object has only __slots__: ('_lat', '_lon', '_alt')
Trying to set new property...
Caught exception; cannot add new property
Trying to modify read-only property...
Caught exception; cannot modify read-only property

The slots and read-only behaviour work fine, but apparently there is some problem with the property getters, since while __repr__ which uses _lat and _lon directly is giving the correct values, the attribute accesses using place.lat and place.lon are instead giving the value of place.alt.

Please advise me on what is wrong with my code and how to fix it.

Upvotes: 0

Views: 125

Answers (1)

icktoofay
icktoofay

Reputation: 129001

The lambda here creates an anonymous function:

namespace[propname] = property(lambda self: getattr(self, "_" + propname))

That function references propname, which is a local variable of the function in which it was defined. Unfortunately, it's not copying the propname value at that moment—it's keeping a reference to that propname variable, and once you get around to actually using that function, the for loop has completed and propname is left with the last value in roprops; namely, alt.

To fix this, you can use a somewhat-hacky-but-widely-recognized way of capturing it by value rather than by reference: create a parameter that shadows the other variable, but with a default value with the value you want:

namespace[propname] = property(lambda self, propname=propname: getattr(self, "_" + propname))

As Karl Knechtel mentions in the comments, you can also use operator.attrgetter, which eliminates the hacky bits altogether:

namespace[propname] = property(operator.attrgetter('_' + propname))

Lastly, as your question was originally posted on Code Review, I'd note that you should probably run your code through pep8.

Upvotes: 3

Related Questions