Using dataclasses with dependent attributes via property - python

I have a class, for example Circle, which has dependent attributes, radius and circumference. It makes sense to use a dataclass here because of the boilerplate for __init__, __eq__, __repr__ and the ordering methods (__lt__, ...).
I choose one of the attributes to be dependent on the other, e.g. the circumference is computed from the radius. Since the class should support initialization with either of the attributes (+ have them included in __repr__ as well as dataclasses.asdict) I annotate both:
from dataclasses import dataclass
import math
#dataclass
class Circle:
radius: float = None
circumference: float = None
#property
def circumference(self):
return 2 * math.pi * self.radius
#circumference.setter
def circumference(self, val):
if val is not type(self).circumference: # <-- awkward check
self.radius = val / (2 * math.pi)
This requires me to add the somewhat awkward check for if val is not type(self).circumference because this is what the setter will receive if no value is provided to __init__.
Then if I wanted to make the class hashable by declaring frozen=True I need to change self.radius = ... to object.__setattr__(self, 'radius', ...) because otherwise this would attempt to assign to a field of a frozen instance.
So my question is if this is a sane way of using dataclasses together with properties or if potential (non-obvious) obstacles lie ahead and I should refrain from using dataclasses in such cases? Or maybe there is even a better way of achieving this goal?

For starters, you could set the attributes in the __init__ method as follows:
from dataclasses import dataclass, InitVar
import math
#dataclass(frozen=True, order=True)
class CircleWithFrozenDataclass:
radius: float = 0
circumference: float = 0
def __init__(self, radius=0, circumference=0):
super().__init__()
if circumference:
object.__setattr__(self, 'circumference', circumference)
object.__setattr__(self, 'radius', circumference / (2 * math.pi))
if radius:
object.__setattr__(self, 'radius', radius)
object.__setattr__(self, 'circumference', 2 * math.pi * radius)
This will still provide you with all the helpful __eq__, __repr__, __hash__, and ordering method injections. While object.__setattr__ looks ugly, note that the CPython implementation itself uses object.__setattr__ to set attributes when injecting the generated __init__ method for a frozen dataclass.
If you really want to get rid of object.__setattr__, you can set frozen=False (the default) and override the __setattr__ method yourself. This is copying how the frozen feature of dataclasses is implemented in CPython. Note that you will also have to turn on unsafe_hash=True as __hash__ is no longer injected since frozen=False.
#dataclass(unsafe_hash=True, order=True)
class CircleUsingDataclass:
radius: float = 0
circumference: float = 0
_initialized: InitVar[bool] = False
def __init__(self, radius=0, circumference=0):
super().__init__()
if circumference:
self.circumference = circumference
self.radius = circumference / (2 * math.pi)
if radius:
self.radius = radius
self.circumference = 2 * math.pi * radius
self._initialized = True
def __setattr__(self, name, value):
if self._initialized and \
(type(self) is __class__ or name in ['radius', 'circumference']):
raise AttributeError(f"cannot assign to field {name!r}")
super().__setattr__(name, value)
def __delattr__(self, name, value):
if self._initialized and \
(type(self) is __class__ or name in ['radius', 'circumference']):
raise AttributeError(f"cannot delete field {name!r}")
super().__delattr__(name, value)
In my opinion, freezing should only happen after the __init__ by default, but for now I will probably use the first approach.

Related

How to implement a python class function that uses class and current object (self)?

