Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

how to get the attribute of setter method of property in python

Please consider the below code

class DataMember():
  def __init__(self, **args):
     self.default = {"required" : False , "type" : "string" , "length": -1}
     self.default.update(args)
  def __call__(self , func):
     #Here I want to set the attribute to method 
     setattr(func , "__dbattr__" , self.default)
     def validate(obj , value):
        #some other code
        func(obj , value)
     return validate

This is my decorator method to decorate the setter method of property of other class, I want to set some attribute to the method. but it doesn't allow me.

I tried as below

class User(DbObject):
  def __init__(self):
      super(User , self)
      self._username = None
  @property
  def Name(self):
      return self._username

  @Name.setter
  @DataMember(length=100)
  def Name(self , value):
      self._username = value

 u = User()
 u.Name = "usernameofapp"
 print(u.Name)
 print(u.Name.__dbattr__)

Got the below error when ran this

Traceback (most recent call last):
File "datatypevalidator.py", line 41, in <module>
print(u.Name.__dbattr__)
AttributeError: 'str' object has no attribute '__dbattr__'

What am I doing wrong, and how can I set some attribute to setter method.

like image 781
Anubrij Chandra Avatar asked Aug 26 '18 10:08

Anubrij Chandra


People also ask

What is setter method in Python?

A setter is a method that sets the value of a property. In OOPs this helps to set the value to private attributes in a class. Basically, using getters and setters ensures data encapsulation. Reasons we use setters and getters are: For completeness of encapsulation.

What is property attribute in Python?

The property() method in Python provides an interface to instance attributes. It encapsulates instance attributes and provides a property, same as Java and C#. The property() method takes the get, set and delete methods as arguments and returns an object of the property class.

What is property and setter in Python?

Getters: These are the methods used in Object-Oriented Programming (OOPS) which helps to access the private attributes from a class. Setters: These are the methods used in OOPS feature which helps to set the value to private attributes in a class.


3 Answers

OK so there are three points of confusion here. Object identity, descriptor protocols and dynamic attributes.

First off, you are assigning __dbattr__ to func.

def __call__(self , func): 
    func.__dbattr__ = self.default  # you don't need setattr
    def validate(obj , value):
        func(obj , value)
    return validate

But this is assigning the attribute to func, which is then only held as a member of validate which in turn replaces func in the class (this is what decorators do ultimately, replace one function with another). So by placing this data on func, we lose access to it (well without some serious hacky __closure__ access). Instead, we should put the data on validate.

def __call__(self , func): 
    def validate(obj , value):
        # other code
        func(obj , value)
    validate.__dbattr__ = self.default
    return validate

Now, does u.Name.__dbattr__ work? No, you still get the same error, but the data is now accessible. To find it, we need to understand python's descriptor protocol which defines how properties work.

Read the linked article for a full explination but effectively, @property works by making an additional class with __get__, __set__ and __del__ methods which when you call inst.property what you actually do is call inst.__class__.property.__get__(inst, inst.__class__) (and similar for inst.property = value --> __set__ and del inst.property --> __del__(). Each of these in turn call the fget, fset and fdel methods which are references to the methods you defined in the class.

So we can find your __dbattr__ not on u.Name (which is the result of the User.Name.__get__(u, User) but on the User.Name.fset method itself! If you think about it (hard), this makes sense. This is the method you put it on. You didn't put it on the value of the result!

User.Name.fset.__dbattr__
Out[223]: {'length': 100, 'required': False, 'type': 'string'}

Right, so we can see this data exists, but it's not on the object we want. How do we get it onto that object? Well, it's actually quite simple.

def __call__(self , func):
    def validate(obj , value):
        # Set the attribute on the *value* we're going to pass to the setter
        value.__dbattr__ = self.default
        func(obj , value)
    return validate

This only works if ultimately the setter returns the value, but in your case it does.

# Using a custom string class (will explain later)
from collections import UserString

u = User()
u.Name = UserString('hello')
u.Name # --> 'hello'
u.Name.__dbattr__  # -->{'length': 100, 'required': False, 'type': 'string'}

You're probably wondering why I used a custom string class. Well if you use a basic string, you'll see the issue

u.Name = 'hello'
---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-238-1feeee60651f> in <module>()
----> 1 u.Name = 'hello'

<ipython-input-232-8529bc6984c8> in validate(obj, value)
      6 
      7         def validate(obj , value):
----> 8             value.__dbattr__ = self.default
      9             func(obj , value)
     10         return validate

AttributeError: 'str' object has no attribute '__dbattr__'

str objects, like most in-built types in python, do not allow random attribute assignment like custom python classes (collections.UserString is a python class wrapper around string that does allow random assignment).

So ultimately, what you originally wanted was impossible with built-in strings but using a custom class allows you to do it.

like image 125
FHTMitchell Avatar answered Nov 04 '22 03:11

FHTMitchell


You need to set the attribute on the wrapper function that is being returned by your decorator class's call method:

class DataMember():
  def __init__(self, **args):
     self.default = {"required" : False , "type" : "string" , "length": -1}
     self.default.update(args)
  def __call__(self , func):
     #Here I want to set the attribute to method
     def validate(obj , value):
        #some other code
        func(obj , value)
     setattr(validate , "__dbattr__" , self.default)
     return validate

class DbObject: pass

class User(DbObject):
    def __init__(self):
        super(User , self)
        self._username = None
    @property
    def Name(self):
        return self._username

    @Name.setter
    @DataMember(length=100)
    def Name(self , value):
        self._username = value

But note, it is not a method, since there is a property on the class, it instances will only ever return a string, the one returned by the getter. To access the setter, you have to do it indirectly through the property, which is on the class:

u = User()
u.Name = "usernameofapp"
print(u.Name)
print(User.Name.fset.__dbattr__)

which prints:

usernameofapp
{'required': False, 'type': 'string', 'length': 100}
like image 26
juanpa.arrivillaga Avatar answered Nov 04 '22 03:11

juanpa.arrivillaga


access __dbattr__ is a bit tricky:

first, you need get the property object:

p = u.__class__.__dict__['Name']

then get back the setter function object, named validate, which is defined inside DataMember.__call__:

setter_func = p.fset

then get back the underlying User.Name(self , value) function object from the closure of setter_func:

ori_func = setter_func.__closure__[0].cell_contents

now you could access __dbattr__:

>>> ori_func.__dbattr__
{'required': False, 'type': 'string', 'length': 100}

but is that useful? I don't know. maybe you could just set __dbattr__ on the validate function object that returned by DataMember.__call__, as other answers have pointed out.

like image 43
georgexsh Avatar answered Nov 04 '22 05:11

georgexsh