28

I have a Python module that contains a number of classes, each representing a particular physical material with its properties (e.g., density, specific heat). Some of the properties are just float members of the class, but many depend on some parameter, e.g., the temperature. I implemented this through @staticmethods, i.e., all of the classes look like

class Copper:
    magnetic_permeability = 1.0

    @staticmethod
    def density(T):
        return 1.0 / (-3.033e-9 + 68.85e-12*T - 6.72e-15*T**2 + 8.56e-18*T**3)

    @staticmethod
    def electric_conductivity(T, p):
        return 1.0141 * T**2 * p

    @staticmethod
    def specific heat(T):
        return # ...


class Silver:
    # ...

class Argon:
    # ...

# ...

The Classes thus merely act as containers for all the data, and the abundance of @staticmethods has me suspecting that there may be a more appropriate design pattern for this use case.

Any hints?

Nico Schlömer
  • 53,797
  • 27
  • 201
  • 249
  • If it's static... that means that it's basically fixed values. If so, it's really data, and might be better handled as more traditional data structure. dictionary if you want to create it live, something like xml/yaml if you want to load if from disk. i even wrote a set of classes once that would load an XML and expose it as attributes, so you could still do things like `metals.copper.magnetic_permeability * 4` even though `metals` was loaded from an XML. something like that might give you the best of both worlds... – Corley Brigman Feb 19 '14 at 14:36
  • 1
    But the properties are dependant on temperature etc. – Jayanth Koushik Feb 19 '14 at 14:37
  • 1
    staticmethod actually seems quite appropriate. – Jayanth Koushik Feb 19 '14 at 14:38
  • Do all the densities depend on `1.0 / (a + b*T - c*T**2 + d*T**3)`, all the electric conductivities on `a * T**2 * p`, all the magnetic permeabilities constant, etc? – Mike Graham Feb 19 '14 at 14:58
  • 1
    @MikeGraham No. The relationships get more complex and vary across the materials. – Nico Schlömer Feb 19 '14 at 15:02
  • 2
    Usually, if you have a class at all, it's because you need to maintain some state. If your class is just staticmethods, then they're really just a bunch of functions and you're using the class to "namespace" the functions. It's a bit code smelly in my opinion, not the pythonic way to do namespacing. – wim Feb 19 '14 at 15:08
  • Sounds like it is okay for a class to have only static methods see [this](https://stackoverflow.com/questions/1942903/is-it-bad-practice-for-a-class-to-have-only-static-fields-and-methods) – cardamom Nov 05 '18 at 10:03

6 Answers6

12

You could name your module copper and create all of these as module level functions, then import copper; copper.density(0).

But what if someone does from copper import density, and you also have a module called cobalt and another called carbon and another called chlorine etc., all with their own density functions? Uh oh.

Since we're all consenting adults here, you can document this and expect your users to know well enough to import just the module. Or you can take your approach; in this case, I would consider putting all of your elements in one module called elements, then the user can from elements import Copper. Static methods would then be appropriate.

2rs2ts
  • 10,662
  • 10
  • 51
  • 95
9

I suspect that a more fitting structure would be to have a Material class, which takes either functions or coefficients as arguments, e.g.

class Material(object):

    def __init__(self, mag_perm, density_coeffs, ...):
        self.mag_perm = mag_perm
        self._density_coeffs = density_coeffs
        ...

    def density(self, T):
        x0, x1, x2, x3 = self._density_coeffs
        return 1.0 / (x0 + (x1 * T) + (x2 * (T ** 2)) + (x3 * (T ** 3)))

Each material then supplies its own coefficients for each calculated parameter:

copper = Material(1.0, (-3.033e-9, 68.85e-12, 6.72e-15, 8.56e-18), ...)
copper.density(300)

If you need more complex relationships (e.g. different calculations) you could use sub-classes of Material and over-load the appropriate calculations.

jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
  • Good - levelling up in the abstraction is appropriate here. – wim Feb 19 '14 at 15:04
  • Having a method called `density` when you pass in a variable also called `density` to `__init__` (which is really a tuple of coefficients used in some calculation of the density), is kind of an insane interface though! – wim Feb 19 '14 at 15:12
  • @MikeGraham that's not the issue -- the problem is that density was not a sensible name for the tuple of four coefficients. – wim Feb 19 '14 at 21:11
2

This is really a "season to taste" question. You could do as you've done -- methods on a class OR you could eliminate the class entirely, and just go with module level functions. In general, I prefer whichever is simpler to read/understand & maintain.

Based strictly on the limited example you shared -- I lean towards module level functions. But of course, a more fleshed out example might alter that opinion.

user590028
  • 11,364
  • 3
  • 40
  • 57
2

How about making the variable properties functions that take all required values as arguments?

def density(T):
    <some function of T>

def electrical_conductivity(T, p):
    <some function of T and p>

def some_other_property(T, magnetic_permeability):
    <some function of T and magnetic permeability>

Then, the fixed properties could be defined through a dictionary.

copper_fixed_properties = {'magnetic_permeability': 1, ...}

You would use this in the following way:

copper_some_other_property = some_other_property(T, copper.magnetic_permeability)
BenMorel
  • 34,448
  • 50
  • 182
  • 322
Jayanth Koushik
  • 9,476
  • 1
  • 44
  • 52
1

Defining a staticmethod is virtually always a mistake. Python has functions, so you'd always just define a module-level function. (You'd have copper.py and inside of it have a plain old def density(T): instead of using a staticmethod.)

