Jak zmienić zmienną modułu z innego modułu?

107

Załóżmy, że mam pakiet o nazwie bari zawiera bar.py:

a = None

def foobar():
    print a

i __init__.py:

from bar import a, foobar

Następnie wykonuję ten skrypt:

import bar

print bar.a
bar.a = 1
print bar.a
bar.foobar()

Oto czego oczekuję:

None
1
1

Oto, co otrzymałem:

None
1
None

Czy ktoś może wyjaśnić moje błędne przekonanie?

shino
źródło

Odpowiedzi:

103

Używasz from bar import a. astaje się symbolem w globalnym zakresie modułu importującego (lub jakimkolwiek zakresie, w którym występuje instrukcja import).

Kiedy przypisujesz nową wartość do a, zmieniasz tylko apunkty wartości , a nie rzeczywistą wartość. Spróbuj zaimportować bar.pybezpośrednio z import barin __init__.pyi przeprowadź tam eksperyment, ustawiając bar.a = 1. W ten sposób faktycznie będziesz modyfikować, bar.__dict__['a']co jest „prawdziwą” wartością aw tym kontekście.

Jest trochę zagmatwany z trzema warstwami, ale bar.a = 1zmienia wartość aw module o nazwie, z barktórego faktycznie pochodzi __init__.py. Nie zmienia wartości tego, aco foobarwidzi, ponieważ znajduje się foobarw aktualnym pliku bar.py. Możesz ustawić, bar.bar.ajeśli chcesz to zmienić.

Jest to jedno z niebezpieczeństw stosowania from foo import barformy importinstrukcji: dzieli się ona barna dwa symbole, jeden widoczny globalnie od wewnątrz, od fooktórego zaczyna się wskazanie pierwotnej wartości i inny symbol widoczny w zakresie, w którym importinstrukcja jest wykonywana. Zmiana punktu, w którym symbol wskazuje, nie zmienia również wskazywanej wartości.

Tego rodzaju rzeczy są zabójcze, gdy próbujesz przejść reloaddo modułu z interaktywnego interpretera.

aaronasterling
źródło
Dzięki! Twoja odpowiedź prawdopodobnie uratowała mi kilka godzin szukania winowajcy.
jnns
Wygląda na to, że nawet to bar.bar.apodejście nie pomoże w większości przypadków użycia. Prawdopodobnie jest to słaby pomysł, aby mieszać kompilację lub ładowanie modułu i przypisania w czasie wykonywania, ponieważ w końcu będziesz mylić siebie (lub innych, którzy używają twojego kodu). Zwłaszcza w językach, które zachowują się nieco niekonsekwentnie, jak prawdopodobnie Python.
matanster
26

Jednym ze źródeł trudności z tym pytaniem jest to, że masz program o nazwie bar/bar.py: import barimportu albo bar/__init__.pyalbo bar/bar.py, w zależności od tego, gdzie to jest zrobione, co sprawia, że jest to trochę kłopotliwe, aby śledzić, które ajest bar.a.

Oto jak to działa:

Kluczem do zrozumienia tego, co się dzieje, aby uświadomić sobie, że w twojej __init__.py,

from bar import a

w efekcie robi coś podobnego

a = bar.a
# … where bar = bar/bar.py (as if bar were imported locally from __init__.py)

i definiuje nową zmienną ( bar/__init__.py:ajeśli chcesz). W ten sposób twój from bar import ain __init__.pywiąże nazwę bar/__init__.py:az oryginalnym bar.py:aobiektem ( None). Dlatego możesz to zrobić from bar import a as a2w __init__.py: w tym przypadku jest jasne, że masz obie bar/bar.py:ai odrębną nazwę zmiennej bar/__init__.py:a2(w twoim przypadku nazwy dwóch zmiennych po prostu są a, ale nadal istnieją w różnych przestrzeniach nazw: w __init__.py, są bar.ai a).

Teraz, kiedy to zrobisz

import bar

print bar.a

