@property or not?

Here’s the snippet of code im working with:

class VolumeInfo(BaseModel):
    name: Path
    suffix: str
    format: str
    size: Size
    page_count: int
    pages: tuple[PageInfo, ...] = tuple()

    @property
    def smallest_page(self) -> PageInfo:
        data = {page.size: page for page in self.pages}
        return data[min(data.keys())]

    @property
    def median_page(self) -> PageInfo:
        from statistics import median_low
        data = {page.size: page for page in self.pages}
        return data[median_low(data.keys())]
    
    def largest_page(self) -> PageInfo:
        data = {page.size: page for page in self.pages}
        return data[max(data.keys())]
    
    def unique_resolutions(self) -> tuple[tuple[Resolution, int], ...]:
        from collections import Counter
        return tuple(Counter(page.resolution for page in self.pages).most_common())

This made me wonder, do I mark these methods as properties or leave them as methods? How do I decide in the first place? Is it a matter of taste or is there a standard for it?

Hi,

the @property decorator allows you to call the method just like you would an attribute without the need for the parentheses pair, (), as is required for normal methods. For example, if you have the following class:

class SomeClass:

    def __init__(self, first_name, last_name):

        self.f_name = first_name
        self.l_name = last_name

    @property
    def print_me(self):
        print('\nMy name is: {} {}'.format(self.f_name, self.l_name))

    def other_method(self):

        print('\nInquiring about @property.')

obj = SomeClass('Jack', 'Smith')

obj.print_me      # No need for parenthesis
print(obj.f_name) # Call attribute
obj.other_method()  # Uses parenthesis

Using the @property is optional and not a requirement. It is more of coding preferences - for the particular example code that you have provided. There are other uses of course, for example, as in using them for decorating setters, getters and deleters methods.

1 Like

The general contract is that properties should be “fast” - this is not well defined, but amortized O(1) is a good target. You could accomplish this by caching the dictionary and a sorted list of keys. (You should think about what happens if there is more than one page with the exact same size. That could throw off your median calculation in pathological cases.) Of course if this class is mutable then you’d need to track when the cached values are “dirty”.

It’s also generally worth aiming for consistency.

Aside from that, the normal way to get the min and max “by” some attribute is by just using the key keyword argument:

return min(self.pages, key=lambda page: page.size)

However, that won’t work with statistics.median_low.

1 Like

Thank you! Think I understand this a bit better now. Also appreciate the min/max by key tip!

For some more context, this is because they look like attributes.

Choosing how things look affects how people think about using your
class. If it looks like an attribute, people will consider it nearly
free to use.

Even if the value gets cached, it the initial computation is expensive
I’d avoid making it a property with some special reason.

3 Likes

Also, properties should not have side-effects, and should avoid raising anything other than AttributeError.

I believe this should very definitely not be a property.

2 Likes