1

I have a function make_package returning a class Package. In another file, I want to import class Package, so I can do type check. My question is, how to import a class that's inside a function?

Following is not the exact code, but similar in structure.

# project/package.py
def make_package(ori, dest):
    class Package:
        origin = ori
        destination = dest
        def __init__(self, item):
            self.item = item
        def to_str(self):
            return self.origin + '-' + self.destination + ' : ' + self.item
    return Package

# project/shipment.py
# Don't know how to import Package from package.py
class Shipment:
    def __init__(self, **kwargs):
        self.shipment = dict()
        for container in kwargs.keys():
            self.shipment[container] = list()
            for item in kwargs[key]:
                if type(item) != Package:
                    raise TypeError('Item must be packed in Package.')
                self.shipment[container].append(item.to_str())

# project/main.py
from .package import make_package
from .shipment import Shipment
us_fr = make_package('US', 'FR')
fr_cn = make_package('FR', 'CN')
shipment = Shipment(container1=[us_fr('item1'), fr_cn('item2')], container2=[us_fr('item3'), fr_cn('item4')])
print(shipment.shipment)
# {
#     'container1' : [
#         'US-FR : item1',
#         'FR-CN' : 'item2'
#     ],
#     'container2' : [
#         'US-FR : item3',
#         'FR-CN' : 'item4'
#     ]
# }

I know one way I can achieve type check is make a dummy variable with make_package inside Shipment, then compare type(item) to type(dummy). However it seems more like a hack. I wonder if there's a better way?

Phoenix-C
  • 23
  • 1
  • 3
  • 5
    Why is the class inside the function? Why not leave it at the global scope? What are you trying to achieve by putting it inside the function? – Bryan Oakley Aug 12 '19 at 23:54
  • The best solution is to just move `Package` outside of the function. Unless you are going to use `make_package` as a factory function in the future, you don't even need that function. – Kevin Welch Aug 12 '19 at 23:58
  • Because I want the class inherit `ori` and `dest` value from the function, but only create object with `item`. This way I can reuse the variable, in example, `us_fr` many times, since I will be use it a lot more later on. If I put the class outside, I probably have to pass 3 arguments `__init__(ori, dest, item)` every time. Which later makes the code super messy. – Phoenix-C Aug 13 '19 at 00:04
  • @KevinWelch in my real code, there're a lot more class variables `Package` inherit from `make_package`'s parameters. It gets really messy if I create `Package` object with all those parameters every time. I feel like it's way cleaner if I can use `make_package` to assign those params first, then only pass in `item` when create new instance, since `item` is the only variable that's different every time. – Phoenix-C Aug 13 '19 at 00:22
  • 1
    @Phoenix-C: Would using `functools.partial` to bind the common parameters to make a constructor that only needs `item` work? That would keep `Package` a singleton, while still letting you have simplified interfaces that construct it with common data. – ShadowRanger Aug 13 '19 at 00:34

1 Answers1

4

There is no way to "import" your class from outside the function, because in reality, there is no one Package type. Every time you call make_package(), it's creating a new type, which happens to have the same name as all the other Package types. However, it is still a unique type, so it will never compare equal to another Package type, even if origin and destination are the same.

You could make all Package types inherit from a "marker" class, and then use isinstance to check if an item is a package:

# project/package.py
class PackageMarker:
    pass

def make_package(ori, dest):
    class Package(PackageMarker):
        ...  # Package body here
    return Package

# project/shipment.py
class Shipment:
    def __init__(self, **kwargs):
        self.shipment = dict()
        for container in kwargs.keys():
            self.shipment[container] = list()
            for item in kwargs[key]:
                if not isinstance(item, PackageMarker):
                    raise TypeError('Item must be packed in Package.')
                self.shipment[container].append(item.to_str())

In my opinion, the way you are creating classes with this factory function is confusing, but perhaps I simply need more context to see why you're doing it this way.

If you want my opinion on how I would refactor it, I would remove the factory function, and do something like this:

# project/package.py
class Package:
    def __init__(self, route, item):
        self.route = route
        self.item = item

    def to_str(self):
        return self.route.to_str() + ' : ' + self.item

# project/route.py
class Route:
    def __init__(self, origin, destination):
        self.origin = origin
        self.destination = destination

    def to_str(self):
        return self.origin + ' - ' + self.destination

# project/shipment.py
from .package import Package

class Shipment:
    def __init__(self, **kwargs):
        self.shipment = dict()
        for container in kwargs.keys():
            self.shipment[container] = list()
            for item in kwargs[key]:
                if not isinstance(item, Package):
                    raise TypeError('Item must be packed in Package.')
                self.shipment[container].append(item.to_str())

# project/main.py
from .package import Package
from .route import Route
from .shipment import Shipment
us_fr = Route('US', 'FR')
fr_cn = Route('FR', 'CN')
shipment = Shipment(
    container1=[Package(us_fr, 'item1'), Package(fr_cn, 'item2')],
    container2=[Package(us_fr, 'item3'), Package(fr_cn, 'item4')]
)
Tobotimus
  • 305
  • 1
  • 6
  • So I'm writing this package for my company internal use (so treat the `main.py` as testing script). The reason I didn't have a parent class outside is because this way, others can write a custom class that extends `PackageMarker` and it'll bypass the type check. I kinda really want to restrict the type to be `Package` only. I'm open to suggestion to rewrite the whole thing. – Phoenix-C Aug 13 '19 at 00:42
  • @Phoenix-C in light of that, I've added a possible implementation which changes your original design more radically. In my opinion, this version is more understandable in terms of the traditional modelling we use in OOP. – Tobotimus Aug 13 '19 at 00:56