uzyskujesz dostęp do zmiennej bar/__init__.py:a(ponieważ import barimportuje twoją bar/__init__.py). To jest zmienna, którą modyfikujesz (do 1). Nie dotykasz zawartości zmiennej bar/bar.py:a. Więc kiedy później to zrobisz

bar.foobar()

wywołujesz bar/bar.py:foobar(), która uzyskuje dostęp do zmiennej az bar/bar.py, która jest nadal None(gdy foobar()jest zdefiniowana, wiąże nazwy zmiennych raz na zawsze, więc ain bar.pyjest bar.py:a, a nie żadna inna azmienna zdefiniowana w innym module - ponieważ może być wiele azmiennych we wszystkich zaimportowanych modułach ). Stąd ostatnie Nonewyjście.

Wniosek: najlepiej jest uniknąć wszelkich niejasności import bar, nie mając żadnego bar/bar.pymodułu (ponieważ bar.__init__.pysprawia, że ​​katalog bar/jest już pakietem, za pomocą którego można również importować import bar).

Eric O Lebigot
źródło
12

Innymi słowy: okazuje się, że to błędne przekonanie jest bardzo łatwe do zrobienia. Jest to podstępnie zdefiniowane w odwołaniu do języka Python: użycie obiektu zamiast symbolu . Sugerowałbym, aby odniesienie do języka Python uczyniło to bardziej przejrzystym i mniej rzadkim.

fromForma nie wiąże się nazwa modułu: przechodzi przez listę identyfikatorów, wygląda każdy z nich w module znajdują się w punkcie (1) i wiąże nazwę w lokalnej przestrzeni nazw do obiektu w ten sposób znaleźć.

JEDNAK:

Podczas importu importujesz bieżącą wartość importowanego symbolu i dodajesz ją do swojej przestrzeni nazw zgodnie z definicją. Nie importujesz odniesienia, efektywnie importujesz wartość.

Dlatego, aby uzyskać zaktualizowaną wartość i, musisz zaimportować zmienną, która zawiera odniesienie do tego symbolu.

Innymi słowy, import NIE jest taki jak importw JAVA, externaldeklaracja w C / C ++ ani nawet useklauzula w PERL.

Raczej poniższa instrukcja w Pythonie:

from some_other_module import a as x

jest bardziej podobny do następującego kodu w K&R C:

extern int a; /* import from the EXTERN file */

int x = a;

(uwaga: w przypadku Pythona „a” i „x” są zasadniczo odniesieniem do rzeczywistej wartości: nie kopiujesz INT, tylko kopiujesz adres odniesienia)

Mark Gerolimatos
źródło
Właściwie uważam, że sposób Pythona jest importznacznie bardziej przejrzysty niż Java, ponieważ przestrzenie / zakresy nazw powinny być zawsze odpowiednio oddzielone i nigdy nie kolidują ze sobą w nieoczekiwany sposób. Tak jak tutaj: zmiana powiązania obiektu z nazwą w przestrzeni nazw (czytaj: przypisanie czegoś do właściwości globalnej modułu) nigdy nie wpływa na inne przestrzenie nazw (czytaj: referencję, która została zaimportowana) w Pythonie. Ale robi to w Javie itp. W Pythonie wystarczy zrozumieć, co jest importowane, podczas gdy w Javie należy również zrozumieć inny moduł, na wypadek gdyby zmienił on później importowaną wartość.
Tino,
2
Muszę się z tym nie zgodzić. Importowanie / włączanie / używanie ma długą historię używania formularza referencyjnego, a nie formy wartości w każdym innym języku.
Mark Gerolimatos
4
Cholera, trafiłem na powrót… jest to oczekiwanie na import przez odniesienie (zgodnie z @OP). W rzeczywistości nowemu programistowi Pythona, bez względu na to, jak bardzo jest doświadczony, należy o tym powiedzieć w sposób „uważny”. To nigdy nie powinno się zdarzyć: w oparciu o powszechne użycie Python wybrał złą ścieżkę. W razie potrzeby utwórz „wartość importu”, ale nie łącz symboli z wartościami w momencie importu.
Mark Gerolimatos