Okay, I admit I’m tagging. But there’s a good chance you don’t know what this article is about.
Some time ago, I wrote an introductory article on descriptors, from which you learned how to define descriptors and how descriptors work.
If you haven’t already, you can read it here: Why Does Python use descriptors
The descriptors that normal people have seen are the ones mentioned in the previous article. What I want to say is that this is just one of the most common uses of the descriptor protocol. In case you didn’t know, there are many Python features that are implemented based on the descriptor protocol. Familiar ones like @property, @classMethod, @staticMethod, and super.
These decorator methods are absolutely familiar to you, but today is not about how to use them, but how to implement them yourself in pure Python.
Let’s talk about property first.
With the foundation of the first article, we know the basic usage of property. I’m going to cut to the chase here and simplify the examples from the first post.
class Student:
def __init__(self, name):
self.name = name
@property
def math(self):
return self._math
@math.setter
def math(self, value):
if 0 <= value <= 100:
self._math = value
else:
raise ValueError("Valid value must be in [0, 100]")
Copy the code
Just to briefly review its use, functions decorated with property, such as math in the example, become properties of Student instances. Assigning to the Math property goes into logical code blocks that use math.setter decorator functions.
Why is property underlying the descriptor protocol? Click on the source code of property through PyCharm. Unfortunately, it is just a pseudo-source code like a document without its specific implementation logic.
However, from this pseudo-source magic function structure composition, you can generally know its implementation logic.
Here, I realized the class property feature myself by imitating its function structure and combining “descriptor protocol”.
The code is as follows:
class TestProperty(object):
def __init__(self, fget=None, fset=None, fdel=None, doc=None):
self.fget = fget
self.fset = fset
self.fdel = fdel
self.__doc__ = doc
def __get__(self, obj, objtype=None):
print("in __get__")
if obj is None:
return self
if self.fget is None:
raise AttributeError
return self.fget(obj)
def __set__(self, obj, value):
print("in __set__")
if self.fset is None:
raise AttributeError
self.fset(obj, value)
def __delete__(self, obj):
print("in __delete__")
if self.fdel is None:
raise AttributeError
self.fdel(obj)
def getter(self, fget):
print("in getter")
return type(self)(fget, self.fset, self.fdel, self.__doc__)
def setter(self, fset):
print("in setter")
return type(self)(self.fget, fset, self.fdel, self.__doc__)
def deleter(self, fdel):
print("in deleter")
return type(self)(self.fget, self.fset, fdel, self.__doc__)
Copy the code
And then the Student class, we’ll change it to the following
class Student:
def __init__(self, name):
self.name = name
# This is the only place that has changed
@TestProperty
def math(self):
return self._math
@math.setter
def math(self, value):
if 0 <= value <= 100:
self._math = value
else:
raise ValueError("Valid value must be in [0, 100]")
Copy the code
To minimize your confusion, here are two things:
-
With TestProperty decoration, Math is no longer a function, but an instance of the TestProperty class. So the second Math function can be decorated with math.setter, essentially calling testProperty. setter to produce a new instance of TestProperty assigned to the second Math.
-
The first math and the second Math are two different TestProperty instances. But they both belong to the same descriptor class (TestProperty), which goes into testProperty.__set__ when math is assigned, and testProperty.__get__ when math is evaluated. On closer inspection, the _math attribute of the Student instance is actually accessed in the end.
With all that said, let’s just run it a little bit more intuitively.
This line is printed directly after the TestProperty is instantiated and assigned to the second Math
in setter
>>>
>>> s1.math = 90
in __set__
>>> s1.math
in __get__
90
Copy the code
For those who have difficulty understanding how property works, please refer to the two points I wrote above. If you have other questions, please add wechat to discuss with me.
1.17.4 How to Implement StaticMethod Based on Descriptors
@classMethod and @staticMethod.
I’ve defined a class that implements static methods in two ways.
class Test:
@staticmethod
def myfunc(a):
print("hello")
# the top and the bottom are equivalent
class Test:
def myfunc(a):
print("hello")
# Important: This is where the descriptor comes in
myfunc = staticmethod(myfunc)
Copy the code
So these two things are equivalent, just like property, but they’re also equivalent.
@TestProperty
def math(self):
return self._math
math = TestProperty(fget=math)
Copy the code
Let’s get back to staticMethod.
From the comments above, you can see that StaticMethod is really just a descriptor class, and myFunc becomes a descriptor at this point. For staticMethod implementation, you can refer to the following code I wrote myself.
Calling this method tells you that each time it is called, it passes through the __get__ of the descriptor class.
>>> Test.myfunc()
in staticmethod __get__
hello
>>> Test().myfunc()
in staticmethod __get__
hello
Copy the code
1.17.4 How to Implement ClassMethod based on descriptors
Same thing with classmethods.
class classmethod(object):
def __init__(self, f):
self.f = f
def __get__(self, instance, owner=None):
print("in classmethod __get__")
def newfunc(*args):
return self.f(owner, *args)
return newfunc
class Test:
def myfunc(cls):
print("hello")
# Important: This is where the descriptor comes in
myfunc = classmethod(myfunc)
Copy the code
The verification results are as follows
>>> Test.myfunc()
in classmethod __get__
hello
>>> Test().myfunc()
in classmethod __get__
hello
Copy the code
Property, StaticMethod, and classMethod relationships with descriptors. I think you have a better understanding of how descriptors are used in Python. I’ll leave it up to you to implement super.