Repository: pynamodb/PynamoDB

Issue:
Model.refresh() doesn't respect attribute deletion
Let's consider this situation.
I have a model, like this:

```
class User(Model):
    name = UnicodeAttribute(hash_key=True)
    subscriptions = UnicodeSetAttribute()
```

Now, let's there is a user with just one subscription:

```
user = User(name='john', subscriptions=set(['news']))
user.save()
```

Then, in some other place of the program, we unsubscribe user from `news`:

```
u = User.get('john')
u.subscriptions.discard('news')
u.save()
```

And finally, in the initial object, we request a refresh:

```
user.refresh()
```

**What happens:**
`user` object has stale value of `subscriptions` field
**Expected behaviour:**
Disappeared value should be noticed, and `user.subscriptions` should be set to `None`.

**How to fix:**
The problem seems to be in `Model._deserialize` method. It walks over received data object and then for each field found in the model it updates that field. What would probably be better is to walk over model fields and update them from incoming data, or set them to `None` if not found.


Discussion:

Patch:
diff --git a/pynamodb/models.py b/pynamodb/models.py
index a44d9e0..5e77a78 100644
--- a/pynamodb/models.py
+++ b/pynamodb/models.py
@@ -894,18 +894,20 @@ class Model(AttributeContainer):
         Reconstructs a model object from JSON.
         """
         hash_key, attrs = data
-        hash_keyname = cls._get_meta_data().hash_keyname
-        hash_key_attr = cls._get_attributes().get(cls._dynamo_to_python_attr(hash_keyname))
-        hash_key = hash_key_attr.deserialize(hash_key)
         range_key = attrs.pop('range_key', None)
         attributes = attrs.pop(pythonic(ATTRIBUTES))
+        hash_keyname = cls._get_meta_data().hash_keyname
+        hash_keytype = cls._get_meta_data().get_attribute_type(hash_keyname)
+        attributes[hash_keyname] = {
+            hash_keytype: hash_key
+        }
         if range_key is not None:
             range_keyname = cls._get_meta_data().range_keyname
             range_keytype = cls._get_meta_data().get_attribute_type(range_keyname)
             attributes[range_keyname] = {
                 range_keytype: range_key
             }
-        item = cls(hash_key)
+        item = cls()
         item._deserialize(attributes)
         return item
 
@@ -1260,14 +1262,13 @@ class Model(AttributeContainer):
 
         :param attrs: A dictionary of attributes to update this item with.
         """
-        for name, attr in attrs.items():
-            attr_name = self._dynamo_to_python_attr(name)
-            attr_instance = self._get_attributes().get(attr_name, None)
-            if attr_instance:
-                attr_type = ATTR_TYPE_MAP[attr_instance.attr_type]
-                value = attr.get(attr_type, None)
+        for name, attr in self._get_attributes().items():
+            value = attrs.get(attr.attr_name, None)
+            if value is not None:
+                value = value.get(ATTR_TYPE_MAP[attr.attr_type], None)
                 if value is not None:
-                    setattr(self, attr_name, attr_instance.deserialize(value))
+                    value = attr.deserialize(value)
+            setattr(self, name, value)
 
     def _serialize(self, attr_map=False, null_check=True):
         """


Test patch:
diff --git a/pynamodb/tests/test_model.py b/pynamodb/tests/test_model.py
index 0f15b96..0e90d20 100644
--- a/pynamodb/tests/test_model.py
+++ b/pynamodb/tests/test_model.py
@@ -702,10 +702,12 @@ class ModelTestCase(TestCase):
 
         with patch(PATCH_METHOD) as req:
             req.return_value = GET_MODEL_ITEM_DATA
+            item.picture = b'to-be-removed'
             item.refresh()
             self.assertEqual(
                 item.custom_user_name,
                 GET_MODEL_ITEM_DATA.get(ITEM).get('user_name').get(STRING_SHORT))
+            self.assertIsNone(item.picture)
 
     def test_complex_key(self):
         """
@@ -4207,8 +4209,8 @@ class ModelTestCase(TestCase):
                 'mapy': {'M': {'baz': {'S': 'bongo'}}}
             }
         }
-        instance = ExplicitRawMapModel(map_attr=map_native)
-        instance._deserialize(map_serialized)
+        instance = ExplicitRawMapModel()
+        instance._deserialize({'map_attr': map_serialized})
         actual = instance.map_attr
         for k, v in six.iteritems(map_native):
             self.assertEqual(v, actual[k])