py: Combine load_attr and store_attr type methods into one (attr).

This simplifies the API for objects and reduces code size (by around 400
bytes on Thumb2, and around 2k on x86).  Performance impact was measured
with Pystone score, but change was barely noticeable.
diff --git a/extmod/moductypes.c b/extmod/moductypes.c
index 7e7128a..f9f0ca7 100644
--- a/extmod/moductypes.c
+++ b/extmod/moductypes.c
@@ -482,13 +482,17 @@
     return MP_OBJ_NULL;
 }
 
-STATIC void uctypes_struct_load_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
-    mp_obj_t val = uctypes_struct_attr_op(self_in, attr, MP_OBJ_NULL);
-    *dest = val;
-}
-
-STATIC bool uctypes_struct_store_attr(mp_obj_t self_in, qstr attr, mp_obj_t val) {
-    return uctypes_struct_attr_op(self_in, attr, val) != MP_OBJ_NULL;
+STATIC void uctypes_struct_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
+    if (dest[0] == MP_OBJ_NULL) {
+        // load attribute
+        mp_obj_t val = uctypes_struct_attr_op(self_in, attr, MP_OBJ_NULL);
+        dest[0] = val;
+    } else {
+        // delete/store attribute
+        if (uctypes_struct_attr_op(self_in, attr, dest[1]) != MP_OBJ_NULL) {
+            dest[0] = MP_OBJ_NULL; // indicate success
+        }
+    }
 }
 
 STATIC mp_obj_t uctypes_struct_subscr(mp_obj_t self_in, mp_obj_t index_in, mp_obj_t value) {
@@ -589,8 +593,7 @@
     .name = MP_QSTR_struct,
     .print = uctypes_struct_print,
     .make_new = uctypes_struct_make_new,
-    .load_attr = uctypes_struct_load_attr,
-    .store_attr = uctypes_struct_store_attr,
+    .attr = uctypes_struct_attr,
     .subscr = uctypes_struct_subscr,
 };
 
diff --git a/py/obj.h b/py/obj.h
index 448db76..a5423f0 100644
--- a/py/obj.h
+++ b/py/obj.h
@@ -265,8 +265,7 @@
 typedef mp_obj_t (*mp_call_fun_t)(mp_obj_t fun, mp_uint_t n_args, mp_uint_t n_kw, const mp_obj_t *args);
 typedef mp_obj_t (*mp_unary_op_fun_t)(mp_uint_t op, mp_obj_t);
 typedef mp_obj_t (*mp_binary_op_fun_t)(mp_uint_t op, mp_obj_t, mp_obj_t);
-typedef void (*mp_load_attr_fun_t)(mp_obj_t self_in, qstr attr, mp_obj_t *dest); // for fail, do nothing; for attr, dest[0] = value; for method, dest[0] = method, dest[1] = self
-typedef bool (*mp_store_attr_fun_t)(mp_obj_t self_in, qstr attr, mp_obj_t value); // return true if store succeeded; if value==MP_OBJ_NULL then delete
+typedef void (*mp_attr_fun_t)(mp_obj_t self_in, qstr attr, mp_obj_t *dest);
 typedef mp_obj_t (*mp_subscr_fun_t)(mp_obj_t self_in, mp_obj_t index, mp_obj_t value);
 
 typedef struct _mp_method_t {
@@ -330,8 +329,18 @@
     mp_unary_op_fun_t unary_op;     // can return MP_OBJ_NULL if op not supported
     mp_binary_op_fun_t binary_op;   // can return MP_OBJ_NULL if op not supported
 
-    mp_load_attr_fun_t load_attr;
-    mp_store_attr_fun_t store_attr; // if value is MP_OBJ_NULL, then delete that attribute
+    // implements load, store and delete attribute
+    //
+    // dest[0] = MP_OBJ_NULL means load
+    //  return: for fail, do nothing
+    //          for attr, dest[0] = value
+    //          for method, dest[0] = method, dest[1] = self
+    //
+    // dest[0,1] = {MP_OBJ_SENTINEL, MP_OBJ_NULL} means delete
+    // dest[0,1] = {MP_OBJ_SENTINEL, object} means store
+    //  return: for fail, do nothing
+    //          for success set dest[0] = MP_OBJ_NULL
+    mp_attr_fun_t attr;
 
     mp_subscr_fun_t subscr;         // implements load, store, delete subscripting
                                     // value=MP_OBJ_NULL means delete, value=MP_OBJ_SENTINEL means load, else store
diff --git a/py/objboundmeth.c b/py/objboundmeth.c
index 84d2012..0f9ff08 100644
--- a/py/objboundmeth.c
+++ b/py/objboundmeth.c
@@ -71,7 +71,11 @@
 }
 
 #if MICROPY_PY_FUNCTION_ATTRS
