Using numpy in python

In julia, we have both import and using.

In python, it seems that we have only import.

What is the counterpart of using in python?

It would help if you said what the difference was in Juila!

In Python, import math makes a local name math that refers to the module math so that you can then refer things in it, e.g. math.sin, math.cos.

The alternative form is from math import sin, cos which makes local names sin and cos that refer to sin and cos from the module math.

It’s also possible to import all of the public names from module math using from math import *, but this is discouraged.

1 Like

For reference re Julia:

https://docs.julialang.org/en/v1/manual/modules/

Julia’s simple using apparently brings both the module name and the components on its “export list” (roughly equivalent to __all__, except it apparently has special syntactic support instead of relying on a “magic” attribute with special semantics) into the current namespace. As such, the closest equivalent is something like

import numpy
from numpy import *

However, the use of star-imports is much less common in Python, as is the use of explicit __all__ to customize the behaviour of a star-import.

Julia’s using with an explicit list is straightforward:

from numpy import byte, sin, zeros # arbitrarily chosen for the example

Julia’s import and using also have implications for the semantics of adding “methods” (this seems to mean specializations based on the static type of the arguments?) to functions. Python’s functions don’t have this, so such distinctions are moot. The standard library provides a dispatch decorator that works on the dynamic type of a single argument; decorators are just syntactic sugar for calling a higher-order function, and this one dynamically creates a new function object. (The functions thus decorated then gain their own, function-specific decorator for “registering” dispatch functions - this looks to me roughly equivalent to Julia’s “methods” but dynamically typed. In Python, “method” only means a function scoped to within a class, or more precisely, looked up on an instance of that class.)

If an imported function was already using that decorator, nothing would prevent the local code from registering special-case dispatch, and such registration would normally affect other code within the same process that had imported the module (because module imports are cached, such that everyone who imports the same module gets the same object). It would not matter how the initial import was done, because it’s the same, common object being modified.

However, an ordinarily-defined function wouldn’t have that functionality. To register dispatches, it would have to be decorated with the dispatch functionality first, which creates a new object. This can be done after the fact (without the decorator syntax), but it would not modify the source module in any way.

The source module can be modified explicitly by any code that can refer to that actual object:

from functools import singledispatch
import other_code

other_code.a_function = singledispatch(other_code.a_function)

@other_code.a_function.register(int)
def specialization(value):
    ...

Again, all other code which imports the other_code module will “see” that change. Note that from other_code import a_function is importing other_code - it just isn’t putting that name into the namespace. It will rely on the same cache of module objects, and look up the other_code attribute from that module to dump into the current namespace.

But we couldn’t make this initial change after just from other_code import a_function, because we wouldn’t have a way to refer to the other_code module so as to modify it.

The import syntax in Python is just syntactic sugar for some variable assignment, on top of the underlying machinery for actually looking up or creating a module object. Examining the bytecode:

>>> import dis
>>> dis.dis('import numpy')
  1           0 LOAD_CONST               0 (0)
              2 LOAD_CONST               1 (None)
              4 IMPORT_NAME              0 (numpy)
              6 STORE_NAME               0 (numpy)
              8 LOAD_CONST               1 (None)
             10 RETURN_VALUE
>>> dis.dis('from numpy import byte, sin, zeros')
  1           0 LOAD_CONST               0 (0)
              2 LOAD_CONST               1 (('byte', 'sin', 'zeros'))
              4 IMPORT_NAME              0 (numpy)
              6 IMPORT_FROM              1 (byte)
              8 STORE_NAME               1 (byte)
             10 IMPORT_FROM              2 (sin)
             12 STORE_NAME               2 (sin)
             14 IMPORT_FROM              3 (zeros)
             16 STORE_NAME               3 (zeros)
             18 POP_TOP
             20 LOAD_CONST               2 (None)
             22 RETURN_VALUE

IMPORT_FROM is basically just doing an attribute assignment. IMPORT_NAME is implemented by the built-in __import__ function. However, this code can’t be directly exactly emulated with function calls and assignments, because import from only makes one IMPORT_NAME call and also doesn’t store the result anywhere except the virtual machine’s stack.

2 Likes