module with classes with only static methods

2020-06-07 07:00发布

问题:

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(object):
    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(object):
    ...

class Argon(object):
    ...

...

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?

回答1:

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.



回答2:

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.



回答3:

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.



回答4:

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)


回答5:

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.