I know we can explicitly write the name of the class to get class attributes, and when you need the method to get class attributes that change from object to object, then the 'self' as the first parameter, should work fine.
However, I don't really know how to do both in a fancy way.
class Alpha:
base = 0
round = 360
def __init__(self, radius: float, angel: float = 0):
self.radius = radius
self.angel = angel
def spin(self, angel: float = 0, K: int = 0):
# need to use both 'base' & 'self.radius'
# should I use it like that?
if self.radius is not Alpha.base and angel is not None:
self.angel = (angel + (360 * K))
It's ok to use it as I did?
Your way is fine if you're not going to subclass it
However if you are going to subclass it then use the following:
class Alpha:
base = 0
round = 360
def __init__(self, radius: float, angel: float = 0):
self.radius = radius
self.angel = angel
def spin(self, angel: float = 0, K: int = 0):
# need to use both 'base' & 'self.radius'
# should I use it like that?
if self.radius is not self.__class__.base and angel is not None:
self.angel = (angel + (360 * K))
self.__class__ returns the class and not the object, however
class Alpha:
base = 0
round = 360
def __init__(self, radius: float, angel: float = 0):
self.radius = radius
self.angel = angel
def spin(self, angel: float = 0, K: int = 0):
# need to use both 'base' & 'self.radius'
# should I use it like that?
if self.radius is not self.base and angel is not None:
self.angel = (angel + (360 * K))
Would also work because the class variables are carried into the object, this would not work as intended with functions that do not have self as first
class foo:
def bar(foobar):
return foobar * 2
foobar = foo()
foobar.bar("foo")
This would result in
TypeError: bar() takes 1 positional argument but 2 were given or similar error
To prevent this error just do add the decorator staticmethod like the example given below:
class foo:
#staticmethod
def bar(foobar):
return foobar * 2
foobar = foo()
foobar.bar("foo")
>>> foofoo
It's ok to use it as I did?
I think this is the right way to create static class variables. I couldn't find any official resource, but lots of famous tutorial websites (like geeksforgeeks or tutorialspoint) explain this technique.
Update: Here it is the official documentation.

Convention for referencing class attributes?

