1

I have a triangle class with vertices, area, and contours. What is the Pythonic way of setting multiple members in a constructor using One method, if needing to set three members in a Constructor at once?

I'm using dictionary method below, however it seems clunky. Wondering if this is appropriate or any optimal method?

This question is a variation of this question, What is a clean, Pythonic way to have multiple constructors in Python?

class TriangleData():

    vertices_key: str = 'vertices'
    triangle_area_key: str = 'triangle_area'
    contours_key: str = 'contours'
     

    def __init__(self, contour_data: np.ndarray):
        self.contour_data = contour_data
        data = self.get_triangle_data()
        self.vertices: np.ndarray = data[self.vertices_key]
        self.triangle_area: float = data[self.triangle_area_key]
        self.poly_dp_contours: np.ndarray = data[self.contours_key]
        self.color = self.get_color()

    def get_triangle_data(self):

        # Run calculations from original contour dataetc etc
       .....

        return {
            self.vertices_key: vertices_final,
            self.triangle_area_key: triangle_area,
            self.contours_key: contours
        }
mattsmith5
  • 540
  • 4
  • 29
  • 67
  • 2
    Doesn't look like you're using `contour_data` at all. – Randy Apr 04 '21 at 00:54
  • I am, just hiding it in the method #comments and ... @Randy – mattsmith5 Apr 04 '21 at 00:57
  • How? You're not passing it to `self.get_triangle_data()`. – Randy Apr 04 '21 at 01:41
  • yeah, its somewhere in here, ... between the dots and comments moment_data = cv2.moments(self.contour_data) @Randy – mattsmith5 Apr 04 '21 at 01:43
  • You'd need to explicitly set `self.contour_data = contour_data` for that to work – Randy Apr 04 '21 at 01:44
  • 2
    Anyway, I realize that's a bit off the track of your question. On a deleted answer that suggested just assigning the attributes in the function, you said your linter didn't like assignment of attributes outside of `__init__`, but linters are generally just style suggestions, so that's what I'd personally do. If you don't like that, you could have your method just `return vertices_final, triangle_area, contours` and then have `self.vertices, self.triangle_area, self.contours = self.get_triangle_data()` in the init – Randy Apr 04 '21 at 01:47
  • ok, feel free to place in answer @Randy I was reading from here, also https://stackoverflow.com/a/38378757/15435022 – mattsmith5 Apr 04 '21 at 01:50
  • 1
    Can you please clarify your issue? Setting three attributes in a constructor is no different than setting three names anywhere. It seems there is a lot of unrelated baggage to this question - why does `get_triangle_data` return a dict? Why is it public, why is it a method instead of a classmethod/constructor? What is the role of the three `XYZ_key` attributes - do you expect them to be changed? Why all this baggage instead of a simple tuple + multiple assignment? – MisterMiyagi Apr 04 '21 at 05:16
  • Probably the reason is that this is not a synthetic example, and some parts of code may become useless in scope of a project if we'll keep simplifying them to be perfect on their own. – madbird Apr 04 '21 at 05:52
  • hi @MisterMiyagi agreed, I was reading dictionary method from here, will switch to tuple prob https://www.geeksforgeeks.org/g-fact-41-multiple-return-values-in-python/ – mattsmith5 Apr 04 '21 at 06:57

3 Answers3

2

This boils down to stylistic preferences. I personally don't have qualms with having some of the attributes defined outside of __init__(...), and so would opt for something like:

class TriangleData():

    def __init__(self, contour_data: np.ndarray):
        self.get_triangle_data(contour_data)

    def get_triangle_data(self, contour_data):
        # calculations
        self.vertices = vertices_final
        self.triangle_area = triangle_area
        self.contours = contours

and disable any linter warnings that didn't like it. You could also opt for something like:

class TriangleData():

    def __init__(self, contour_data: np.ndarray):
        self.vertices, self.triangle_area, self.contours = self.get_triangle_data(contour_data)

    def get_triangle_data(self, contour_data):
        # calculations
        return vertices_final, triangle_area, contours

which I think should satify the linter as-is.

Randy
  • 14,349
  • 2
  • 36
  • 42
1

There is a wide spectrum of approaches and libraries to eliminate redundancy of a plain __init__(). Please take a look at the dataclasses, attrs, NamedTuple and Pydantic.


UPD: I'm agree with Randy: there's nothing wrong about setting attributes outside of __init__.

Beside of that, you could update instance dict directly. This would be better in case when the final set of fields should be computed dynamically.

class TriangleData():

    def __init__(self, contour_data: np.ndarray):
        self.contour_data = contour_data
        data = self.get_triangle_data()

        # That's strange, that you return that field from `get` method 
        # with different name, than you need to bind. Could we avoid that?
        # data['poly_dp_contours'] = data.pop('contorous')

        self.set_triangle_data(**data)

    def set_triangle_data(self, vertices, triangle, contorous):
        self.vertices = vertices
        self.triangle = triangle
        self.contorous = contorous

    # or even like below

    def set_triangle_data(self, **data):
        valid_keys = ['vertices', 'triangle', 'contorous', <...>]
        vars(self).update({k: data[k] for k in valid_keys if k in data})
            
        
    def get_triangle_data(self):

        # Run calculations from original contour dataetc etc
        .....

        return {
            self.vertices_key: vertices_final,
            self.triangle_area_key: triangle_area,
            self.contours_key: contours,
        }
madbird
  • 1,326
  • 7
  • 11
0

To follow the pattern of setting/initializing variables in the constructor, from Instance variables in methods outside the constructor (Python) -- why and how? (see answer from BrianOakley and others)

You can declare your object members as follows, and them set them in a method afterwards.

def __init__(self, contour_data: np.ndarray):
    self.contour_data = contour_data
    self.vertices: np.ndarray = np.asarray([])
    self.triangle_area: float = float()
    self.contour_final: np.ndarray = np.asarray([])

    self.set_triangle_data()

def set_triangle_data(self):
    # Run calculations from original contour data etc
    .....
    self.vertices = vertices_final,
    self.triangle_area = triangle_area
    self.contour_final = contours
mattsmith5
  • 540
  • 4
  • 29
  • 67