Bloquer la portée en Python

93

Lorsque vous codez dans d'autres langues, vous créez parfois une étendue de bloc, comme ceci:

statement
...
statement
{
    statement
    ...
    statement
}
statement
...
statement

Un des objectifs (parmi tant d'autres) est d'améliorer la lisibilité du code: montrer que certaines instructions forment une unité logique ou que certaines variables locales ne sont utilisées que dans ce bloc.

Existe-t-il une manière idiomatique de faire la même chose en Python?

Johan Råde
la source
2
One purpose (of many) is to improve code readability- Le code Python, écrit correctement (c'est-à-dire suivant le zen de python ) n'aurait pas besoin d'une telle garniture pour être lisible. En fait, c'est l'une des (nombreuses) choses que j'aime chez Python.
Burhan Khalid
J'ai essayé de jouer avec __exit__et withdéclaration, en changeant le globals()mais j'ai échoué.
Ruggero Turra
1
il serait très utile de définir une durée de vie variable, liée à l'acquisition de ressources
Ruggero Turra
24
@BurhanKhalid: Ce n'est pas vrai. Le zen de Python ne vous empêche pas de polluer une portée locale avec une variable temporaire ici et là. Si vous transformez chaque utilisation d'une seule variable temporaire en définissant par exemple une fonction imbriquée qui est appelée immédiatement, le zen de Python ne sera pas heureux non plus. Limiter explicitement la portée d'une variable est un outil pour améliorer la lisibilité, car il répond directement "ces identifiants sont-ils utilisés ci-dessous?" - une question qui peut se poser en lisant même le code Python le plus élégant.
bluenote10
17
@BurhanKhalid C'est bien de ne pas avoir de fonctionnalité. Mais appeler ça «zen» est tout simplement dégoûtant.
Phil

Réponses:

80

Non, il n'y a pas de prise en charge du langage pour créer une étendue de bloc.

Les constructions suivantes créent la portée:

  • module
  • classe
  • fonction (y compris lambda)
  • expression de générateur
  • compréhensions (dict, set, list (en Python 3.x))
ThomasH
la source
37

La manière idiomatique en Python est de garder vos fonctions courtes. Si vous pensez que vous en avez besoin, refactorisez votre code! :)

Python crée une nouvelle portée pour chaque module, classe, fonction, expression de générateur, compréhension de dict, compréhension d'ensemble et en Python 3.x également pour chaque compréhension de liste. En dehors de cela, il n'y a pas de portées imbriquées à l'intérieur des fonctions.

Sven Marnach
la source
12
"La chose la plus importante dans la programmation est la capacité de donner un nom à quelque chose. La deuxième chose la plus importante est de ne pas être obligé de donner un nom à quelque chose." Pour la plupart, Python nécessite que les portées (pour les variables, etc.) reçoivent des noms. À cet égard, les variables Python sont le deuxième test le plus important.
Krazy Glew
19
Les éléments les plus importants de la programmation sont la possibilité de gérer les dépendances de votre application et de gérer les portées des blocs de code. Les blocs anonymes vous permettent de limiter la durée de vie des rappels, alors que sinon, vos rappels ne sont utilisés qu'une seule fois, mais vivent pendant toute la durée du programme, ce qui entraîne un encombrement global de la portée et nuit à la lisibilité du code.
Dmitry
Je viens de remarquer que les variables sont également locales pour les compréhensions dict / set. J'ai essayé Python 2.7 et 3.3, mais je ne sais pas si cela dépend de la version.
wjandrea
1
@wjandrea Vous avez raison - ajouté à la liste. Il ne devrait y avoir aucune différence entre les versions de Python pour ces derniers.
Sven Marnach
3
Je reformulerais la dernière phrase, car vous pouvez très bien créer des fonctions dans des fonctions. Il y a donc des portées imbriquées dans les fonctions.
ThomasH
17

Vous pouvez faire quelque chose de similaire à une portée de bloc C ++ en Python en déclarant une fonction dans votre fonction, puis en l'appelant immédiatement. Par exemple:

def my_func():
    shared_variable = calculate_thing()

    def do_first_thing():
        ... = shared_variable
    do_first_thing()

    def do_second_thing():
        foo(shared_variable)
        ...
    do_second_thing()

Si vous ne savez pas pourquoi vous voudrez peut-être faire cela, cette vidéo pourrait vous convaincre.

Le principe de base est de tout définir aussi étroitement que possible sans introduire de `` garbage '' (types / fonctions supplémentaires) dans une portée plus large que ce qui est absolument nécessaire - Rien d'autre ne veut utiliser la do_first_thing()méthode par exemple, elle ne doit donc pas être étendue en dehors du fonction d'appel.

Ben
la source
C'est aussi la façon dont les développeurs Google utilisent dans les didacticiels TensorFlow, comme on le voit par exemple ici
Nino Filiu
13

Je suis d'accord qu'il n'y a pas de portée de blocage. Mais un endroit dans python 3 le fait SEMBLER comme s'il avait une portée de bloc.

qu'est-il arrivé qui a donné ce regard? Cela fonctionnait correctement dans python 2. mais pour arrêter les fuites de variables dans python 3, ils ont fait cette astuce et ce changement donne l'impression qu'il a une portée de bloc ici.

Laisse-moi expliquer.


Selon l'idée de portée, lorsque nous introduisons des variables avec les mêmes noms dans la même portée, sa valeur doit être modifiée.

c'est ce qui se passe dans python 2

>>> x = 'OLD'
>>> sample = [x for x in 'NEW']
>>> x
'W'

Mais dans python 3, même si la variable avec le même nom est introduite, elle ne remplace pas, la compréhension de la liste agit comme un bac à sable pour une raison quelconque et semble créer une nouvelle portée.

>>> x = 'OLD'
>>> sample = [x for x in 'NEW']
>>> x
'OLD'

et cette réponse va à l'encontre de la déclaration de answerer @ Thomas Le seul moyen de créer une portée est des fonctions, des classes ou des modules parce que cela ressemble à un autre endroit pour créer une nouvelle portée.

Harish Kayarohanam
la source
0

Les modules (et packages) sont un excellent moyen pythonique de diviser votre programme en espaces de noms séparés, ce qui semble être un objectif implicite de cette question. En effet, alors que j'apprenais les bases de Python, je me sentais frustré par l'absence de fonctionnalité de portée de bloc. Cependant, une fois que j'ai compris les modules Python, je pourrais réaliser plus élégamment mes objectifs précédents sans avoir besoin d'une portée de bloc.

En tant que motivation et pour orienter les gens vers la bonne direction, je pense qu'il est utile de donner des exemples explicites de certaines des constructions de portée de Python. J'explique d'abord ma tentative infructueuse d'utiliser des classes Python pour implémenter la portée de bloc. Ensuite, j'explique comment j'ai réalisé quelque chose de plus utile en utilisant les modules Python. À la fin, je décris une application pratique des packages au chargement et au filtrage des données.

Tentative de portée de bloc avec des classes

Pendant quelques instants, j'ai pensé que j'avais atteint la portée du bloc en collant du code à l'intérieur d'une déclaration de classe:

x = 5
class BlockScopeAttempt:
    x = 10
    print(x) # Output: 10
print(x) # Output: 5

Malheureusement, cela se décompose lorsqu'une fonction est définie:

x = 5 
class BlockScopeAttempt: 
    x = 10
    print(x) # Output: 10
    def printx2(): 
        print(x) 
    printx2() # Output: 5!!!

C'est parce que les fonctions définies dans une classe utilisent une portée globale. Le moyen le plus simple (mais pas le seul) de résoudre ce problème est de spécifier explicitement la classe:

x = 5 
class BlockScopeAttempt: 
    x = 10
    print(x) # Output: 10
    def printx2(): 
        print(BlockScopeAttempt.x)  # Added class name
    printx2() # Output: 10

Ce n'est pas si élégant car il faut écrire les fonctions différemment selon qu'elles sont ou non contenues dans une classe.

De meilleurs résultats avec les modules Python

Les modules sont très similaires aux classes statiques, mais les modules sont beaucoup plus propres d'après mon expérience. Pour faire de même avec les modules, je crée un fichier appelé my_module.pydans le répertoire de travail courant avec le contenu suivant:

x = 10
print(x) # (A)

def printx():
    global x
    print(x) # (B)

Ensuite, dans mon fichier principal ou session interactive (par exemple Jupyter), je fais

x = 5
import my_module # Output: 10 from (A)
my_module.printx() # Output: 10 from (B)
print(x) # Output: 5

Comme explication, chaque fichier Python définit un module qui a son propre espace de noms global. L'importation d'un module vous permet d'accéder aux variables de cet espace de noms avec la .syntaxe.

Si vous travaillez avec des modules dans une session interactive, vous pouvez exécuter ces deux lignes au début

%load_ext autoreload
%autoreload 2

et les modules seront automatiquement rechargés lorsque leurs fichiers correspondants seront modifiés.

Packages de chargement et de filtrage des données

L'idée de packages est une légère extension du concept de modules. Un package est un répertoire contenant un __init__.pyfichier (éventuellement vide) , qui est exécuté lors de l'importation. Les modules / packages de ce répertoire sont accessibles avec la .syntaxe.

Pour l'analyse des données, j'ai souvent besoin de lire un gros fichier de données, puis d'appliquer de manière interactive divers filtres. La lecture d'un fichier prend plusieurs minutes, donc je ne veux le faire qu'une seule fois. Sur la base de ce que j'ai appris à l'école sur la programmation orientée objet, j'avais l'habitude de penser qu'il fallait écrire le code de filtrage et de chargement en tant que méthodes dans une classe. Un inconvénient majeur de cette approche est que si je redéfinis ensuite mes filtres, la définition de ma classe change, donc je dois recharger toute la classe, y compris les données.

Aujourd'hui, avec Python, je définis un package appelé my_dataqui contient des sous-modules nommés loadet filter. À l'intérieur de filter.pyje peux faire une importation relative:

from .load import raw_data

Si je modifie filter.py, puis autoreloaddétectera les changements. Il ne se recharge pas load.py, donc je n'ai pas besoin de recharger mes données. De cette façon, je peux prototyper mon code de filtrage dans un bloc-notes Jupyter, l'envelopper en tant que fonction, puis couper-coller de mon bloc-notes directement dans filter.py. Comprendre cela a révolutionné mon flux de travail et m'a converti de sceptique à un croyant au «Zen of Python».

Ben Mares
la source