That is to say, copper.py would look like

magnetic_permeability = 1.0

def density(T):
    return 1.0 / (-3.033e-9 + 68.85e-12*T - 6.72e-15*T**2 + 8.56e-18*T**3)

def electric_conductivity(T, p):
    return 1.0141 * T**2 * p

def specific heat(T):
    return ...

In this particular case, do you actually have multiple materials? If so, then you probably want them to be instances, not classes or modules. If you e.g., don't want them all to have the same rational cubic form for the thermal density dependence, you can make a subclass and have an instance of that or you can make a class that accepts functions as arguments.

class Material(object):
    def __init__(self, density, electric conductivity):
        self.density = density
        self.electric_conductivity = electric_conductivity

copper = Material(
    density=lambda T: 1.0 / (-3.033e-9 + 68.85e-12*T - 
                             6.72e-15*T**2 + 8.56e-18*T**3),
    electric_conductivity=lambda T, p: 1.0141 * T**2 * p
)

You can also make a metaclass if you want to maintain a declarative style.


By the way

class Copper():
    def __init__(self):
        self.magnetic_permeability = 1.0
    ...

probably doesn't do what you want to. This makes magnetic_permeability only accessible in an instance of copper. I don't recommend using classes rather than instances or modules for this, but if you did, you'd need to do

class Copper(object):
    magnetic_permeability = 1.0
    ...

to be able to do Copper.magnetic_permeability


Note that I'm inheriting with object so that we're using Python 2 "new style classes". The changes are subtle, but it's nicer if you just ensure you'll never run into them.

Mike Graham
  • 73,987
  • 14
  • 101
  • 130
0

While agreeing the other answers, here's a decorator that makes all methods of the decorated class a staticmethod:

import types

def staticclass(cls):
    for fname, f in cls.__dict__.items():
        if isinstance(f, types.FunctionType):
           setattr(cls, fname, staticmethod(f))
    return cls

Usage example:

@staticclass
class Copper:
    magnetic_permeability = 1.0

    def density(T):
        return 1.0 / (-3.033e-9 + 68.85e-12*T - 6.72e-15*T**2 + 8.56e-18*T**3)

    def electric_conductivity(T, p):
        return 1.0141 * T**2 * p

    def specific_heat(T):
        return # ...
Berci
  • 544
  • 1
  • 7
  • 10