What is the convention for referencing class attributes? I understand that there are two ways of referencing class attributes:
class Circle1:
pi = 3.14
def __init__(self, radius=1):
self.radius = radius
def get_circumference():
return 2 * self.pi * self.radius
class Circle2:
pi = 3.14
def __init__(self, radius=2):
self.radius = radius
def get_circumference():
return 2 * Circle2.pi * self.radius
The first way is referencing it as self.attribute, while the second way is to reference it as Class.attribute. Which way is the convention for referencing class attributes? Or is there no convention and is it just a matter of preference?
It really depends on how you're going to use it. Class.attribute will set it as an attribut for all instances of that class, while self.attribute is only for specific instances. In your case, pi is always (around) 3.14, so it should be a class attribute, while the radius is specific to any one circle, so it should be set as self.radius.
Also, to help shorten your code, you should only use 1 class of circles, and make the radius mandatory (don't specify default) because that is the only thing that changes.

How to type annotate overrided methods in a subclass?

Say I already have a method with type annotations:
class Shape:
def area(self) -> float:
raise NotImplementedError
Which I will then subclass multiple times:
class Circle:
def area(self) -> float:
return math.pi * self.radius ** 2
class Rectangle:
def area(self) -> float:
return self.height * self.width
As you can see, I'm duplicating the -> float quite a lot. Say I have 10 different shapes, with multiple methods like this, some of which contain parameters too. Is there a way to just "copy" the annotation from the parent class, similar to what functools.wraps() does with docstrings?
This might work, though I'm sure to miss the edge cases, like additional arguments:
from functools import partial, update_wrapper
def annotate_from(f):
return partial(update_wrapper,
wrapped=f,
assigned=('__annotations__',),
updated=())
which will assign "wrapper" function's __annotations__ attribute from f.__annotations__ (keep in mind that it is not a copy).
According to documents the update_wrapper function's default for assigned includes __annotations__ already, but I can see why you'd not want to have all the other attributes assigned from wrapped.
With this you can then define your Circle and Rectangle as
class Circle:
#annotate_from(Shape.area)
def area(self):
return math.pi * self.radius ** 2
class Rectangle:
#annotate_from(Shape.area)
def area(self):
return self.height * self.width
and the result
In [82]: Circle.area.__annotations__
Out[82]: {'return': builtins.float}
In [86]: Rectangle.area.__annotations__
Out[86]: {'return': builtins.float}
As a side effect your methods will have an attribute __wrapped__, which will point to Shape.area in this case.
A less standard (if you can call the above use of update_wrapper standard) way to accomplish handling of overridden methods can be achieved using a class decorator:
from inspect import getmembers, isfunction, signature
def override(f):
"""
Mark method overrides.
"""
f.__override__ = True
return f
def _is_method_override(m):
return isfunction(m) and getattr(m, '__override__', False)
def annotate_overrides(cls):
"""
Copy annotations of overridden methods.
"""
bases = cls.mro()[1:]
for name, method in getmembers(cls, _is_method_override):
for base in bases:
if hasattr(base, name):
break
else:
raise RuntimeError(
'method {!r} not found in bases of {!r}'.format(
name, cls))
base_method = getattr(base, name)
method.__annotations__ = base_method.__annotations__.copy()
return cls
and then:
#annotate_overrides
class Rectangle(Shape):
#override
def area(self):
return self.height * self.width
Again, this will not handle overriding methods with additional arguments.
You can use a class decorator to update your subclass methods annotations. In your decorator you will need to walk through your class definition then update only those methods that are present in your superclass. Of course to access the superclass you need to use the it __mro__ which is just the tuple of the class, subclass, till object. Here we are interested in the second element in that tuple which is at index 1 thus __mro__[1] or using the cls.mro()[1]. Last and not least your decorator must return the class.
def wraps_annotations(cls):
mro = cls.mro()[1]
vars_mro = vars(mro)
for name, value in vars(cls).items():
if callable(value) and name in vars_mro:
value.__annotations__.update(vars(mro).get(name).__annotations__)
return cls
Demo:
>>> class Shape:
... def area(self) -> float:
... raise NotImplementedError
...
>>> import math
>>>
>>> #wraps_annotations
... class Circle(Shape):
... def area(self):
... return math.pi * self.radius ** 2
...
>>> c = Circle()
>>> c.area.__annotations__
{'return': <class 'float'>}
>>> #wraps_annotations
... class Rectangle(Shape):
... def area(self):
... return self.height * self.width
...
>>> r = Rectangle()
>>> r.area.__annotations__
{'return': <class 'float'>}

Can named arguments be used with Python enums?

Example:
class Planet(Enum):
MERCURY = (mass: 3.303e+23, radius: 2.4397e6)
def __init__(self, mass, radius):
self.mass = mass # in kilograms
self.radius = radius # in meters
Ref: https://docs.python.org/3/library/enum.html#planet
Why do I want to do this? If there are a few primitive types (int, bool) in the constructor list, it would be nice to used named arguments.
While you can't use named arguments the way you describe with enums, you can get a similar effect with a namedtuple mixin:
from collections import namedtuple
from enum import Enum
Body = namedtuple("Body", ["mass", "radius"])
class Planet(Body, Enum):
MERCURY = Body(mass=3.303e+23, radius=2.4397e6)
VENUS = Body(mass=4.869e+24, radius=6.0518e6)
EARTH = Body(mass=5.976e+24, radius=3.3972e6)
# ... etc.
... which to my mind is cleaner, since you don't have to write an __init__ method.
Example use:
>>> Planet.MERCURY
<Planet.MERCURY: Body(mass=3.303e+23, radius=2439700.0)>
>>> Planet.EARTH.mass
5.976e+24
>>> Planet.VENUS.radius
6051800.0
Note that, as per the docs, "mix-in types must appear before Enum itself in the sequence of bases".
The accepted answer by #zero-piraeus can be slightly extended to allow default arguments as well. This is very handy when you have a large enum with most entries having the same value for an element.
class Body(namedtuple('Body', "mass radius moons")):
def __new__(cls, mass, radius, moons=0):
return super().__new__(cls, mass, radius, moons)
def __getnewargs__(self):
return (self.mass, self.radius, self.moons)
class Planet(Body, Enum):
MERCURY = Body(mass=3.303e+23, radius=2.4397e6)
VENUS = Body(mass=4.869e+24, radius=6.0518e6)
EARTH = Body(5.976e+24, 3.3972e6, moons=1)
Beware pickling will not work without the __getnewargs__.
class Foo:
def __init__(self):
self.planet = Planet.EARTH # pickle error in deepcopy
from copy import deepcopy
f1 = Foo()
f2 = deepcopy(f1) # pickle error here
For Python 3.6.1+ the typing.NamedTuple can be used, which also allows for setting default values, which leads to prettier code. The example by #shao.lo then looks like this:
from enum import Enum
from typing import NamedTuple
class Body(NamedTuple):
mass: float
radius: float
moons: int=0
class Planet(Body, Enum):
MERCURY = Body(mass=3.303e+23, radius=2.4397e6)
VENUS = Body(mass=4.869e+24, radius=6.0518e6)
EARTH = Body(5.976e+24, 3.3972e6, moons=1)
This also supports pickling. The typing.Any can be used if you don't want to specify the type.
Credit to #monk-time, who's answer here inspired this solution.
If going beyond the namedtuple mix-in check out the aenum library1. Besides having a few extra bells and whistles for Enum it also supports NamedConstant and a metaclass-based NamedTuple.
Using aenum.Enum the above code could look like:
from aenum import Enum, enum, _reduce_ex_by_name
class Planet(Enum, init='mass radius'):
MERCURY = enum(mass=3.303e+23, radius=2.4397e6)
VENUS = enum(mass=4.869e+24, radius=6.0518e6)
EARTH = enum(mass=5.976e+24, radius=3.3972e6)
# replace __reduce_ex__ so pickling works
__reduce_ex__ = _reduce_ex_by_name
and in use:
--> for p in Planet:
... print(repr(p))
<Planet.MERCURY: enum(radius=2439700.0, mass=3.3030000000000001e+23)>
<Planet.EARTH: enum(radius=3397200.0, mass=5.9760000000000004e+24)>
<Planet.VENUS: enum(radius=6051800.0, mass=4.8690000000000001e+24)>
--> print(Planet.VENUS.mass)
4.869e+24
1 Disclosure: I am the author of the Python stdlib Enum, the enum34 backport, and the Advanced Enumeration (aenum) library.

Python: confused with classes, attributes and methods in OOP

I'm learning Python OOP now and confused with somethings in the code below.
Questions:
def __init__(self, radius=1):
What does the argument/attribute "radius = 1" mean exactly?
Why isn't it just called "radius"?
The method area() has no argument/attribute "radius".
Where does it get its "radius" from in the code?
How does it know that the radius is 5?
class Circle:
pi = 3.141592
def __init__(self, radius=1):
self.radius = radius
def area(self):
return self.radius * self.radius * Circle.pi
def setRadius(self, radius):
self.radius = radius
def getRadius(self):
return self.radius
c = Circle()
c.setRadius(5)
Also,
In the code below, why is the attribute/argument name missing in the brackets?
Why was is not written like this: def __init__(self, name)
and def getName(self, name)?
class Methods:
def __init__(self):
self.name = 'Methods'
def getName(self):
return self.name
The def method(self, argument=value): syntax defines a keyword argument, with a default value. Using that argument is now optional, if you do not specify it, the default value is used instead. In your example, that means radius is set to 1.
Instances are referred to, within a method, with the self parameter. The name and radius values are stored on self as attributes (self.name = 'Methods' and self.radius = radius) and can later be retrieved by referring to that named attribute (return self.name, return self.radius * self.radius * Circle.pi).
I can heartily recommend you follow the Python tutorial, it'll explain all this and more.
def __init__(self, radius=1):
self.radius = radius
This is default value setting to initialize a variable for the class scope.This is to avoid any garbage output in case some user calls c.Area() right after c = Circle().
In the code below, why is the attribute/argument "name" missing in the brackets?
In the line self.name = 'Methods' you are creating a variable name initialized to string value Methods.
Why was is not written like this: def init(self, name) and def
getName(self, name)?
self.name is defined for the class scope. You can get and set its value anywhere inside the class.
The syntax radius = 1 specifies a parameter "radius" which has a default value of 1:
def my_func(param=1):
... print(param)
...
my_func() #uses the default value
1
my_func(2) #uses the value passed
2
Note that in python there exists more kinds of parameters: positional and keyword parameters, or both.
Usually parameters can be assigned both using the positional notation and the keyword:
>>> def my_func(a,b,c):
... print (a,b,c)
...
>>> my_func(1,2,3)
(1, 2, 3)
>>> my_func(1,2,c=3)
(1, 2, 3)
Python uses "explicit" instance passing, so the first self parameter is used to pass the instance on which the methods are called. You can think of self as being the this of Java. But you must always use it to access instance attributes/methods. You can't call just area(), you must say self.area().
When you do self.attribute = 1 you create a new attribute attribute with value 1 and assign it to the instance self. So in the area() method self.radius refers to the radius attribute of the self instance.
The __init__ method is a special method. It's something similar to a constructor.
It is called when you instantiate the class. Python has a lot of these "special methods", for example the method __add__(self, other) is called when using the operator +.

Categories

Resources