-STATIC void bound_meth_load_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
+STATIC void bound_meth_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
+    if (dest[0] != MP_OBJ_NULL) {
+        // not load attribute
+        return;
+    }
     if (attr == MP_QSTR___name__) {
         mp_obj_bound_meth_t *o = self_in;
         dest[0] = MP_OBJ_NEW_QSTR(mp_obj_fun_get_name(o->meth));
@@ -87,7 +91,7 @@
 #endif
     .call = bound_meth_call,
 #if MICROPY_PY_FUNCTION_ATTRS
-    .load_attr = bound_meth_load_attr,
+    .attr = bound_meth_attr,
 #endif
 };
 
diff --git a/py/objcomplex.c b/py/objcomplex.c
index 1415068..f4a6885 100644
--- a/py/objcomplex.c
+++ b/py/objcomplex.c
@@ -141,7 +141,11 @@
     return mp_obj_complex_binary_op(op, lhs->real, lhs->imag, rhs_in);
 }
 
-STATIC void complex_load_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
+STATIC void complex_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
+    if (dest[0] != MP_OBJ_NULL) {
+        // not load attribute
+        return;
+    }
     mp_obj_complex_t *self = self_in;
     if (attr == MP_QSTR_real) {
         dest[0] = mp_obj_new_float(self->real);
@@ -157,7 +161,7 @@
     .make_new = complex_make_new,
     .unary_op = complex_unary_op,
     .binary_op = complex_binary_op,
-    .load_attr = complex_load_attr,
+    .attr = complex_attr,
 };
 
 mp_obj_t mp_obj_new_complex(mp_float_t real, mp_float_t imag) {
diff --git a/py/objexcept.c b/py/objexcept.c
index 987a54f..2d23f16 100644
--- a/py/objexcept.c
+++ b/py/objexcept.c
@@ -140,7 +140,11 @@
     }
 }
 
-STATIC void exception_load_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
+STATIC void exception_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
+    if (dest[0] != MP_OBJ_NULL) {
+        // not load attribute
+        return;
+    }
     mp_obj_exception_t *self = self_in;
     if (attr == MP_QSTR_args) {
         dest[0] = self->args;
@@ -168,7 +172,7 @@
     .name = MP_QSTR_BaseException,
     .print = mp_obj_exception_print,
     .make_new = mp_obj_exception_make_new,
-    .load_attr = exception_load_attr,
+    .attr = exception_attr,
     .locals_dict = (mp_obj_t)&exc_locals_dict,
 };
 
@@ -181,7 +185,7 @@
     .name = MP_QSTR_ ## exc_name, \
     .print = mp_obj_exception_print, \
     .make_new = mp_obj_exception_make_new, \
-    .load_attr = exception_load_attr, \
+    .attr = exception_attr, \
     .bases_tuple = (mp_obj_t)&mp_type_ ## base_name ## _base_tuple, \
 };
 
diff --git a/py/objfun.c b/py/objfun.c
index 76adfef..f00a90a 100644
--- a/py/objfun.c
+++ b/py/objfun.c
@@ -296,7 +296,11 @@
 }
 
 #if MICROPY_PY_FUNCTION_ATTRS
-STATIC void fun_bc_load_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
+STATIC void fun_bc_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
+    if (dest[0] != MP_OBJ_NULL) {
+        // not load attribute
+        return;
+    }
     if (attr == MP_QSTR___name__) {
         dest[0] = MP_OBJ_NEW_QSTR(mp_obj_fun_get_name(self_in));
     }
@@ -311,7 +315,7 @@
 #endif
     .call = fun_bc_call,
 #if MICROPY_PY_FUNCTION_ATTRS
-    .load_attr = fun_bc_load_attr,
+    .attr = fun_bc_attr,
 #endif
 };
 
diff --git a/py/objmodule.c b/py/objmodule.c
index 02292ff..971c7f3 100644
--- a/py/objmodule.c
+++ b/py/objmodule.c
@@ -51,48 +51,48 @@
     print(env, "<module '%s'>", name);
 }
 
-STATIC void module_load_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
+STATIC void module_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
     mp_obj_module_t *self = self_in;
-    mp_map_elem_t *elem = mp_map_lookup(&self->globals->map, MP_OBJ_NEW_QSTR(attr), MP_MAP_LOOKUP);
-    if (elem != NULL) {
-        dest[0] = elem->value;
-    }
-}
-
-STATIC bool module_store_attr(mp_obj_t self_in, qstr attr, mp_obj_t value) {
-    mp_obj_module_t *self = self_in;
-    mp_obj_dict_t *dict = self->globals;
-    if (dict->map.is_fixed) {
-        #if MICROPY_CAN_OVERRIDE_BUILTINS
-        if (dict == &mp_module_builtins_globals) {
-            if (MP_STATE_VM(mp_module_builtins_override_dict) == NULL) {
-                MP_STATE_VM(mp_module_builtins_override_dict) = mp_obj_new_dict(1);
-            }
-            dict = MP_STATE_VM(mp_module_builtins_override_dict);
-        } else
-        #endif
-        {
-            // can't delete or store to fixed map
-            return false;
+    if (dest[0] == MP_OBJ_NULL) {
+        // load attribute
+        mp_map_elem_t *elem = mp_map_lookup(&self->globals->map, MP_OBJ_NEW_QSTR(attr), MP_MAP_LOOKUP);
+        if (elem != NULL) {
+            dest[0] = elem->value;
         }
-    }
-    if (value == MP_OBJ_NULL) {
-        // delete attribute
-        mp_obj_dict_delete(dict, MP_OBJ_NEW_QSTR(attr));
     } else {
-        // store attribute
-        // TODO CPython allows STORE_ATTR to a module, but is this the correct implementation?
-        mp_obj_dict_store(dict, MP_OBJ_NEW_QSTR(attr), value);
+        // delete/store attribute
+        mp_obj_dict_t *dict = self->globals;
+        if (dict->map.is_fixed) {
+            #if MICROPY_CAN_OVERRIDE_BUILTINS
+            if (dict == &mp_module_builtins_globals) {
+                if (MP_STATE_VM(mp_module_builtins_override_dict) == NULL) {
+                    MP_STATE_VM(mp_module_builtins_override_dict) = mp_obj_new_dict(1);
+                }
+                dict = MP_STATE_VM(mp_module_builtins_override_dict);
+            } else
+            #endif
+            {
+                // can't delete or store to fixed map
+                return;
+            }
+        }
+        if (dest[1] == MP_OBJ_NULL) {
+            // delete attribute
+            mp_obj_dict_delete(dict, MP_OBJ_NEW_QSTR(attr));
+        } else {
+            // store attribute
+            // TODO CPython allows STORE_ATTR to a module, but is this the correct implementation?
+            mp_obj_dict_store(dict, MP_OBJ_NEW_QSTR(attr), dest[1]);
+        }
+        dest[0] = MP_OBJ_NULL; // indicate success
     }
-    return true;
 }
 
 const mp_obj_type_t mp_type_module = {
     { &mp_type_type },
     .name = MP_QSTR_module,
     .print = module_print,
-    .load_attr = module_load_attr,
-    .store_attr = module_store_attr,
+    .attr = module_attr,
 };
 
 mp_obj_t mp_obj_new_module(qstr module_name) {
diff --git a/py/objnamedtuple.c b/py/objnamedtuple.c
index 1996b41..9cc6da1 100644
--- a/py/objnamedtuple.c
+++ b/py/objnamedtuple.c
@@ -68,20 +68,20 @@
     print(env, ")");
 }
 
-STATIC void namedtuple_load_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
-    mp_obj_namedtuple_t *self = self_in;
-    int id = namedtuple_find_field((mp_obj_namedtuple_type_t*)self->tuple.base.type, attr);
-    if (id == -1) {
-        return;
+STATIC void namedtuple_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
+    if (dest[0] == MP_OBJ_NULL) {
+        // load attribute
+        mp_obj_namedtuple_t *self = self_in;
+        int id = namedtuple_find_field((mp_obj_namedtuple_type_t*)self->tuple.base.type, attr);
+        if (id == -1) {
+            return;
+        }
+        dest[0] = self->tuple.items[id];
+    } else {
+        // delete/store attribute
+        // provide more detailed error message than we'd get by just returning
+        nlr_raise(mp_obj_new_exception_msg(&mp_type_AttributeError, "can't set attribute"));
     }
-    dest[0] = self->tuple.items[id];
-}
-
-STATIC bool namedtuple_store_attr(mp_obj_t self_in, qstr attr, mp_obj_t value) {
-    (void)self_in;
-    (void)attr;
-    (void)value;
-    nlr_raise(mp_obj_new_exception_msg(&mp_type_AttributeError, "can't set attribute"));
 }
 
 STATIC mp_obj_t namedtuple_make_new(mp_obj_t type_in, mp_uint_t n_args, mp_uint_t n_kw, const mp_obj_t *args) {
@@ -154,8 +154,7 @@
     o->base.make_new = namedtuple_make_new;
     o->base.unary_op = mp_obj_tuple_unary_op;
     o->base.binary_op = mp_obj_tuple_binary_op;
-    o->base.load_attr = namedtuple_load_attr;
-    o->base.store_attr = namedtuple_store_attr;
+    o->base.attr = namedtuple_attr;
     o->base.subscr = mp_obj_tuple_subscr;
     o->base.getiter = mp_obj_tuple_getiter;
     o->base.bases_tuple = (mp_obj_t)&namedtuple_base_tuple;
diff --git a/py/objrange.c b/py/objrange.c
index ff62cc5..9f7e659 100644
--- a/py/objrange.c
+++ b/py/objrange.c
@@ -168,7 +168,11 @@
 
 
 #if MICROPY_PY_BUILTINS_RANGE_ATTRS
-STATIC void range_load_attr(mp_obj_t o_in, qstr attr, mp_obj_t *dest) {
+STATIC void range_attr(mp_obj_t o_in, qstr attr, mp_obj_t *dest) {
+    if (dest[0] != MP_OBJ_NULL) {
+        // not load attribute
+        return;
+    }
     mp_obj_range_t *o = o_in;
     if (attr == MP_QSTR_start) {
         dest[0] = mp_obj_new_int(o->start);
@@ -189,6 +193,6 @@
     .subscr = range_subscr,
     .getiter = range_getiter,
 #if MICROPY_PY_BUILTINS_RANGE_ATTRS
-    .load_attr = range_load_attr,
+    .attr = range_attr,
 #endif
 };
diff --git a/py/objtype.c b/py/objtype.c
index a5fb17f..cb71105 100644
--- a/py/objtype.c
+++ b/py/objtype.c
@@ -440,7 +440,7 @@
     }
 }
 
-void mp_obj_instance_load_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
+STATIC void mp_obj_instance_load_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
     // logic: look in instance members then class locals
     assert(is_instance_type(mp_obj_get_type(self_in)));
     mp_obj_instance_t *self = self_in;
@@ -512,7 +512,7 @@
     }
 }
 
-bool mp_obj_instance_store_attr(mp_obj_t self_in, qstr attr, mp_obj_t value) {
+STATIC bool mp_obj_instance_store_attr(mp_obj_t self_in, qstr attr, mp_obj_t value) {
     mp_obj_instance_t *self = self_in;
 
     #if MICROPY_PY_BUILTINS_PROPERTY || MICROPY_PY_DESCRIPTORS
@@ -602,6 +602,16 @@
     }
 }
 
+void mp_obj_instance_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
+    if (dest[0] == MP_OBJ_NULL) {
+        mp_obj_instance_load_attr(self_in, attr, dest);
+    } else {
+        if (mp_obj_instance_store_attr(self_in, attr, dest[1])) {
+            dest[0] = MP_OBJ_NULL; // indicate success
+        }
+    }
+}
+
 STATIC mp_obj_t instance_subscr(mp_obj_t self_in, mp_obj_t index, mp_obj_t value) {
     mp_obj_instance_t *self = self_in;
     mp_obj_t member[2] = {MP_OBJ_NULL};
@@ -774,52 +784,52 @@
     return o;
 }
 
-// for fail, do nothing; for attr, dest[0] = value; for method, dest[0] = method, dest[1] = self
-STATIC void type_load_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
-    assert(MP_OBJ_IS_TYPE(self_in, &mp_type_type));
-    mp_obj_type_t *self = self_in;
-#if MICROPY_CPYTHON_COMPAT
-    if (attr == MP_QSTR___name__) {
-        dest[0] = MP_OBJ_NEW_QSTR(self->name);
-        return;
-    }
-#endif
-    struct class_lookup_data lookup = {
-        .obj = self_in,
-        .attr = attr,
-        .meth_offset = 0,
-        .dest = dest,
-        .is_type = true,
-    };
-    mp_obj_class_lookup(&lookup, self);
-}
-
-STATIC bool type_store_attr(mp_obj_t self_in, qstr attr, mp_obj_t value) {
+STATIC void type_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
     assert(MP_OBJ_IS_TYPE(self_in, &mp_type_type));
     mp_obj_type_t *self = self_in;
 
-    // TODO CPython allows STORE_ATTR to a class, but is this the correct implementation?
+    if (dest[0] == MP_OBJ_NULL) {
+        // load attribute
+        #if MICROPY_CPYTHON_COMPAT
+        if (attr == MP_QSTR___name__) {
+            dest[0] = MP_OBJ_NEW_QSTR(self->name);
+            return;
+        }
+        #endif
+        struct class_lookup_data lookup = {
+            .obj = self_in,
+            .attr = attr,
+            .meth_offset = 0,
+            .dest = dest,
+            .is_type = true,
+        };
+        mp_obj_class_lookup(&lookup, self);
+    } else {
+        // delete/store attribute
 
-    if (self->locals_dict != NULL) {
-        assert(MP_OBJ_IS_TYPE(self->locals_dict, &mp_type_dict)); // Micro Python restriction, for now
-        mp_map_t *locals_map = mp_obj_dict_get_map(self->locals_dict);
-        if (value == MP_OBJ_NULL) {
-            // delete attribute
-            mp_map_elem_t *elem = mp_map_lookup(locals_map, MP_OBJ_NEW_QSTR(attr), MP_MAP_LOOKUP_REMOVE_IF_FOUND);
-            // note that locals_map may be in ROM, so remove will fail in that case
-            return elem != NULL;
-        } else {
-            // store attribute
-            mp_map_elem_t *elem = mp_map_lookup(locals_map, MP_OBJ_NEW_QSTR(attr), MP_MAP_LOOKUP_ADD_IF_NOT_FOUND);
-            // note that locals_map may be in ROM, so add will fail in that case
-            if (elem != NULL) {
-                elem->value = value;
-                return true;
+        // TODO CPython allows STORE_ATTR to a class, but is this the correct implementation?
+
+        if (self->locals_dict != NULL) {
+            assert(MP_OBJ_IS_TYPE(self->locals_dict, &mp_type_dict)); // Micro Python restriction, for now
+            mp_map_t *locals_map = mp_obj_dict_get_map(self->locals_dict);
+            if (dest[1] == MP_OBJ_NULL) {
+                // delete attribute
+                mp_map_elem_t *elem = mp_map_lookup(locals_map, MP_OBJ_NEW_QSTR(attr), MP_MAP_LOOKUP_REMOVE_IF_FOUND);
+                // note that locals_map may be in ROM, so remove will fail in that case
+                if (elem != NULL) {
+                    dest[0] = MP_OBJ_NULL; // indicate success
+                }
+            } else {
+                // store attribute
+                mp_map_elem_t *elem = mp_map_lookup(locals_map, MP_OBJ_NEW_QSTR(attr), MP_MAP_LOOKUP_ADD_IF_NOT_FOUND);
+                // note that locals_map may be in ROM, so add will fail in that case
+                if (elem != NULL) {
+                    elem->value = dest[1];
+                    dest[0] = MP_OBJ_NULL; // indicate success
+                }
             }
         }
     }
-
-    return false;
 }
 
 const mp_obj_type_t mp_type_type = {
@@ -828,8 +838,7 @@
     .print = type_print,
     .make_new = type_make_new,
     .call = type_call,
-    .load_attr = type_load_attr,
-    .store_attr = type_store_attr,
+    .attr = type_attr,
 };
 
 mp_obj_t mp_obj_new_type(qstr name, mp_obj_t bases_tuple, mp_obj_t locals_dict) {
@@ -865,8 +874,7 @@
     o->call = mp_obj_instance_call;
     o->unary_op = instance_unary_op;
     o->binary_op = instance_binary_op;
-    o->load_attr = mp_obj_instance_load_attr;
-    o->store_attr = mp_obj_instance_store_attr;
+    o->attr = mp_obj_instance_attr;
     o->subscr = instance_subscr;
     o->getiter = instance_getiter;
     //o->iternext = ; not implemented
@@ -921,8 +929,12 @@
     return mp_obj_new_super(args[0], args[1]);
 }
 
-// for fail, do nothing; for attr, dest[0] = value; for method, dest[0] = method, dest[1] = self
-STATIC void super_load_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
+STATIC void super_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest) {
+    if (dest[0] != MP_OBJ_NULL) {
+        // not load attribute
+        return;
+    }
+
     assert(MP_OBJ_IS_TYPE(self_in, &mp_type_super));
     mp_obj_super_t *self = self_in;
 
@@ -960,7 +972,7 @@
     .name = MP_QSTR_super,
     .print = super_print,
     .make_new = super_make_new,
-    .load_attr = super_load_attr,
+    .attr = super_attr,
 };
 
 mp_obj_t mp_obj_new_super(mp_obj_t type, mp_obj_t obj) {
diff --git a/py/objtype.h b/py/objtype.h
index 176e802..de1909b 100644
--- a/py/objtype.h
+++ b/py/objtype.h
@@ -37,9 +37,8 @@
     // TODO maybe cache __getattr__ and __setattr__ for efficient lookup of them
 } mp_obj_instance_t;
 
-// these need to be exposed for MICROPY_OPT_CACHE_MAP_LOOKUP_IN_BYTECODE to work
-void mp_obj_instance_load_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest);
-bool mp_obj_instance_store_attr(mp_obj_t self_in, qstr attr, mp_obj_t value);
+// this needs to be exposed for MICROPY_OPT_CACHE_MAP_LOOKUP_IN_BYTECODE to work
+void mp_obj_instance_attr(mp_obj_t self_in, qstr attr, mp_obj_t *dest);
 
 // these need to be exposed so mp_obj_is_callable can work correctly
 bool mp_obj_instance_is_callable(mp_obj_t self_in);
diff --git a/py/runtime.c b/py/runtime.c
index 2bb9a02..b04a4af 100644
--- a/py/runtime.c
+++ b/py/runtime.c
@@ -917,9 +917,9 @@
         dest[0] = (mp_obj_t)&mp_builtin_next_obj;
         dest[1] = obj;
 
-    } else if (type->load_attr != NULL) {
+    } else if (type->attr != NULL) {
         // this type can do its own load, so call it
-        type->load_attr(obj, attr, dest);
+        type->attr(obj, attr, dest);
 
     } else if (type->locals_dict != NULL) {
         // generic method lookup
@@ -961,8 +961,11 @@
 void mp_store_attr(mp_obj_t base, qstr attr, mp_obj_t value) {
     DEBUG_OP_printf("store attr %p.%s <- %p\n", base, qstr_str(attr), value);
     mp_obj_type_t *type = mp_obj_get_type(base);
-    if (type->store_attr != NULL) {
-        if (type->store_attr(base, attr, value)) {
+    if (type->attr != NULL) {
+        mp_obj_t dest[2] = {MP_OBJ_SENTINEL, value};
+        type->attr(base, attr, dest);
+        if (dest[0] == MP_OBJ_NULL) {
+            // success
             return;
         }
     }
diff --git a/py/vm.c b/py/vm.c
index d81f558..55203b0 100644
--- a/py/vm.c
+++ b/py/vm.c
@@ -315,7 +315,7 @@
                     MARK_EXC_IP_SELECTIVE();
                     DECODE_QSTR;
                     mp_obj_t top = TOP();
-                    if (mp_obj_get_type(top)->load_attr == mp_obj_instance_load_attr) {
+                    if (mp_obj_get_type(top)->attr == mp_obj_instance_attr) {
                         mp_obj_instance_t *self = top;
                         mp_uint_t x = *ip;
                         mp_obj_t key = MP_OBJ_NEW_QSTR(qst);
@@ -405,7 +405,7 @@
                     MARK_EXC_IP_SELECTIVE();
                     DECODE_QSTR;
                     mp_obj_t top = TOP();
-                    if (mp_obj_get_type(top)->store_attr == mp_obj_instance_store_attr && sp[-1] != MP_OBJ_NULL) {
+                    if (mp_obj_get_type(top)->attr == mp_obj_instance_attr && sp[-1] != MP_OBJ_NULL) {
                         mp_obj_instance_t *self = top;
                         mp_uint_t x = *ip;
                         mp_obj_t key = MP_OBJ_NEW_QSTR(qst);